<rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Anime-and-Manga | AkitaOnRails.com</title><link>https://akitaonrails.github.io/en/tags/anime-and-manga/</link><description>Fabio Akita's blog — tech, career, and assorted geek topics. English edition.</description><generator>Hugo -- gohugo.io</generator><language>en</language><lastBuildDate>Mon, 19 Oct 2026 23:20:51 +0000</lastBuildDate><atom:link href="https://akitaonrails.github.io/en/tags/anime-and-manga/index.xml" rel="self" type="application/rss+xml"/><item><title>[Manga-Downloadr] Improving the Crystal/Ruby from bursts to pool stream</title><link>https://akitaonrails.github.io/en/2016/06/07/manga-downloadr-improving-the-crystal-ruby-from-bursts-to-pool-stream/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/06/07/manga-downloadr-improving-the-crystal-ruby-from-bursts-to-pool-stream/</guid><pubDate>Tue, 07 Jun 2016 17:13:00 -0300</pubDate><description>&lt;p&gt;Yesterday &lt;a href="http://www.akitaonrails.com/en/2016/06/06/manga-downloadr-portando-de-crystal-para-ruby-e-um-pouco-de-jruby"target="_blank" rel="noopener"&gt;I posted&lt;/a&gt; how I built a new implementation of the Manga Reader Downloader in Crystal, ported to Ruby, tested on JRuby and compared against Elixir. Just to recap, these were the results to fetch chapters, pages and image links of a sample manga:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;MRI Ruby 2.3.1: 57 seconds&lt;/li&gt;
&lt;li&gt;JRuby 9.1.1.0: 45 seconds&lt;/li&gt;
&lt;li&gt;Crystal 0.17.0: 59 seconds&lt;/li&gt;
&lt;li&gt;Elixir: 14 seconds&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I also said how it was an unfair comparison as the Elixir version uses a different - and obviously more efficient - algorithm.&lt;/p&gt;</description><content:encoded><![CDATA[<p>Yesterday <a href="http://www.akitaonrails.com/en/2016/06/06/manga-downloadr-portando-de-crystal-para-ruby-e-um-pouco-de-jruby"target="_blank" rel="noopener">I posted</a> how I built a new implementation of the Manga Reader Downloader in Crystal, ported to Ruby, tested on JRuby and compared against Elixir. Just to recap, these were the results to fetch chapters, pages and image links of a sample manga:</p>
<ul>
<li>MRI Ruby 2.3.1: 57 seconds</li>
<li>JRuby 9.1.1.0: 45 seconds</li>
<li>Crystal 0.17.0: 59 seconds</li>
<li>Elixir: 14 seconds</li>
</ul>
<p>I also said how it was an unfair comparison as the Elixir version uses a different - and obviously more efficient - algorithm.</p>
<p>It was the &ldquo;first-version-that-worked&rdquo; so I decided to go ahead and improve the implementations. In the Ruby/JRuby version I added the <a href="https://github.com/meh/ruby-thread"target="_blank" rel="noopener">thread</a> gem to have a good enough implementation of a Thread pool that works for Ruby and JRuby. I probably should have used Concurrent-Ruby but I was having some trouble making FixedThreadPool work.</p>
<p>Anyway, now all versions will have a constant pool of requests running and we can make a better comparison.</p>
<p>Another thing that may have skewed the results against Crystal is that it seems to have a <a href="https://github.com/crystal-lang/crystal/issues/2660"target="_blank" rel="noopener">faulty DNS resolver</a> implementation, so for now I just added the Manga Reader IP address directly to my <code>/etc/hosts</code> file to avoid <code>getaddrinfo: Name or service not known</code> exceptions.</p>
<p>To begin, let&rsquo;s test the same Elixir implementation, and as expected the result is still the same:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>11,49s user 0,82s system 77% cpu 15,827 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now, the MRI Ruby with the &ldquo;batch burst&rdquo; algorithm was taking 57 seconds and this new implementation using a ThreadPool runs much better:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>12,67s user 0,92s system 50% cpu 27,149 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Crystal, it&rsquo;s a bit more complicated as there is no way to implement the equivalent of a &ldquo;Fiber Pool&rdquo;. What we have to do is run an infinite loop until the last process signals a loop break. Within the loop we create a maximum number of fibers, wait for each one to signal that it finished through an individual channel and loop again to create a new fiber, and so on. Compared to yesterday&rsquo;s 59 seconds, this is much better:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>5,29s user 0,33s system 26% cpu 21,166 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The JRuby version is not so fast though. Still better than yesterday&rsquo;s 45 seconds but now it&rsquo;s losing even to MRI:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>49,24s user 1,41s system 146% cpu 34,602 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I tried to use the <code>--dev</code> flag for <a href="https://github.com/jruby/jruby/wiki/Improving-startup-time"target="_blank" rel="noopener">faster start up time</a> and it does improve a bit, getting it closer to MRI:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>22,26s user 0,99s system 76% cpu 30,320 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Not sure if it can be improved more though, any tips are welcome - don&rsquo;t forget to comment below.</p>
<p>So, Elixir is still at least twice as fast as Crystal at this point. But this also demonstrates how a different algorithm does make a huge difference where it matters. I can probably tweak it a bit more but this should suffice for now.</p>
<h3>Changing the Ruby implementation to use ThreadPool<span class="hx:absolute hx:-mt-20" id="changing-the-ruby-implementation-to-use-threadpool"></span>
    <a href="#changing-the-ruby-implementation-to-use-threadpool" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;thread/pool&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">MangaDownloadr</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">Concurrency</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">pool</span>    <span class="o">=</span> <span class="no">Thread</span><span class="o">.</span><span class="n">pool</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">mutex</span>   <span class="o">=</span> <span class="no">Mutex</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">      <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">collection</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="n">pool</span><span class="o">.</span><span class="n">process</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">          <span class="n">engine</span>  <span class="o">=</span> <span class="vi">@turn_on_engine</span> <span class="p">?</span> <span class="vi">@engine_klass</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">domain</span><span class="p">)</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">          <span class="n">reply</span> <span class="o">=</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">engine</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">flatten</span>
</span></span><span class="line"><span class="cl">          <span class="n">mutex</span><span class="o">.</span><span class="n">synchronize</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">            <span class="n">results</span> <span class="o">+=</span> <span class="p">(</span> <span class="n">reply</span> <span class="o">||</span> <span class="o">[]</span> <span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="n">pool</span><span class="o">.</span><span class="n">shutdown</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">results</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The idea is that we will have a fixed number of spawned native threads (in this case, determined by <code>@config.download_batch_size</code>). As one thread finishes it will pop a new link from the <code>collection</code> Array, essentially working as a &ldquo;queue&rdquo; to be depleted.</p>
<p>The results are accumulated in the <code>results</code> Array. Because many threads may want to modify it at once we have to synchronize access through a Mutex.</p>
<p>This way we always have a fixed amount of workers performing requests constantly instead of slicing the <code>collection</code> Array and doing bursts as in the previous version.</p>
<h3>Changing the Crystal implementation to simulate a Fibers Pool<span class="hx:absolute hx:-mt-20" id="changing-the-crystal-implementation-to-simulate-a-fibers-pool"></span>
    <a href="#changing-the-crystal-implementation-to-simulate-a-fibers-pool" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The Crystal version became a bit more complicated as I didn&rsquo;t find a pool library to pull in. I found a rough implementation of a pool in <a href="http://stackoverflow.com/a/30854065/1529907"target="_blank" rel="noopener">this stackoverflow post</a> and I was able to implement an improved version into a new shard so you can take advantage of it in your projects. Check out the source code at <a href="https://github.com/akitaonrails/fiberpool"target="_blank" rel="noopener">akitaonrails/fiberpool</a>. This is how I added it to my project:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">dependencies</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="l">...</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">fiberpool</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">github</span><span class="p">:</span><span class="w"> </span><span class="l">akitaonrails/fiberpool</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is how I used it. Notice that the logic itself is very close to the MRI Ruby version, but using a &ldquo;Fiber Pool&rdquo; instead of a Thread Pool.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;fiberpool&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">CrMangaDownloadr</span>
</span></span><span class="line"><span class="cl">  <span class="n">struct</span> <span class="no">Concurrency</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="vi">@config</span> <span class="p">:</span> <span class="no">Config</span><span class="p">,</span> <span class="vi">@turn_on_engine</span> <span class="o">=</span> <span class="kp">true</span><span class="p">);</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">collection</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">A</span><span class="p">)</span><span class="sc">?,</span> <span class="n">engine_class</span> <span class="p">:</span> <span class="n">E</span><span class="o">.</span><span class="n">class</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span> <span class="p">:</span> <span class="n">A</span><span class="p">,</span> <span class="n">E?</span> <span class="o">-&gt;</span> <span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)</span><span class="sc">?)</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span> <span class="n">of</span> <span class="n">B</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">collection</span>
</span></span><span class="line"><span class="cl">        <span class="n">pool</span> <span class="o">=</span> <span class="no">Fiberpool</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">pool</span><span class="o">.</span><span class="n">run</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">          <span class="n">engine</span> <span class="o">=</span> <span class="k">if</span> <span class="vi">@turn_on_engine</span>
</span></span><span class="line"><span class="cl">                     <span class="n">engine_class</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">domain</span><span class="p">,</span> <span class="vi">@config</span><span class="o">.</span><span class="n">cache_http</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                   <span class="k">end</span>
</span></span><span class="line"><span class="cl">          <span class="k">if</span> <span class="n">reply</span> <span class="o">=</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">            <span class="n">results</span><span class="o">.</span><span class="n">concat</span><span class="p">(</span><span class="n">reply</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">          <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="n">results</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>But again, this is very I/O intensive and both the Ruby and Crystal versions take advantage of the fact that they can do more work while waiting for one HTTP request to finish.</p>
<p><a name="reproducing-tests"> </a></p>
<h3>Reproducing the Tests<span class="hx:absolute hx:-mt-20" id="reproducing-the-tests"></span>
    <a href="#reproducing-the-tests" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>I implemented a &ldquo;Test Mode&rdquo; in all 3 implementations. You can clone from my repositories:</p>
<ul>
<li><a href="https://github.com/akitaonrails/cr_manga_downloadr"target="_blank" rel="noopener">Crystal version</a></li>
<li><a href="https://github.com/akitaonrails/manga-downloadr"target="_blank" rel="noopener">Ruby/JRuby version</a></li>
<li><a href="https://github.com/akitaonrails/ex_manga_downloadr"target="_blank" rel="noopener">Elixir version</a></li>
</ul>
<p>And you can run the test mode like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># crystal:
time ./cr_manga_downloadr --test

# MRI:
time bin/manga-downloadr --test

# JRuby (you have to edit Gemfile to uncomment the JRuby engine):
time jruby --dev -S bin/manga-downloadr --test

# Elixir:
time ./ex_manga_downloadr --test</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This will run only the fetching of chapters, pages and image links, skipping the actual downloading of the images, optimization through mogrify and PDF compilation. Those skipped parts take too long and don&rsquo;t say anything about the tested languages.</p>
<p>And if you want to test just the CPU intensive parts and avoid all networking interference altogether, you can turn on HTTP Cache mode and run the tests twice so the first run will cache everything first, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code># crystal:
time ./cr_manga_downloadr --test --cache

# MRI:
time bin/manga-downloadr --test --cache

# JRuby (you have to edit Gemfile to uncomment the JRuby engine):
time jruby --dev -S bin/manga-downloadr --test --cache

# Elixir:
time CACHE_HTTP=true ./ex_manga_downloadr --test</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So, <strong>with all requests already cached</strong> these are the results:</p>
<p>Elixir:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>7,13s user 0,24s system 331% cpu 2,227 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>MRI Ruby:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>5.590000   0.180000   5.770000 (  5.678714)
5,87s user 0,21s system 101% cpu 5,996 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>JRuby:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>10.580000   0.180000  10.760000 (  3.184472)
14,54s user 0,44s system 262% cpu 5,716 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Crystal:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>1.610000   0.050000   1.660000 (  1.344123)
1,62s user 0,06s system 124% cpu 1,350 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The Ruby/JRuby/Crystal versions have internal benchmarks to take away startup time (this is why they have 2 lines of times).</p>
<p>So, Elixir is very fast. It takes roughly 2 seconds to parse all the 1,900 HTML files to find the links.</p>
<p>Ruby is the slowest, obviously. It takes almost 6 seconds.</p>
<p>The JRuby version also takes almost 6 seconds but internally it processes in 3 seconds, the rest is startup time and warming up of the JVM underneath.</p>
<p>And Crystal is the fastest, as you would expect because it&rsquo;s a super optimized binary doing CPU bound operations, clocking in at a bit more than 1 second.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Even though the algorithms are roughly similar, Elixir is still winning by a very large margin in the total process (with the external HTTP requests).</p>
<p>There is more than just interpreter/compiler speeds, there is more than single-thread, multi-thread, fibers infrastructure.</p>
<p>We also have the maturity of the respective standard libraries (including TCP stack, HTTP client libraries, String/Array/Regex operations, etc) and 3rd party libraries (libXML, Nokogiri, etc). So there is a lot that can interfere with the tests. I&rsquo;d guess that Crystal&rsquo;s standard library, especially the networking parts, are not battle-tested enough at this point (pre 1.0!).</p>
<p>So, the summary with the new results is this:</p>
<ul>
<li>MRI Ruby 2.3.1: 27 seconds</li>
<li>JRuby 9.1.1.0: 30 seconds</li>
<li>Crystal 0.17.0: 21 seconds</li>
<li>Elixir: 15 seconds</li>
</ul>
<p>Let me know if you have ideas to make them even faster!</p>
]]></content:encoded><category>anime-and-manga</category><category>performance</category><category>distributed-systems</category></item><item><title>[Manga-Downloadr] Porting from Crystal to Ruby (and a bit of JRuby)</title><link>https://akitaonrails.github.io/en/2016/06/06/manga-downloadr-porting-from-crystal-to-ruby-and-a-bit-of-jruby/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2016/06/06/manga-downloadr-porting-from-crystal-to-ruby-and-a-bit-of-jruby/</guid><pubDate>Mon, 06 Jun 2016 18:06:00 -0300</pubDate><description>&lt;p&gt;I have this old pet project of mine called &lt;a href="http://www.akitaonrails.com/2014/12/17/small-bites-downloader-para-mangareader-kindle-edition"target="_blank" rel="noopener"&gt;Manga Downloadr&lt;/a&gt; that I published in 2014. It was a very rough version. I was experimenting with Typhoeus asynchronous requests and in the end the code ended up becoming super messed up.&lt;/p&gt;
&lt;p&gt;The nature of the original Manga Downloader is no different from a web crawler: it fetches HTML pages, parses them in order to find collections of links and keeps fetching until a set of images is downloaded. Then I organize them in volumes, optimize them to fit the Kindle screen resolution, and compile them down into PDF files. This makes this project an interesting exercise in trying to make concurrent HTTP requests and process the results.&lt;/p&gt;</description><content:encoded><![CDATA[<p>I have this old pet project of mine called <a href="http://www.akitaonrails.com/2014/12/17/small-bites-downloader-para-mangareader-kindle-edition"target="_blank" rel="noopener">Manga Downloadr</a> that I published in 2014. It was a very rough version. I was experimenting with Typhoeus asynchronous requests and in the end the code ended up becoming super messed up.</p>
<p>The nature of the original Manga Downloader is no different from a web crawler: it fetches HTML pages, parses them in order to find collections of links and keeps fetching until a set of images is downloaded. Then I organize them in volumes, optimize them to fit the Kindle screen resolution, and compile them down into PDF files. This makes this project an interesting exercise in trying to make concurrent HTTP requests and process the results.</p>
<p>A year later I was learning Elixir. The Manga Downloadr was a nice candidate for me to figure out how to implement the same thing in a different language. You can follow my learning process in <a href="http://www.akitaonrails.com/exmangadownloadr"target="_blank" rel="noopener">this series of posts</a>.</p>
<p>Finally, I&rsquo;ve been learning more about Crystal, a Ruby-inspired platform that can compile Ruby-like source code into LLVM-optimized native binaries. And as a bonus it features Go-like CSP channels and fibers to allow for concurrent code.</p>
<p>So I adapted my Elixir version to Crystal and the result is this code you can find over Github as <a href="https://github.com/akitaonrails/cr_manga_downloadr"target="_blank" rel="noopener">akitaonrails/cr_manga_downloadr</a>.</p>
<p>It works very well and performs really fast, limited mainly by how many requests MangaReader can respond to concurrently and the speed/reliability of the Internet connection. So, as my original Ruby version was terrible code, it was a good time to rewrite it. And as Crystal is surprisingly close to Ruby I decided to port it over.</p>
<blockquote>
  <p>The port was almost too trivial</p>

</blockquote>
<p>It was mostly copying and pasting the Crystal code without the Type annotations. And I had to replace the lightweight Fibers and Channel implementation for concurrency with traditional Ruby Threads.</p>
<p>The new version 2.0 for the Ruby version of the tool can be found in this Github repository: <a href="https://github.com/akitaonrails/manga-downloadr"target="_blank" rel="noopener">akitaonrails/manga-downloadr</a>.</p>
<h3>Ruby, Ruby everywhere!<span class="hx:absolute hx:-mt-20" id="ruby-ruby-everywhere"></span>
    <a href="#ruby-ruby-everywhere" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Moving between Ruby and Crystal is not so difficult. The Crystal team did a fantastic job implementing a very solid Standard Library (stdlib) that very closely resembles MRI Ruby&rsquo;s.</p>
<p>For example, let&rsquo;s compare a snippet from my Crystal version first:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">page_link</span> <span class="p">:</span> <span class="nb">String</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">get</span> <span class="n">page_link</span> <span class="k">do</span> <span class="o">|</span><span class="n">html</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">images</span> <span class="o">=</span> <span class="n">html</span><span class="o">.</span><span class="n">xpath</span><span class="p">(</span><span class="s2">&#34;//img[contains(@id, &#39;img&#39;)]&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">as</span><span class="p">(</span><span class="no">XML</span><span class="o">::</span><span class="no">NodeSet</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">image_alt</span> <span class="o">=</span> <span class="n">images</span><span class="o">[</span><span class="mi">0</span><span class="o">][</span><span class="s2">&#34;alt&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">image_src</span> <span class="o">=</span> <span class="n">images</span><span class="o">[</span><span class="mi">0</span><span class="o">][</span><span class="s2">&#34;src&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">image_alt</span> <span class="o">&amp;&amp;</span> <span class="n">image_src</span>
</span></span><span class="line"><span class="cl">      <span class="n">extension</span>      <span class="o">=</span> <span class="n">image_src</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">last</span>
</span></span><span class="line"><span class="cl">      <span class="n">list</span>           <span class="o">=</span> <span class="n">image_alt</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reverse</span>
</span></span><span class="line"><span class="cl">      <span class="n">title_name</span>     <span class="o">=</span> <span class="n">list</span><span class="o">[</span><span class="mi">4</span><span class="o">..-</span><span class="mi">1</span><span class="o">].</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">chapter_number</span> <span class="o">=</span> <span class="n">list</span><span class="o">[</span><span class="mi">3</span><span class="o">].</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s1">&#39;0&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">page_number</span>    <span class="o">=</span> <span class="n">list</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s1">&#39;0&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">image_src</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="no">CrMangaDownloadr</span><span class="o">::</span><span class="no">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">uri</span><span class="o">.</span><span class="n">host</span> <span class="n">as</span> <span class="nb">String</span><span class="p">,</span> <span class="n">uri</span><span class="o">.</span><span class="n">path</span> <span class="n">as</span> <span class="nb">String</span><span class="p">,</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">title_name</span><span class="si">}</span><span class="s2">-Chap-</span><span class="si">#{</span><span class="n">chapter_number</span><span class="si">}</span><span class="s2">-Pg-</span><span class="si">#{</span><span class="n">page_number</span><span class="si">}</span><span class="s2">.</span><span class="si">#{</span><span class="n">extension</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="no">Exception</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;Couldn&#39;t find proper metadata alt in the image tag&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Now let&rsquo;s check the ported Ruby version:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">page_link</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">get</span> <span class="n">page_link</span> <span class="k">do</span> <span class="o">|</span><span class="n">html</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">images</span> <span class="o">=</span> <span class="n">html</span><span class="o">.</span><span class="n">css</span><span class="p">(</span><span class="s1">&#39;#img&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="n">image_alt</span> <span class="o">=</span> <span class="n">images</span><span class="o">[</span><span class="mi">0</span><span class="o">][</span><span class="s2">&#34;alt&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">    <span class="n">image_src</span> <span class="o">=</span> <span class="n">images</span><span class="o">[</span><span class="mi">0</span><span class="o">][</span><span class="s2">&#34;src&#34;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="k">if</span> <span class="n">image_alt</span> <span class="o">&amp;&amp;</span> <span class="n">image_src</span>
</span></span><span class="line"><span class="cl">      <span class="n">extension</span>      <span class="o">=</span> <span class="n">image_src</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34;.&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">last</span>
</span></span><span class="line"><span class="cl">      <span class="n">list</span>           <span class="o">=</span> <span class="n">image_alt</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span><span class="o">.</span><span class="n">reverse</span>
</span></span><span class="line"><span class="cl">      <span class="n">title_name</span>     <span class="o">=</span> <span class="n">list</span><span class="o">[</span><span class="mi">4</span><span class="o">..-</span><span class="mi">1</span><span class="o">].</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">chapter_number</span> <span class="o">=</span> <span class="n">list</span><span class="o">[</span><span class="mi">3</span><span class="o">].</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s1">&#39;0&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">page_number</span>    <span class="o">=</span> <span class="n">list</span><span class="o">[</span><span class="mi">0</span><span class="o">].</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="s1">&#39;0&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">      <span class="n">uri</span> <span class="o">=</span> <span class="no">URI</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">image_src</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="no">Image</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="n">uri</span><span class="o">.</span><span class="n">host</span><span class="p">,</span> <span class="n">uri</span><span class="o">.</span><span class="n">path</span><span class="p">,</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">title_name</span><span class="si">}</span><span class="s2">-Chap-</span><span class="si">#{</span><span class="n">chapter_number</span><span class="si">}</span><span class="s2">-Pg-</span><span class="si">#{</span><span class="n">page_number</span><span class="si">}</span><span class="s2">.</span><span class="si">#{</span><span class="n">extension</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">else</span>
</span></span><span class="line"><span class="cl">      <span class="k">raise</span> <span class="no">Exception</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;Couldn&#39;t find proper metadata alt in the image tag&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>It&rsquo;s uncanny how similar they are, down to stdlib calls such as <code>URI.parse</code> or Array methods such as <code>split</code>.</p>
<blockquote>
  <p>Once you remove the Type annotations from the Crystal version, it&rsquo;s 99% Ruby.</p>

</blockquote>
<p>Ruby doesn&rsquo;t care if you&rsquo;re trying to call a method on a Nil object - at source code compiling time. Crystal does compile-time checks and if it feels like the method call will be over Nil, it won&rsquo;t even compile. So this is one big win to avoid subtle bugs.</p>
<p>In Rails we are used to the <code>#try</code> method. Ruby 2.3 introduced the <a href="http://mitrev.net/ruby/2015/11/13/the-operator-in-ruby/"target="_blank" rel="noopener">safe navigation operator</a>.</p>
<p>So, in Ruby 2.3 with Rails, both of the following lines are valid:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">obj</span><span class="o">.</span><span class="n">try</span><span class="p">(</span><span class="ss">:something</span><span class="p">)</span><span class="o">.</span><span class="n">try</span><span class="p">(</span><span class="ss">:something2</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="n">obj</span><span class="o">&amp;.</span><span class="n">something</span><span class="o">&amp;.</span><span class="n">something2</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In Crystal we can do the following:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">obj</span><span class="o">.</span><span class="n">try</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">something</span><span class="p">)</span><span class="o">.</span><span class="n">try</span><span class="p">(</span><span class="o">&amp;.</span><span class="n">something2</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So, it&rsquo;s close. Use with care.</p>
<p>As I mentioned before, Crystal is close to Ruby but it&rsquo;s not meant to be compatible, so we can&rsquo;t just load Rubygems without porting. In this example I don&rsquo;t have Nokogiri to parse the HTML. But this is where the stdlib shines: Crystal comes with good enough XML/HTML and JSON parsers. So we can parse HTML as XML and use plain XPath instead.</p>
<p>Instead of Ruby&rsquo;s <code>Net::HTTP</code> we have <code>HTTP::Client</code> (but their methods and semantics are surprisingly similar).</p>
<p>There are other differences, for example, this is the main file that requires all the others in Ruby:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="vg">$LOAD_PATH</span><span class="o">.</span><span class="n">unshift</span> <span class="no">File</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">dirname</span><span class="p">(</span><span class="bp">__FILE__</span><span class="p">),</span> <span class="s2">&#34;lib&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/records.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/downloadr_client.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/concurrency.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/chapters.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/pages.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/page_image.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/image_downloader.rb&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;manga-downloadr/workflow.rb&#34;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is the Crystal version of the same manifest:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;./cr_manga_downloadr/*&#34;</span>
</span></span><span class="line"><span class="cl"><span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>On the other hand, we need to be a bit more explicit in each Crystal source code file, and declare the specific dependencies where needed. For example, in the <code>pages.cr</code> file it starts like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;./downloadr_client&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">require</span> <span class="s2">&#34;xml&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">CrMangaDownloadr</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">Pages</span> <span class="o">&lt;</span> <span class="no">DownloadrClient</span><span class="p">(</span><span class="nb">Array</span><span class="p">(</span><span class="nb">String</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Crystal has less room for &ldquo;magic&rdquo; but it&rsquo;s able to maintain a high level of abstraction anyway.</p>
<h3>A Word about Types<span class="hx:absolute hx:-mt-20" id="a-word-about-types"></span>
    <a href="#a-word-about-types" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><blockquote>
  <p>We can spend the next decade masturbating over all there is about Types, and it will be extremely boring.</p>

</blockquote>
<p>The only thing you must understand: the compiler must know the method signature of classes before you can use them. There is no Runtime component that can introspect objects on the fly, like in Ruby and other dynamic languages (even Objective-C/Swift can do more dynamic stuff than Crystal).</p>
<p>Most of the time the Crystal compiler will be smart enough to infer the types for you, so you don&rsquo;t need to be absolutely explicit. You should follow the compiler&rsquo;s lead to know when to use Type Annotations.</p>
<p>What may really scare you at first is the need for Type Annotations, to understand Generics and so forth. The compiler will print out every scary error dump that you will need to get used to. Most scary errors will usually be a Type Annotation missing or you trying to call a method on a possibly Nil object.</p>
<p>For example, if I change the following line in the <code>page_image_spec.cr</code> test file:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="c1"># line 8:</span>
</span></span><span class="line"><span class="cl"><span class="n">image</span> <span class="o">=</span> <span class="no">CrMangaDownloadr</span><span class="o">::</span><span class="no">PageImage</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="s2">&#34;www.mangareader.net&#34;</span><span class="p">)</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="s2">&#34;/naruto/662/2&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1"># line 10:</span>
</span></span><span class="line"><span class="cl"><span class="c1"># image.try(&amp;.host).should eq(&#34;i8.mangareader.net&#34;)</span>
</span></span><span class="line"><span class="cl"><span class="n">image</span><span class="o">.</span><span class="n">host</span><span class="o">.</span><span class="n">should</span> <span class="n">eq</span><span class="p">(</span><span class="s2">&#34;i8.mangareader.net&#34;</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The commented out line recognizes that the <code>image</code> instance might come as Nil, so we add an explicit <code>#try</code> call in the spec.</p>
<p>If we try to compile without this recognition, this is the error the compiler will dump on you:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>$ crystal spec                                                        [
Error in ./spec/cr_manga_downloadr/page_image_spec.cr:10: undefined method &#39;host&#39; for Nil (compile-time type is CrMangaDownloadr::Image?)

    image.host.should eq(&#34;i8.mangareader.net&#34;)
          ^~~~

=============================================================================

Nil trace:

  ./spec/cr_manga_downloadr/page_image_spec.cr:8

        image = CrMangaDownloadr::PageImage.new(&#34;www.mangareader.net&#34;).fetch(&#34;/naruto/662/2&#34;)
...</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>There is a large stack trace dump after that snippet above, but you only need to pay attention to the first few lines that already say what&rsquo;s wrong: &ldquo;undefined method &lsquo;host&rsquo; for Nil (compile-time type is CrMangaDownloadr::Image?)&rdquo;. If you know how to read, you shouldn&rsquo;t have any problems most of the time.</p>
<p>Now, the <code>Chapters</code>, <code>Pages</code>, <code>PageImage</code> (all subclasses of <code>DownloadrClient</code>) are basically the same thing: they do <code>HTTP::Client</code> requests.</p>
<p>This is how the <code>Pages</code> class is implemented:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">CrMangaDownloadr</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">Pages</span> <span class="o">&lt;</span> <span class="no">DownloadrClient</span><span class="p">(</span><span class="nb">Array</span><span class="p">(</span><span class="nb">String</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">chapter_link</span> <span class="p">:</span> <span class="nb">String</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">get</span> <span class="n">chapter_link</span> <span class="k">do</span> <span class="o">|</span><span class="n">html</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="n">nodes</span> <span class="o">=</span> <span class="n">html</span><span class="o">.</span><span class="n">xpath_nodes</span><span class="p">(</span><span class="s2">&#34;//div[@id=&#39;selectpage&#39;]//select[@id=&#39;pageMenu&#39;]//option&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">nodes</span><span class="o">.</span><span class="n">map</span> <span class="p">{</span> <span class="o">|</span><span class="n">node</span><span class="o">|</span> <span class="o">[</span><span class="n">chapter_link</span><span class="p">,</span> <span class="n">node</span><span class="o">.</span><span class="n">text</span> <span class="n">as</span> <span class="nb">String</span><span class="o">].</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34;/&#34;</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><code>#get</code> is a method from the <code>DownloadrClient</code> superclass that receives a String <code>chapter_link</code> and a block. The block receives a parsed <code>html</code> collection of nodes and we can play with it, returning an Array of Strings.</p>
<p>That&rsquo;s why we have the <code>(Array(String))</code> when inheriting from <code>DownloadrClient</code>. Let&rsquo;s see how the <code>DownloadrClient</code> superclass is implemented.</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">module</span> <span class="nn">CrMangaDownloadr</span>
</span></span><span class="line"><span class="cl">  <span class="k">class</span> <span class="nc">DownloadrClient</span><span class="p">(</span><span class="n">T</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="k">def</span> <span class="nf">get</span><span class="p">(</span><span class="n">uri</span> <span class="p">:</span> <span class="nb">String</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span> <span class="p">:</span> <span class="no">XML</span><span class="o">::</span><span class="no">Node</span> <span class="o">-&gt;</span> <span class="n">T</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">response</span> <span class="o">=</span> <span class="vi">@http_client</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">uri</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">case</span> <span class="n">response</span><span class="o">.</span><span class="n">status_code</span>
</span></span><span class="line"><span class="cl">      <span class="k">when</span> <span class="mi">301</span>
</span></span><span class="line"><span class="cl">        <span class="n">get</span> <span class="n">response</span><span class="o">.</span><span class="n">headers</span><span class="o">[</span><span class="s2">&#34;Location&#34;</span><span class="o">]</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span>
</span></span><span class="line"><span class="cl">      <span class="k">when</span> <span class="mi">200</span>
</span></span><span class="line"><span class="cl">        <span class="n">parsed</span> <span class="o">=</span> <span class="no">XML</span><span class="o">.</span><span class="n">parse_html</span><span class="p">(</span><span class="n">response</span><span class="o">.</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">block</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">parsed</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="o">...</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can see that this class receives a Generic Type and it uses it as the return type for the yielded block in the <code>#get</code> method. The <code>XML::Node -&gt; T</code> is the declaration of the signature for the block, sending <code>XML::Node</code> and receiving whatever the type <code>T</code> is. At compile time, imagine this <code>T</code> being replaced by <code>Array(String)</code>. That&rsquo;s how you can create classes that can deal with any number of different Types without having to overload for polymorphism.</p>
<p>If you come from Java, C#, Go or any other modern statically typed language, you probably already know what a Generic is.</p>
<p>You can go very far with Generics, check out how our <code>Concurrency.cr</code> begins:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Concurrency</span><span class="p">(</span><span class="n">A</span><span class="p">,</span> <span class="n">B</span><span class="p">,</span> <span class="n">C</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">collection</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">A</span><span class="p">)</span><span class="sc">?,</span> <span class="o">&amp;</span><span class="n">block</span> <span class="p">:</span> <span class="n">A</span><span class="p">,</span> <span class="n">C?</span> <span class="o">-&gt;</span> <span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)</span><span class="sc">?)</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)?</span>
</span></span><span class="line"><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span> <span class="n">of</span> <span class="n">B</span>
</span></span><span class="line"><span class="cl">    <span class="n">collection</span><span class="o">.</span><span class="n">try</span> <span class="o">&amp;.</span><span class="n">each_slice</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">batch</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">engine</span>  <span class="o">=</span> <span class="k">if</span> <span class="vi">@turn_on_engine</span>
</span></span><span class="line"><span class="cl">                  <span class="n">C</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">domain</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="n">channel</span> <span class="o">=</span> <span class="no">Channel</span><span class="p">(</span><span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)</span><span class="sc">?)</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">      <span class="n">batch</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is how we use it in the <code>workflow.cr</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="kp">private</span> <span class="k">def</span> <span class="nf">fetch_pages</span><span class="p">(</span><span class="n">chapters</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="nb">String</span><span class="p">)</span><span class="sc">?)</span>
</span></span><span class="line"><span class="cl">  <span class="nb">puts</span> <span class="s2">&#34;Fetching pages from all chapters ...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">reactor</span> <span class="o">=</span> <span class="no">Concurrency</span><span class="p">(</span><span class="nb">String</span><span class="p">,</span> <span class="nb">String</span><span class="p">,</span> <span class="no">Pages</span><span class="p">)</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">reactor</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="n">chapters</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">link</span><span class="p">,</span> <span class="n">engine</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">engine</span><span class="o">.</span><span class="n">try</span><span class="p">(</span> <span class="o">&amp;.</span><span class="n">fetch</span><span class="p">(</span><span class="n">link</span><span class="p">)</span> <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>In this example, imagine <code>A</code> being replaced by <code>String</code>, <code>B</code> also being replaced by <code>String</code> and C being replaced by <code>Pages</code> in the <code>Concurrency</code> class.</p>
<p>This is the &ldquo;first-version-that-worked&rdquo; so it&rsquo;s probably not very idiomatic. Either this could be solved with less Generics exercising or maybe I could simplify it with the use of Macros. But it&rsquo;s working quite ok as it is.</p>
<p>The pure Ruby version ends up just like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">class</span> <span class="nc">Concurrency</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="n">engine_klass</span> <span class="o">=</span> <span class="kp">nil</span><span class="p">,</span> <span class="n">config</span> <span class="o">=</span> <span class="no">Config</span><span class="o">.</span><span class="n">new</span><span class="p">,</span> <span class="n">turn_on_engine</span> <span class="o">=</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">    <span class="n">collection</span><span class="o">&amp;.</span><span class="n">each_slice</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">batch</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">mutex</span>   <span class="o">=</span> <span class="no">Mutex</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">      <span class="n">threads</span> <span class="o">=</span> <span class="n">batch</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="o">...</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This version is much &ldquo;simpler&rdquo; in terms of source code density. But on the other hand we would have to test this Ruby version a lot more because it has many different permutations (we even inject classes through <code>engine_klass</code>) that we must make sure respond correctly. In practice we should add tests for all combinations of the initializer&rsquo;s arguments.</p>
<p>In the Crystal version, because all types have been checked at compile-time, it was more demanding in terms of annotations; but we can be pretty sure that if it compiles, it will run as expected.</p>
<blockquote>
  <p>I&rsquo;m not saying that Crystal doesn&rsquo;t need any specs.</p>

</blockquote>
<p>Compilers can only go so far. But how much is &ldquo;so far&rdquo;? Whenever you&rsquo;re &ldquo;forced&rdquo; to add Type Annotations, I will state that those parts are either trying to be too smart or they are intrinsically complex. Those are parts that would require extra levels of tests in Ruby and, if we can add the annotations properly, we can have fewer tests (we don&rsquo;t need to cover most permutations) in the Crystal version (exponential complexity of permutations could go down to a linear complexity, I think).</p>
<h3>A Word about Ruby Threads<span class="hx:absolute hx:-mt-20" id="a-word-about-ruby-threads"></span>
    <a href="#a-word-about-ruby-threads" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The main concepts you must understand about concurrency in Ruby are these:</p>
<ul>
<li>MRI Ruby has a GIL, a Global Interpreter Lock, that forbids code from running concurrently.</li>
<li>MRI Ruby does have access and exposes native Threads since version 1.9. But even if you fire up multiple Threads, they will run sequentially because only one thread can hold the Lock at a time.</li>
<li>I/O operations are the exception: they do release the Lock for other threads to run while the operation is waiting to complete. The OS will signal the program through OS-level poll.</li>
</ul>
<p>This means that if your app is <strong>I/O intensive</strong> (HTTP requests, File reads or writes, socket operations, etc), you will have <em>some</em> concurrency. A web server, such as Puma for example, can take some advantage of Threads because a big part of the operations involves receiving HTTP requests and sending HTTP responses over the wire, which would make the Ruby process idle while waiting.</p>
<p>If your app is CPU intensive (heavy algorithms, data crunching, stuff that really makes the CPU hot) then you can&rsquo;t take advantage of native Threads, only one will run at a time. If you have multiple cores in your CPU, you can Fork your process to the number of cores available.</p>
<p>You should check out <a href="https://github.com/grosser/parallel"target="_blank" rel="noopener">grosser/parallel</a> to make it easy.</p>
<p>This is why Puma also has a &ldquo;worker&rdquo; mode. &ldquo;Worker&rdquo; is the name we usually give to forked children of processes.</p>
<p>In the case of this downloader process, it will perform thousands of HTTP requests to scrape the needed metadata from the MangaReader pages. So it&rsquo;s definitely much more I/O intensive than CPU intensive (the CPU intensive parts are the HTML parsing and later the image resizing and PDF compiling).</p>
<p>A sequential version of what has to be done, in Ruby, looks like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch_sequential</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">  <span class="n">engine</span>  <span class="o">=</span> <span class="vi">@turn_on_engine</span> <span class="p">?</span> <span class="vi">@engine_klass</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">domain</span><span class="p">)</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">  <span class="n">collection</span><span class="o">&amp;.</span><span class="n">each_slice</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">batch</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">batch</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">batch_results</span> <span class="o">=</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">engine</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">flatten</span>
</span></span><span class="line"><span class="cl">      <span class="n">results</span> <span class="o">+=</span> <span class="p">(</span> <span class="n">batch_results</span> <span class="o">||</span> <span class="o">[]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">results</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If we have 10,000 links in the <code>collection</code>, we first slice it to what <code>@config.download_batch_size</code> defines and we iterate over those smaller slices, calling some block and accumulating the results. This is a naive algorithm, as you will find out in the next section, but bear with me.</p>
<p>In Elixir you can fire up micro-processes to make the HTTP requests in parallel. In Crystal you can fire up Fibers and wait for the HTTP requests to complete and signal the results through Channels.</p>
<p>Both are lightweight ways and you can have hundreds or even thousands running in parallel. Manga Reader will probably complain if you try that many at once, so the limit is not in the code, but in the external service.</p>
<p>To transform the sequential version into a concurrent one, this is what we can do in Crystal:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">collection</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">A</span><span class="p">)</span><span class="sc">?,</span> <span class="o">&amp;</span><span class="n">block</span> <span class="p">:</span> <span class="n">A</span><span class="p">,</span> <span class="n">C?</span> <span class="o">-&gt;</span> <span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)</span><span class="sc">?)</span> <span class="p">:</span> <span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)?</span>
</span></span><span class="line"><span class="cl">  <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span> <span class="n">of</span> <span class="n">B</span>
</span></span><span class="line"><span class="cl">  <span class="n">collection</span><span class="o">.</span><span class="n">try</span> <span class="o">&amp;.</span><span class="n">each_slice</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">batch</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">channel</span> <span class="o">=</span> <span class="no">Channel</span><span class="p">(</span><span class="nb">Array</span><span class="p">(</span><span class="n">B</span><span class="p">)</span><span class="sc">?)</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="n">batch</span><span class="o">.</span><span class="n">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="n">spawn</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">engine</span>  <span class="o">=</span> <span class="k">if</span> <span class="vi">@turn_on_engine</span>
</span></span><span class="line"><span class="cl">                    <span class="n">C</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">domain</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">                  <span class="k">end</span>
</span></span><span class="line"><span class="cl">        <span class="n">reply</span> <span class="o">=</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">engine</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">channel</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="n">reply</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="n">engine</span><span class="o">.</span><span class="n">try</span> <span class="o">&amp;.</span><span class="n">close</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">batch</span><span class="o">.</span><span class="n">size</span><span class="o">.</span><span class="n">times</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">reply</span> <span class="o">=</span> <span class="n">channel</span><span class="o">.</span><span class="n">receive</span>
</span></span><span class="line"><span class="cl">      <span class="k">if</span> <span class="n">reply</span>
</span></span><span class="line"><span class="cl">        <span class="n">results</span><span class="o">.</span><span class="n">concat</span><span class="p">(</span><span class="n">reply</span><span class="o">.</span><span class="n">flatten</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">channel</span><span class="o">.</span><span class="n">close</span>
</span></span><span class="line"><span class="cl">    <span class="nb">puts</span> <span class="s2">&#34;Processed so far: </span><span class="si">#{</span><span class="n">results</span><span class="o">.</span><span class="n">try</span> <span class="o">&amp;.</span><span class="n">size</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">results</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Fetching a huge collection and slicing it in smaller &lsquo;batches&rsquo; is easy. Now we have a smaller <code>batch</code> collection. For each item (usually a URI) we <code>spawn</code> a Fiber and call a block that will request and process the results. Once it&rsquo;s finished processing it sends the results over a <code>channel</code>.</p>
<p>Once we finish iterating over the batch and spawning that many Fibers we can &ldquo;wait&rdquo; for them by doing <code>channel.receive</code>, which will start receiving results as soon as the Fibers finish requesting/processing each URI.</p>
<p>We accumulate the results and go over the next batch of the collection until finished. The amount of concurrency is determined by the size of the batch (it&rsquo;s like <a href="http://www.akitaonrails.com/en/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate/"target="_blank" rel="noopener">what I did with &lsquo;poolboy&rsquo; over Elixir</a> where we start a fixed number of processes to run in parallel and avoid doing a Denial of Service to Manga Reader).</p>
<p>By the way, this Crystal implementation is similar to what you would do if you were in Go, using Channels.</p>
<p>In the Ruby version you can fire up native Threads - which has a lot of overhead to spawn! - and assume the HTTP requests will run almost all in parallel. Because it&rsquo;s I/O intensive, you can have them all in parallel. This is what it looks like:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">fetch</span><span class="p">(</span><span class="n">collection</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">block</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">results</span> <span class="o">=</span> <span class="o">[]</span>
</span></span><span class="line"><span class="cl">  <span class="n">collection</span><span class="o">&amp;.</span><span class="n">each_slice</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">download_batch_size</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">batch</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">    <span class="n">mutex</span>   <span class="o">=</span> <span class="no">Mutex</span><span class="o">.</span><span class="n">new</span>
</span></span><span class="line"><span class="cl">    <span class="n">threads</span> <span class="o">=</span> <span class="n">batch</span><span class="o">.</span><span class="n">map</span> <span class="k">do</span> <span class="o">|</span><span class="n">item</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="no">Thread</span><span class="o">.</span><span class="n">new</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">        <span class="n">engine</span>  <span class="o">=</span> <span class="vi">@turn_on_engine</span> <span class="p">?</span> <span class="vi">@engine_klass</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="vi">@config</span><span class="o">.</span><span class="n">domain</span><span class="p">)</span> <span class="p">:</span> <span class="kp">nil</span>
</span></span><span class="line"><span class="cl">        <span class="no">Thread</span><span class="o">.</span><span class="n">current</span><span class="o">[</span><span class="s2">&#34;results&#34;</span><span class="o">]</span> <span class="o">=</span> <span class="n">block</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">item</span><span class="p">,</span> <span class="n">engine</span><span class="p">)</span><span class="o">&amp;.</span><span class="n">flatten</span>
</span></span><span class="line"><span class="cl">        <span class="n">mutex</span><span class="o">.</span><span class="n">synchronize</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">          <span class="n">results</span> <span class="o">+=</span> <span class="p">(</span> <span class="no">Thread</span><span class="o">.</span><span class="n">current</span><span class="o">[</span><span class="s2">&#34;results&#34;</span><span class="o">]</span> <span class="o">||</span> <span class="o">[]</span> <span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="k">end</span>
</span></span><span class="line"><span class="cl">      <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="n">threads</span><span class="o">.</span><span class="n">each</span><span class="p">(</span><span class="o">&amp;</span><span class="ss">:join</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">puts</span> <span class="s2">&#34;Processed so far: </span><span class="si">#{</span><span class="n">results</span><span class="o">&amp;.</span><span class="n">size</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">results</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Threads are all initialized in a &ldquo;paused&rdquo; state. Once we instantiate those many Threads, we can <code>#join</code> on each of them and wait for them all to finish.</p>
<p>Once each Thread finishes the same URI request/process, the results must be accumulated in a global storage, in this case a simple array called <code>results</code>. But because we might have the chance of 2 or more threads trying to update the same array, we might as well synchronize the access (I&rsquo;m not sure if Ruby&rsquo;s Array access is already synchronized, but I guess not). To synchronize access we use a Mutex, which is a fine-grained lock, to make sure only 1 thread can modify the global array at once.</p>
<p>To prove that Ruby can support concurrent I/O operations, I have added 2 methods to the <code>Concurrent</code> class, the first is just <code>#fetch</code> and it&rsquo;s the Thread implementation above. The second is called <code>#fetch_sequential</code> and it is the sequential version also shown at the beginning of this section. And I added the following spec:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">it</span> <span class="s2">&#34;should check that the fetch implementation runs in less time than the sequential version&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">reactor</span> <span class="o">=</span> <span class="no">MangaDownloadr</span><span class="o">::</span><span class="no">Concurrency</span><span class="o">.</span><span class="n">new</span><span class="p">(</span><span class="no">MangaDownloadr</span><span class="o">::</span><span class="no">Pages</span><span class="p">,</span> <span class="n">config</span><span class="p">,</span> <span class="kp">true</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">collection</span> <span class="o">=</span> <span class="o">[</span><span class="s2">&#34;/onepunch-man/96&#34;</span><span class="o">]</span> <span class="o">*</span> <span class="mi">10</span>
</span></span><span class="line"><span class="cl">  <span class="no">WebMock</span><span class="o">.</span><span class="n">allow_net_connect!</span>
</span></span><span class="line"><span class="cl">  <span class="k">begin</span>
</span></span><span class="line"><span class="cl">    <span class="n">concurrent_measurement</span> <span class="o">=</span> <span class="no">Benchmark</span><span class="o">.</span><span class="n">measure</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">results</span> <span class="o">=</span> <span class="n">reactor</span><span class="o">.</span><span class="n">fetch</span><span class="p">(</span><span class="n">collection</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">link</span><span class="p">,</span> <span class="n">engine</span><span class="o">|</span> <span class="n">engine</span><span class="o">&amp;.</span><span class="n">fetch</span><span class="p">(</span><span class="n">link</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="n">sequential_measurement</span> <span class="o">=</span> <span class="no">Benchmark</span><span class="o">.</span><span class="n">measure</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">      <span class="n">results</span> <span class="o">=</span> <span class="n">reactor</span><span class="o">.</span><span class="n">send</span><span class="p">(</span><span class="ss">:fetch_sequential</span><span class="p">,</span> <span class="n">collection</span><span class="p">)</span> <span class="p">{</span> <span class="o">|</span><span class="n">link</span><span class="p">,</span> <span class="n">engine</span><span class="o">|</span> <span class="n">engine</span><span class="o">&amp;.</span><span class="n">fetch</span><span class="p">(</span><span class="n">link</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="sr">/\((.*?)\)$/</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="n">concurrent_measurement</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">cm</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">      <span class="sr">/\((.*?)\)/</span><span class="o">.</span><span class="n">match</span><span class="p">(</span><span class="n">sequential_measurement</span><span class="o">.</span><span class="n">to_s</span><span class="p">)</span> <span class="k">do</span> <span class="o">|</span><span class="n">sm</span><span class="o">|</span>
</span></span><span class="line"><span class="cl">        <span class="c1"># expected for the concurrent version to be close to 10 times faster than sequential</span>
</span></span><span class="line"><span class="cl">        <span class="n">expect</span><span class="p">(</span><span class="n">sm</span><span class="o">[</span><span class="mi">1</span><span class="o">].</span><span class="n">to_f</span><span class="p">)</span><span class="o">.</span><span class="n">to</span> <span class="n">be</span> <span class="o">&gt;</span> <span class="p">(</span> <span class="n">cm</span><span class="o">[</span><span class="mi">1</span><span class="o">].</span><span class="n">to_f</span> <span class="o">*</span> <span class="mi">9</span> <span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">ensure</span>
</span></span><span class="line"><span class="cl">    <span class="no">WebMock</span><span class="o">.</span><span class="n">disable_net_connect!</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Because it uses WebMock, I first disable it during this spec. I create a fake collection of 10 real links to MangaReader. And then I benchmark the Thread-based concurrent version and the plain sequential version. Because we have 10 links and they are all the same you can assume that the sequential version will be almost <strong>10 times slower</strong> than the Thread-based version. And this is exactly what this spec compares and proves (the spec fails if the concurrent version is not at least 9x faster).</p>
<p>To compare all versions of the Manga Downloadrs I let it download and compile an entire manga collection, in this case &ldquo;One-Punch Man&rdquo;, which has almost <strong>1,900 pages/images</strong>. I am just measuring the fetching and scraping processes and skipping the actual image downloading, image resizing and PDF generation as they take the majority of the time and the resizing and PDF part are all done by ImageMagick&rsquo;s mogrify and convert tools.</p>
<p>This is how long this new Ruby version takes to fetch and scrape almost 1,900 pages (using MRI Ruby 2.3.1):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>12,42s user 1,33s system 23% cpu 57,675 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is how long the Crystal version takes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>4,03s user 0,40s system 7% cpu 59,207 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Just for fun I tried to run the Ruby version under JRuby 9.1.1.0. To run with JRuby just add the following line in the <code>Gemfile</code>:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">ruby</span> <span class="s2">&#34;2.3.0&#34;</span><span class="p">,</span> <span class="ss">:engine</span> <span class="o">=&gt;</span> <span class="s1">&#39;jruby&#39;</span><span class="p">,</span> <span class="ss">:engine_version</span> <span class="o">=&gt;</span> <span class="s1">&#39;9.1.1.0&#39;</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Bundle install, run normally, and this is the result:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>47,80s user 1,99s system 108% cpu 45,967 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is how long the Elixir version takes:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>11,38s user 1,04s system 85% cpu 14,590 total</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Reality Check!<span class="hx:absolute hx:-mt-20" id="reality-check"></span>
    <a href="#reality-check" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>If you just look at the times above you might come to the wrong conclusions.</p>
<p>First of all, it&rsquo;s an <strong>unfair comparison</strong>. The Elixir version uses a very different algorithm than the Ruby and Crystal versions.</p>
<p>In Elixir I boot up a pool of processes, around 50 of them. Then I start 50 HTTP requests at once. Once each process finishes, it releases itself back to the pool and I can fire up another HTTP request from the queue of links. So it&rsquo;s a <strong>constant stream</strong> of at most 50 HTTP requests, constantly.</p>
<p>The Crystal and Ruby/JRuby versions slice the 1,900 links into batches of 40 links and then I fire up 40 requests at once. This implementation waits for all 40 to finish and they fire up another 40. So it&rsquo;s never a constant stream, it&rsquo;s bursts of 40 requests. So each batch is slowed down by the slowest request in the batch, not giving a chance for other requests to go through.</p>
<p>It&rsquo;s a difference in architecture. Elixir makes it much easier to do streams and Crystal, with its CSP style, makes it easier to do bursts. A better approach would be to queue up the 1,900 links and use something like Sidekiq.cr to go over one link at a time (spawning 40 fibers to serve as a &ldquo;pool&rdquo;, for example).</p>
<p>The Elixir version has a more efficient architecture, which is why it takes no more than 15 seconds to fetch all the image links while the Crystal version takes almost a full minute to finish (the accumulation of the slowest requests in each batch).</p>
<p>Now, you will be surprised that the Crystal version is actually a bit slower than the Ruby version! And you won&rsquo;t be too surprised to see JRuby being faster at 45 seconds!</p>
<p>This is more evidence that you should not dismiss Ruby (and that you should try JRuby more often). As I explained before, it does support concurrency in I/O operations and the applications I tested are all I/O heavy.</p>
<p>The difference is probably in the maturity of Ruby&rsquo;s <code>Net::HTTP</code> library against Crystal&rsquo;s <code>HTTP::Client</code>. I tried many tweaks in the Crystal version but I couldn&rsquo;t get much faster. I tried to do larger batches, but for some reason the application just hangs, pauses, and never releases. I have to Ctrl-C out of it and retry until it finally goes through. If someone knows what I am doing wrong, please don&rsquo;t forget to write in the comments section below.</p>
<p>Some of this is probably due to MangaReader&rsquo;s unreliable servers, they probably have some kind of DoS protection, throttling connections or something like this.</p>
<p>Anyway, when they go through, because the Ruby and Crystal algorithms are essentially the same, they take roughly the same time to complete. So what&rsquo;s missing is for me to evolve this algorithm to either use something like Sidekiq or implement an internal queue/pool of workers scheme.</p>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>The goal of this experiment was to learn more of Crystal&rsquo;s ability and how easy it would be to go back and forth with Ruby.</p>
<p>As you can see, there are many differences, but it&rsquo;s not so difficult. I may be missing something, but I stumbled upon some difficulties when pushing <code>HTTP::Client</code> + Fibers too hard, as I explained above. If you know what I am doing wrong, please let me know.</p>
<p>The difference between the Elixir vs Ruby/Crystal algorithms shows not just language performance differences, but also the importance of architecture and algorithms in the overall performance. This test was not conclusive, just a hint that my naive Fibers implementation needs some more work, and that Elixir&rsquo;s natural handling of parallel processes makes it easier to achieve higher levels of parallelism.</p>
<p>This serves as a taste of what Crystal can do, and also that Ruby is still in the game. But also that Elixir surely is something to look at closely.</p>
]]></content:encoded><category>anime-and-manga</category><category>programming-languages</category><category>ruby</category></item><item><title>Ex Manga Downloadr - Part 3: Mangafox Support!</title><link>https://akitaonrails.github.io/en/2015/12/02/ex-manga-downloadr-part-3-mangafox-support/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/12/02/ex-manga-downloadr-part-3-mangafox-support/</guid><pubDate>Wed, 02 Dec 2015 16:27:00 -0200</pubDate><description>&lt;p&gt;I thought &lt;a href="http://www.akitaonrails.com/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener"&gt;Part 2&lt;/a&gt; would be my last article about this tool, but turns out it&amp;rsquo;s just too much fun to let it go easily. As usual, all the source code is on my &lt;a href="https://github.com/akitaonrails/ex_manga_downloadr"target="_blank" rel="noopener"&gt;Github repository&lt;/a&gt;. And the gist of the post is that now you can do this:&lt;/p&gt;
&lt;div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code"&gt;
&lt;div&gt;&lt;pre&gt;&lt;code&gt;git pull
mix escript.build
./ex_manga_downloadr -n onepunch -u http://mangafox.me/manga/onepunch_man/ -d /tmp/onepunch -s mangafox&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0"&gt;
&lt;button
class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
title="Copy code"
&gt;
&lt;div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"&gt;&lt;/div&gt;
&lt;/button&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;And there you go: download from Mangafox built-in! \o/&lt;/p&gt;</description><content:encoded><![CDATA[<p>I thought <a href="http://www.akitaonrails.com/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener">Part 2</a> would be my last article about this tool, but turns out it&rsquo;s just too much fun to let it go easily. As usual, all the source code is on my <a href="https://github.com/akitaonrails/ex_manga_downloadr"target="_blank" rel="noopener">Github repository</a>. And the gist of the post is that now you can do this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>git pull
mix escript.build
./ex_manga_downloadr -n onepunch -u http://mangafox.me/manga/onepunch_man/ -d /tmp/onepunch -s mangafox</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And there you go: download from Mangafox built-in! \o/</p>
<p>It starts when I wanted to download manga that is not available at MangaReader but exists under Mangafox.</p>
<p>So, the initial endeavor was to copy the MangaReader parser modules (IndexPage, ChapterPage, and Page) and paste them at a specific &ldquo;lib/ex_manga_downloadr/mangafox&rdquo; folder. Same thing done to the unit tests folder. Just a matter of copying and pasting the files and change the &ldquo;MangaReader&rdquo; module name to &ldquo;Mangafox&rdquo;.</p>
<p>Of course the URL formats are different, the Floki CSS selectors are a bit different, so that&rsquo;s what had to change in the parser. For example, this is how I parse the chapter links from the main page at MangaReader:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#listing a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And this is the same thing but for Mangafox:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">html</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="s2">&#34;.chlist a[class=&#39;tips&#39;]&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">},</span> <span class="p">{</span><span class="s2">&#34;title&#34;</span><span class="p">,</span> <span class="n">_</span><span class="p">},</span> <span class="p">{</span><span class="s2">&#34;class&#34;</span><span class="p">,</span> <span class="s2">&#34;tips&#34;</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Exactly the same logic but the pattern matching structure is different because the returning HTML DOM nodes are different.</p>
<p>Another difference is that MangaReader returns everything in plain text by default, but Mangafox returns everything Gzipped regardless of whether I send the &ldquo;Accept-Encoding&rdquo; HTTP header (curiously, if I retry several times it changes behavior and sometimes sends plain text).</p>
<p>What I did differently was to check if the returned <tt>%HTTPotion.Response{}</tt> structure had a &ldquo;Content-Encoding&rdquo; header set to &ldquo;gzip&rdquo; and if so, gunzip it using the built-in Erlang &ldquo;zlib&rdquo; package (nothing to import!):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">gunzip</span><span class="p">(</span><span class="n">body</span><span class="p">,</span> <span class="n">headers</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="n">headers</span><span class="o">[</span><span class="ss">:&#34;Content-Encoding&#34;</span><span class="o">]</span> <span class="o">==</span> <span class="s2">&#34;gzip&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="ss">:zlib</span><span class="o">.</span><span class="n">gunzip</span><span class="p">(</span><span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="n">body</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I would&rsquo;ve preferred if HTTPotion did that out of the box for me (#OpportunityToContribute!), but this was easy enough.</p>
<p>Once the unit tests were passing correctly after tuning the scrapper (HTTPotion requests) and parser (Floki selectors) it was time to make my Worker aware of the existence of this new set of modules.</p>
<p>The Workflow module just calls the Worker, which in turn does the heavy lifting of fetching pages and downloading images. The Worker called the MangaReader module directly, like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">PoolManagement</span><span class="o">.</span><span class="n">Worker</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">GenServer</span>
</span></span><span class="line"><span class="cl">  <span class="n">use</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span>
</span></span><span class="line"><span class="cl">  <span class="nb">require</span> <span class="no">Logger</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">chapter_page</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Task</span><span class="o">.</span><span class="n">async</span> <span class="n">fn</span> <span class="o">-&gt;</span> 
</span></span><span class="line"><span class="cl">      <span class="ss">:poolboy</span><span class="o">.</span><span class="n">transaction</span> <span class="ss">:worker_pool</span><span class="p">,</span> <span class="n">fn</span><span class="p">(</span><span class="n">server</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="no">GenServer</span><span class="o">.</span><span class="n">call</span><span class="p">(</span><span class="n">server</span><span class="p">,</span> <span class="p">{</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">},</span> <span class="vi">@timeout_ms</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="k">end</span><span class="p">,</span> <span class="vi">@transaction_timeout_ms</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">handle_call</span><span class="p">({</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="no">ChapterPages</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">),</span> <span class="n">state</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="o">...</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That &ldquo;<tt>use ExMangaDownloadr.MangaReader</tt>&rdquo; statement up above is just a macro that will alias the corresponding modules:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmacro</span> <span class="n">__using__</span><span class="p">(</span><span class="n">_opts</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">quote</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">IndexPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">ChapterPage</span>
</span></span><span class="line"><span class="cl">      <span class="k">alias</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">Page</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>So when I call &ldquo;<tt>ChapterPages.pages(chapter_link)</tt>&rdquo; it&rsquo;s a shortcut to use the fully qualified module name like this: &ldquo;<tt>ExMangaDownloadr.MangaReader.ChapterPages.pages(chapter_link)</tt>&rdquo;.</p>
<p>An Elixir module namespace is just an Atom. Nested module names have the full, dot-separated name, prefixed with its parent. For example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">Foo</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">defmodule</span> <span class="no">Bar</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">defmodule</span> <span class="no">Xyz</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">       <span class="k">def</span> <span class="nf">teste</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">       <span class="k">end</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>You can just call &ldquo;<tt>Foo.Bar.Xyz.teste()</tt>&rdquo; and that&rsquo;s it. But there is a small trick. Elixir also transparently prefixes the full module name with &ldquo;Elixir&rdquo;. So in reality, the full module name is &ldquo;Elixir.Foo.Bar.Xyz&rdquo;, in order to make sure no Elixir module ever conflicts with an existing Erlang module.</p>
<p>This is important because of this new function I added to the Worker module first:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">manga_source</span><span class="p">(</span><span class="n">source</span><span class="p">,</span> <span class="n">module</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">source</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangareader&#34;</span> <span class="o">-&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="s2">&#34;Elixir.ExMangaDownloadr.MangaReader.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="s2">&#34;mangafox&#34;</span>    <span class="o">-&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">to_atom</span><span class="p">(</span><span class="s2">&#34;Elixir.ExMangaDownloadr.Mangafox.</span><span class="si">#{</span><span class="n">module</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is how I map from &ldquo;mangafox&rdquo; to the new &ldquo;ExMangaDownloadr.Mangafox.&rdquo; namespace. And because of the dynamic, message passing nature of Elixir, I can replace this code:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">handle_call</span><span class="p">({</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="no">ChapterPages</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="n">chapter_link</span><span class="p">),</span> <span class="n">state</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>With this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">handle_call</span><span class="p">({</span><span class="ss">:chapter_page</span><span class="p">,</span> <span class="n">chapter_link</span><span class="p">,</span> <span class="n">source</span><span class="p">},</span> <span class="n">_from</span><span class="p">,</span> <span class="n">state</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">links</span> <span class="o">=</span> <span class="n">source</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">manga_source</span><span class="p">(</span><span class="s2">&#34;ChapterPage&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">apply</span><span class="p">(</span><span class="ss">:pages</span><span class="p">,</span> <span class="o">[</span><span class="n">chapter_link</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:reply</span><span class="p">,</span> <span class="n">links</span><span class="p">,</span> <span class="n">state</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I can now choose between the &ldquo;Elixir.ExMangaDownloadr.Mangafox.ChapterPage&rdquo; or &ldquo;Elixir.ExMangaDownloadr.MangaReader.ChapterPage&rdquo; modules, call the <tt>pages/1</tt> function and send the same argument as before. I just have to make sure I can receive a &ldquo;source&rdquo; string from the command line now, so I change the CLI module like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">parse_args</span><span class="p">(</span><span class="n">args</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">parse</span> <span class="o">=</span> <span class="no">OptionParser</span><span class="o">.</span><span class="n">parse</span><span class="p">(</span><span class="n">args</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">switches</span><span class="p">:</span> <span class="o">[</span><span class="nb">name</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">url</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">directory</span><span class="p">:</span> <span class="ss">:string</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="ss">:string</span><span class="o">]</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">    <span class="ss">aliases</span><span class="p">:</span> <span class="o">[</span><span class="ss">n</span><span class="p">:</span> <span class="ss">:name</span><span class="p">,</span> <span class="ss">u</span><span class="p">:</span> <span class="ss">:url</span><span class="p">,</span> <span class="ss">d</span><span class="p">:</span> <span class="ss">:directory</span><span class="p">,</span> <span class="ss">s</span><span class="p">:</span> <span class="ss">:source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="n">parse</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="o">[</span><span class="nb">name</span><span class="p">:</span> <span class="n">manga_name</span><span class="p">,</span> <span class="ss">url</span><span class="p">:</span> <span class="n">url</span><span class="p">,</span> <span class="ss">directory</span><span class="p">:</span> <span class="n">directory</span><span class="p">,</span> <span class="ss">source</span><span class="p">:</span> <span class="n">source</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">source</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="o">[</span><span class="nb">name</span><span class="p">:</span> <span class="n">manga_name</span><span class="p">,</span> <span class="ss">directory</span><span class="p">:</span> <span class="n">directory</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="n">_</span><span class="p">,</span> <span class="n">_</span><span class="p">,</span> <span class="n">_</span> <span class="p">}</span> <span class="o">-&gt;</span> <span class="n">process</span><span class="p">(</span><span class="ss">:help</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Compared to the previous version I just added the &ldquo;<tt>:source</tt>&rdquo; string argument to the OptionParser and passed the captured value to <tt>process/4</tt>. I should add some validation here to avoid strings different than &ldquo;mangareader&rdquo; or &ldquo;mangafox&rdquo;, but I will leave that to another time.</p>
<p>And in the Workflow module, instead of starting from just the manga URL, now I have to start with both the URL and the manga source:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">  <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Which means that each of the above functions has to not only return the new URL lists but also pass through the source:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">chapters</span><span class="p">(</span><span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">_manga_title</span><span class="p">,</span> <span class="n">chapter_list</span><span class="p">}</span> <span class="o">=</span> <span class="n">source</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Worker</span><span class="o">.</span><span class="n">manga_source</span><span class="p">(</span><span class="s2">&#34;IndexPage&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">apply</span><span class="p">(</span><span class="ss">:chapters</span><span class="p">,</span> <span class="o">[</span><span class="n">url</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="o">[</span><span class="n">chapter_list</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This was the only function in the Workflow module hardcoded to MangaReader so I also made it dynamic using the same <tt>manga_source/2</tt> function from the Worker, and notice the return value being &ldquo;<tt>[chapter_list, source]</tt>&rdquo; instead of just &ldquo;<tt>chapter_list</tt>&rdquo;.</p>
<p>And now, I can finally test with &ldquo;<tt>mix test</tt>&rdquo; and create the new executable command line binary with &ldquo;<tt>mix escript.build</tt>&rdquo; and run the new version like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>./ex_manga_downloadr -n onepunch -u http://mangafox.me/manga/onepunch_man/ -d /tmp/onepunch -s mangafox</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The Mangafox site is very unreliable with several concurrent connections and it quickly times out sometimes, dumping ugly errors like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>15:58:46.637 [error] Task #PID&lt;0.2367.0&gt; started from #PID&lt;0.124.0&gt; terminating
** (stop) exited in: GenServer.call(#PID&lt;0.90.0&gt;, {:page_download_image, {&#34;http://z.mfcdn.net/store/manga/11362/TBD-053.2/compressed/h006.jpg&#34;, &#34;Onepunch-Man 53.2: 53rd Punch [Fighting Spirit] (2) at MangaFox.me-h006.jpg&#34;}, &#34;/tmp/onepunch&#34;}, 1000000)
    ** (EXIT) an exception was raised:
        ** (HTTPotion.HTTPError) connection_closing
            (httpotion) lib/httpotion.ex:209: HTTPotion.handle_response/1</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>I haven&rsquo;t figured out how to retry HTTPotion requests properly yet. But one small thing I did was add an availability check in the Worker module. So you can just re-run the same command line and it will resume downloading only the remaining files:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">download_image</span><span class="p">({</span><span class="n">image_src</span><span class="p">,</span> <span class="n">image_filename</span><span class="p">},</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">filename</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/</span><span class="si">#{</span><span class="n">image_filename</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">filename</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Image </span><span class="si">#{</span><span class="n">filename</span><span class="si">}</span><span class="s2"> already downloaded, skipping.&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">image_src</span><span class="p">,</span> <span class="n">filename</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">  <span class="k">else</span>
</span></span><span class="line"><span class="cl">    <span class="no">Logger</span><span class="o">.</span><span class="n">debug</span><span class="p">(</span><span class="s2">&#34;Downloading image </span><span class="si">#{</span><span class="n">image_src</span><span class="si">}</span><span class="s2"> to </span><span class="si">#{</span><span class="n">filename</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">image_src</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="o">[</span><span class="ss">headers</span><span class="p">:</span> <span class="o">[</span><span class="s2">&#34;User-Agent&#34;</span><span class="p">:</span> <span class="vi">@user_agent</span><span class="o">]</span><span class="p">,</span> <span class="ss">timeout</span><span class="p">:</span> <span class="vi">@http_timeout</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">_headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="no">File</span><span class="o">.</span><span class="n">write!</span><span class="p">(</span><span class="n">filename</span><span class="p">,</span> <span class="n">body</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">image_src</span><span class="p">,</span> <span class="n">filename</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:err</span><span class="p">,</span> <span class="n">image_src</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This should at least reduce rework. Another thing I am still working on is this other bit at the main &ldquo;CLI.process&rdquo; function:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">directory</span><span class="p">,</span> <span class="n">source</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">File</span><span class="o">.</span><span class="n">mkdir_p!</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">dump_file</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">directory</span><span class="si">}</span><span class="s2">/images_list.dump&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="n">images_list</span> <span class="o">=</span> <span class="k">if</span> <span class="no">File</span><span class="o">.</span><span class="n">exists?</span><span class="p">(</span><span class="n">dump_file</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">                  <span class="ss">:erlang</span><span class="o">.</span><span class="n">binary_to_term</span><span class="p">(</span><span class="no">File</span><span class="o">.</span><span class="n">read</span><span class="p">(</span><span class="n">dump_file</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                <span class="k">else</span>
</span></span><span class="line"><span class="cl">                  <span class="n">list</span> <span class="o">=</span> <span class="o">[</span><span class="n">url</span><span class="p">,</span> <span class="n">source</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">                    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">                    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">                    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span>
</span></span><span class="line"><span class="cl">                  <span class="no">File</span><span class="o">.</span><span class="n">write</span><span class="p">(</span><span class="n">dump_file</span><span class="p">,</span> <span class="ss">:erlang</span><span class="o">.</span><span class="n">term_to_binary</span><span class="p">(</span><span class="n">list</span><span class="p">))</span>
</span></span><span class="line"><span class="cl">                  <span class="n">list</span>
</span></span><span class="line"><span class="cl">                <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="n">images_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">process_downloads</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">optimize_images</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">compile_pdfs</span><span class="p">(</span><span class="n">manga_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">finish_process</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>As you can see the idea is to serialize the final images URL links to a file using the built-in serializer &ldquo;<tt>:erlang.binary_to_term/1</tt>&rdquo; and check if that dump file exists, and deserialize with &ldquo;<tt>:erlang.term_to_binary/1</tt>&rdquo; before fetching all pages all over again. Now the process can resume directly from the <tt>process_downloads/2</tt> function after.</p>
<p>Mangafox is terribly unreliable and I will need to figure out a better way to retry timed out connections without having to crash and manually restart from the command line. It&rsquo;s either a bad site or a clever one that shuts down scrapers like me, although I am guessing it&rsquo;s just bad infrastructure on their side.</p>
<p>If I downgrade from 50 processes to 5 in the pool, it seems to be able to handle it better (but the process slows down, of course):</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl">    <span class="n">pool_options</span> <span class="o">=</span> <span class="o">[</span>
</span></span><span class="line"><span class="cl">      <span class="nb">name</span><span class="p">:</span> <span class="p">{</span><span class="ss">:local</span><span class="p">,</span> <span class="ss">:worker_pool</span><span class="p">},</span>
</span></span><span class="line"><span class="cl">      <span class="ss">worker_module</span><span class="p">:</span> <span class="no">PoolManagement</span><span class="o">.</span><span class="n">Worker</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">size</span><span class="p">:</span> <span class="mi">5</span><span class="p">,</span>
</span></span><span class="line"><span class="cl">      <span class="ss">max_overflow</span><span class="p">:</span> <span class="mi">0</span>
</span></span><span class="line"><span class="cl">    <span class="o">]</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you see time out errors, change this parameter. MangaReader still supports 50 or more for concurrency.</p>
<p>And now you know how to add support for more manga sources. Feel free to send me a Pull Request! :-)</p>
]]></content:encoded><category>elixir-and-erlang</category><category>anime-and-manga</category><category>automation</category></item><item><title>Ex Manga Downloader, an exercise with Elixir</title><link>https://akitaonrails.github.io/en/2015/11/18/ex-manga-downloader-an-exercise-with-elixir/</link><guid isPermaLink="true">https://akitaonrails.github.io/en/2015/11/18/ex-manga-downloader-an-exercise-with-elixir/</guid><pubDate>Wed, 18 Nov 2015 17:26:00 -0200</pubDate><description>&lt;p&gt;&lt;strong&gt;Update 11/19/15:&lt;/strong&gt; In this article I mention a few doubts I had, so read this and then follow through &lt;a href="http://www.akitaonrails.com/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener"&gt;Part 2&lt;/a&gt; to see how I solved it.&lt;/p&gt;
&lt;p&gt;As an exercise (and also because I&amp;rsquo;m obviously an &lt;a href="https://en.wikipedia.org/wiki/Otaku"target="_blank" rel="noopener"&gt;Otaku&lt;/a&gt;) I &lt;a href="https://github.com/akitaonrails/ex_manga_downloadr/blob/master/lib/ex_manga_downloadr/mangareader/index_page.ex"target="_blank" rel="noopener"&gt;implemented&lt;/a&gt; a simple Elixir-based scraper for the great &lt;a href="http://www.mangareader.net/"target="_blank" rel="noopener"&gt;MangaReader&lt;/a&gt; website. One can argue whether it&amp;rsquo;s OK to scrape their website, and one might also argue whether their providing those mangas is OK in the first place, so let&amp;rsquo;s not go down this path.&lt;/p&gt;</description><content:encoded><![CDATA[<p><strong>Update 11/19/15:</strong> In this article I mention a few doubts I had, so read this and then follow through <a href="http://www.akitaonrails.com/en/2015/11/19/ex-manga-downloadr-parte-2-poolboy-ao-resgate"target="_blank" rel="noopener">Part 2</a> to see how I solved it.</p>
<p>As an exercise (and also because I&rsquo;m obviously an <a href="https://en.wikipedia.org/wiki/Otaku"target="_blank" rel="noopener">Otaku</a>) I <a href="https://github.com/akitaonrails/ex_manga_downloadr/blob/master/lib/ex_manga_downloadr/mangareader/index_page.ex"target="_blank" rel="noopener">implemented</a> a simple Elixir-based scraper for the great <a href="http://www.mangareader.net/"target="_blank" rel="noopener">MangaReader</a> website. One can argue whether it&rsquo;s OK to scrape their website, and one might also argue whether their providing those mangas is OK in the first place, so let&rsquo;s not go down this path.</p>
<p>I had an older version <a href="https://github.com/akitaonrails/manga-downloadr"target="_blank" rel="noopener">written in Ruby</a>. It still works but it&rsquo;s in sore need of a good refactoring (sorry about that). The purpose of that version was to see if I could actually do parallel fetching and retry connections using Typhoeus.</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/525/big_ex_manga_downloadr.png" alt="OnePunch Man downloaded"  loading="lazy" /></p>
<p>As I&rsquo;ve been progressing in my studies of Elixir, that tool felt like a great candidate to test my current knowledge of the platform. It would make me test:</p>
<ol>
<li>Fetch and parse ad hoc content through HTTP (<a href="https://github.com/myfreeweb/httpotion"target="_blank" rel="noopener">HTTPotion</a> and <a href="https://github.com/philss/floki"target="_blank" rel="noopener">Floki</a>).</li>
<li>Test parallel/asynchronous downloads (Elixir&rsquo;s built-in <a href="http://elixir-lang.org/docs/stable/elixir/Task.html"target="_blank" rel="noopener">Task</a> module).</li>
<li>The built-in command line and <a href="http://elixir-lang.org/docs/stable/elixir/OptionParser.html"target="_blank" rel="noopener">option parser</a> support.</li>
<li>Basic testing through ExUnit and mocking the workflow (<a href="https://github.com/jjh42/mock"target="_blank" rel="noopener">Mock</a>).</li>
</ol>
<p>The exercise was very interesting, and a scraper is also an ideal candidate for TDD. The initial steps had to go like this:</p>
<ol>
<li>Parse the main manga page to fetch all the chapter links.</li>
<li>Parse each chapter page to fetch all the individual pages.</li>
<li>Parse each page to parse the main embedded image (the actual manga page).</li>
</ol>
<p>For each of those initial steps I did a simple unit test and the <tt>IndexPage</tt>, <tt>ChapterPage</tt> and <tt>Page</tt> modules. They have roughly the same structure, this is one example:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defmodule</span> <span class="no">ExMangaDownloadr</span><span class="o">.</span><span class="n">MangaReader</span><span class="o">.</span><span class="n">IndexPage</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">def</span> <span class="nf">chapters</span><span class="p">(</span><span class="n">manga_root_url</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="k">case</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="n">manga_root_url</span><span class="p">,</span> <span class="o">[</span><span class="ss">timeout</span><span class="p">:</span> <span class="mi">30_000</span><span class="o">]</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="n">body</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="n">_headers</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">fetch_manga_title</span><span class="p">(</span><span class="n">body</span><span class="p">),</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">body</span><span class="p">)</span> <span class="p">}</span>
</span></span><span class="line"><span class="cl">      <span class="n">_</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">        <span class="p">{</span><span class="ss">:err</span><span class="p">,</span> <span class="s2">&#34;not found&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">fetch_manga_title</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#mangaproperties h1&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;h1&#34;</span><span class="p">,</span> <span class="o">[]</span><span class="p">,</span> <span class="o">[</span><span class="n">title</span><span class="o">]</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">title</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="mi">0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">defp</span> <span class="n">fetch_chapters</span><span class="p">(</span><span class="n">html</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="no">Floki</span><span class="o">.</span><span class="n">find</span><span class="p">(</span><span class="n">html</span><span class="p">,</span> <span class="s2">&#34;#listing a&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span> <span class="n">fn</span> <span class="p">{</span><span class="s2">&#34;a&#34;</span><span class="p">,</span> <span class="o">[</span><span class="p">{</span><span class="s2">&#34;href&#34;</span><span class="p">,</span> <span class="n">url</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="n">_</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">url</span> <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Here I am already exercising some of Elixir&rsquo;s awesome features such as pattern matching the result of the <tt>HTTPotion.get/2</tt> function to extract the body from the returning record.</p>
<p>Then I pass the HTML body to 2 different functions: <tt>fetch_manga_title</tt> and <tt>fetch_chapters</tt>. They both use the Floki package which can use CSS selectors to return a List. Then I need to walk through the list (using <tt>Enum.map/2</tt> for example) and pattern match on it to extract the values I need.</p>
<p><a href="http://elixir-lang.org/getting-started/pattern-matching.html"target="_blank" rel="noopener">Pattern Matching</a> is one of the most important concepts to learn about Elixir/Erlang. It&rsquo;s different from simply assigning a value to a variable, it can be used to destructure a value into its components and get their individual parts.</p>
<p>Then I just went through building the skeleton for the command line interface. This is already explained in other tutorials such as <a href="http://asquera.de/blog/2015-04-10/writing-a-commandline-app-in-elixir/"target="_blank" rel="noopener">this</a> and <a href="https://speakerdeck.com/st23am/writing-command-line-applications-in-elixir"target="_blank" rel="noopener">this</a>, so I won&rsquo;t waste time explaining it again. At the core I needed to have the following workflow:</p>
<ol>
<li>Starting from the Manga main URL at MangaReader, extract the chapters</li>
<li>Then loop through the chapters and fetch all the pages</li>
<li>Then loop through the pages and fetch all the image sources</li>
<li>Then loop through the images and download them all to a temporary directory</li>
<li>Then sort through files in the directory and move them to sub-directories of 250 images each (I think it is a good size for each volume)</li>
<li>Finally, resize and convert all the images of each sub-directory into a PDF file for my Kindle to consume.</li>
</ol>
<p>This workflow is defined like this:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">process</span><span class="p">(</span><span class="n">manga_name</span><span class="p">,</span> <span class="n">url</span><span class="p">,</span> <span class="n">directory</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="no">File</span><span class="o">.</span><span class="n">mkdir_p!</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="n">url</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">chapters</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">pages</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">images_sources</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">process_downloads</span><span class="p">(</span><span class="n">directory</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">optimize_images</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">compile_pdfs</span><span class="p">(</span><span class="n">manga_name</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">finish_process</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This is one place where the pipeline notation from Elixir really shines. It&rsquo;s much better than having to write this equivalent:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Workflow</span><span class="o">.</span><span class="n">compile_pdfs</span><span class="p">(</span><span class="no">Workflow</span><span class="o">.</span><span class="n">optimize_images</span><span class="p">(</span><span class="n">directory</span><span class="p">))</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This notation is just syntactic sugar where the returning value of the previous statement is used as the first argument of the following function. Combine that with other syntactic sugars such as parentheses being optional (just like beloved Ruby) and you have a clear exposure of &ldquo;transforming from a URL into compiled PDFs&rdquo;.</p>
<p>I separated the Workflow into its own module and each step is very similar, each taking a list and walking through it. This is the simplest of them:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">pages</span><span class="p">(</span><span class="n">chapter_list</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">chapter_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span> <span class="no">ChapterPage</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">)</span> <span class="k">end</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">await</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="vi">@http_timeout</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p">(</span><span class="o">[]</span><span class="p">,</span> <span class="n">fn</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">list</span><span class="p">},</span> <span class="n">acc</span> <span class="o">-&gt;</span> <span class="n">acc</span> <span class="o">++</span> <span class="n">list</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>If you&rsquo;re new to Elixir here you will find another oddity, this <tt>&quot;&amp;(x(&amp;1))&quot;</tt>, this is just a shortcut macro to this other similar statement:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">(</span><span class="n">list</span><span class="p">)</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">  <span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">    <span class="no">ChapterPage</span><span class="o">.</span><span class="n">pages</span><span class="p">(</span><span class="n">list</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span><span class="p">)</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p><a href="http://elixir-lang.org/docs/stable/elixir/Enum.html"target="_blank" rel="noopener">Enum</a> is one of the most useful modules you need to master. If you come from Ruby it feels like home, you must learn all of its functions. You&rsquo;re usually having to transform one collection into another, so it&rsquo;s important to study it thoroughly.</p>
<h3>A few problems understanding parallel HTTP processing (W.I.P.)<span class="hx:absolute hx:-mt-20" id="a-few-problems-understanding-parallel-http-processing-wip"></span>
    <a href="#a-few-problems-understanding-parallel-http-processing-wip" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>Then there is this <tt>Task.async/await</tt> deal. If you&rsquo;re from a language that has Threads, it&rsquo;s quite similar: you start several different Threads and await all of them returning before continuing. But a Task in Elixir is not a real thread, it&rsquo;s a <a href="https://en.wikipedia.org/wiki/Green_threads#Green_threads_in_other_languages"target="_blank" rel="noopener">&ldquo;green thread&rdquo;</a> or, in Erlang lingo, a very lightweight &ldquo;process&rdquo;. Erlang uses processes for everything, and so does Elixir. Under the hood, the &ldquo;Task&rdquo; module encapsulates the entire OTP framework for supervisors/workers. But instead of having to deal right now with <a href="http://elixir-lang.org/getting-started/mix-otp/genserver.html"target="_blank" rel="noopener">OTP GenServer</a> I decided to go the simpler route for now, and the &ldquo;Task&rdquo; module accomplishes just that.</p>
<p>Then I ended up with a problem. If I just kept going like this, spawning hundreds of async HTTP calls, I quickly ended up with this exception:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>17:10:55.882 [error] Task #PID&lt;0.2217.0&gt; started from #PID&lt;0.69.0&gt; terminating
** (HTTPotion.HTTPError) req_timedout
    (httpotion) lib/httpotion.ex:209: HTTPotion.handle_response/1
    (ex_manga_downloadr) lib/ex_manga_downloadr/mangareader/page.ex:6: ExMangaDownloadr.MangaReader.Page.image/1
    (elixir) lib/task/supervised.ex:74: Task.Supervised.do_apply/2
    (elixir) lib/task/supervised.ex:19: Task.Supervised.async/3
    (stdlib) proc_lib.erl:240: :proc_lib.init_p_do_apply/3
Function: #Function&lt;12.106612505/0 in ExMangaDownloadr.Workflow.images_sources/1&gt;
    Args: []</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>That&rsquo;s why there is <tt>@maximum_fetches 80</tt> at the top of the <tt>Workflow</tt> module, together with this other odd construction:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="k">def</span> <span class="nf">images_sources</span><span class="p">(</span><span class="n">pages_list</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">pages_list</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="n">chunk</span><span class="p">(</span><span class="vi">@maximum_fetches</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reduce</span><span class="p">(</span><span class="o">[]</span><span class="p">,</span> <span class="n">fn</span> <span class="n">pages_chunk</span><span class="p">,</span> <span class="n">acc</span> <span class="o">-&gt;</span>
</span></span><span class="line"><span class="cl">      <span class="n">result</span> <span class="o">=</span> <span class="n">pages_chunk</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">async</span><span class="p">(</span><span class="n">fn</span> <span class="o">-&gt;</span> <span class="no">Page</span><span class="o">.</span><span class="n">image</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">)</span> <span class="k">end</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="o">&amp;</span><span class="p">(</span><span class="no">Task</span><span class="o">.</span><span class="n">await</span><span class="p">(</span><span class="o">&amp;</span><span class="mi">1</span><span class="p">,</span> <span class="vi">@http_timeout</span><span class="p">)))</span>
</span></span><span class="line"><span class="cl">        <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">map</span><span class="p">(</span><span class="n">fn</span> <span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="n">image</span><span class="p">}</span> <span class="o">-&gt;</span> <span class="n">image</span> <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">acc</span> <span class="o">++</span> <span class="n">result</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span><span class="p">)</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>This gets a huge list (such as all the pages of a very long manga like Naruto), breaks it down into smaller lists of 80 elements and then proceeds to fire up the asynchronous Tasks, reducing the results back to a plain List. The <tt>chunk/2</tt> private function just gets the smaller size between the list length and the maximum fetches value.</p>
<p>Sometimes it breaks down if the maximum is larger, sometimes it doesn&rsquo;t, so my guess is that my code isn&rsquo;t dealing with network instabilities (with some retry logic) or that the MangaReader site is queueing up above my designated timeout (which I set to 30 seconds). Either way, keeping the maximum value lower than 100 seems to be a good balance without crashing the workflow.</p>
<p>This is one part where I&rsquo;m not entirely sure what to do to deal with uncertainties like the external website not responding or the network going down for a little while. HTTPotion has support for asynchronous calls, but I don&rsquo;t know what the difference is between using that and just making synchronous calls within parallel processes with Task, the way I&rsquo;m doing. And in either case, they are supervised workers, how do I handle the exceptions, how should I implement logic to retry the call once it fails? If anyone has more knowledge about this, a comment below will be really appreciated.</p>
<p>Finally, there is one dirty trick behind why I like to use MangaReader: it&rsquo;s very friendly to scrapers because on each page of the manga the image is annotated with an &ldquo;alt&rdquo; attribute with the format &ldquo;[manga name] [chapter number] - [page number]&rdquo;. So I just had to reformat it a bit, adding a pad of zeroes before the chapter and page number so a simple sort of the downloaded files gives me the correct order. MangaFox is not so friendly. This is how to reformat it:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="n">defp</span> <span class="n">normalize_metadata</span><span class="p">(</span><span class="n">image_src</span><span class="p">,</span> <span class="n">image_alt</span><span class="p">)</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">extension</span>      <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">image_src</span><span class="p">,</span> <span class="s2">&#34;.&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="o">-</span><span class="mi">1</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">list</span>           <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">image_alt</span><span class="p">)</span>      <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reverse</span>
</span></span><span class="line"><span class="cl">  <span class="n">title_name</span>     <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">slice</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="mi">4</span><span class="p">,</span> <span class="no">Enum</span><span class="o">.</span><span class="n">count</span><span class="p">(</span><span class="n">list</span><span class="p">)</span> <span class="o">-</span> <span class="mi">1</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">join</span><span class="p">(</span><span class="s2">&#34; &#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">chapter_number</span> <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="mi">3</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="sc">?0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="n">page_number</span>    <span class="o">=</span> <span class="no">Enum</span><span class="o">.</span><span class="n">at</span><span class="p">(</span><span class="n">list</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="nb">String</span><span class="o">.</span><span class="n">rjust</span><span class="p">(</span><span class="mi">5</span><span class="p">,</span> <span class="sc">?0</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">  <span class="p">{</span><span class="n">image_src</span><span class="p">,</span> <span class="s2">&#34;</span><span class="si">#{</span><span class="n">title_name</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">chapter_number</span><span class="si">}</span><span class="s2"> - Page </span><span class="si">#{</span><span class="n">page_number</span><span class="si">}</span><span class="s2">.</span><span class="si">#{</span><span class="n">extension</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>Once I have all the images, I spawn another external process using <a href="https://github.com/alco/porcelain"target="_blank" rel="noopener">Porcelain</a> to deal with shelling out to run ImageMagick&rsquo;s Mogrify and Convert tools to resize all the images down to 600x800 pixels (Kindle Voyage resolution) and pack them together into a PDF file. This results in PDF files with 250 pages and around 20Mb in size. Now it is just a matter of copying the files to my Kindle through USB.</p>
<p>The ImageMagick code is quite boring, I just generate the commands in the following format for Mogrify:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#34;mogrify -resize #{@image_dimensions} #{directory}/*.jpg&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>And compile the PDFs with this other command:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><pre><code>&#34;convert #{volume_directory}/*.jpg #{volume_file}&#34;</code></pre></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>(By the way, notice the Ruby-like String interpolation we&rsquo;re used to.)</p>
<p><img src="https://akitaonrails.s3.amazonaws.com/assets/image_asset/image/526/big_11249911_10153742264949837_7817568172948440418_n.jpg" alt="OnePunch Man in Kindle"  loading="lazy" /></p>
<p>Technically I could copy the MangaReader module files into a new MangaFox module and repurpose the same Workflow logic once I tweak the parsers to deal with MangaFox page format. But I leave that as an exercise to the reader.</p>
<p>The MangaReader module tests do real calls to their website. I left it that way on purpose because if the test fails it means that they changed the website format and the parser needs tweaking to conform. But after a few years, I never saw them change enough to break my old Ruby parser.</p>
<p>Just as a final exercise I imported the Mock package, to control what some inner pieces of the Workflow implementation return. It&rsquo;s called Mock but it&rsquo;s more like stubbing particular functions of a module. I can declare a block where I override the <tt>File.rename/1</tt> so it doesn&rsquo;t actually try to move a file that doesn&rsquo;t exist in the test environment. This makes the test more brittle because it depends on a particular implementation, which is never good, but when we are dealing with external I/O, this might be the only way to isolate it. This is how the Workflow test was done. Again, if there is a better way I am eager to learn it, please comment down below.</p>
<p>This is how a unit test with Mock looks like, stubbing both the HTTPotion and File modules:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="nb">test</span> <span class="s2">&#34;workflow tries to download the images&#34;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="n">with_mock</span> <span class="no">HTTPotion</span><span class="p">,</span> <span class="o">[</span><span class="ss">get</span><span class="p">:</span> <span class="n">fn</span><span class="p">(</span><span class="n">_url</span><span class="p">,</span> <span class="n">_options</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="o">%</span><span class="no">HTTPotion</span><span class="o">.</span><span class="n">Response</span><span class="p">{</span> <span class="ss">body</span><span class="p">:</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">headers</span><span class="p">:</span> <span class="kp">nil</span><span class="p">,</span> <span class="ss">status_code</span><span class="p">:</span> <span class="mi">200</span> <span class="p">}</span> <span class="k">end</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">    <span class="n">with_mock</span> <span class="no">File</span><span class="p">,</span> <span class="o">[</span><span class="n">write!</span><span class="p">:</span> <span class="n">fn</span><span class="p">(</span><span class="n">_filename</span><span class="p">,</span> <span class="n">_body</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kp">nil</span> <span class="k">end</span><span class="o">]</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">      <span class="n">assert</span> <span class="no">Workflow</span><span class="o">.</span><span class="n">process_downloads</span><span class="p">(</span><span class="o">[</span><span class="p">{</span><span class="s2">&#34;http://src_foo&#34;</span><span class="p">,</span> <span class="s2">&#34;filename_foo&#34;</span><span class="p">}</span><span class="o">]</span><span class="p">,</span> <span class="s2">&#34;/tmp&#34;</span><span class="p">)</span> <span class="o">==</span> <span class="o">[</span><span class="p">{</span><span class="ss">:ok</span><span class="p">,</span> <span class="s2">&#34;http://src_foo&#34;</span><span class="p">,</span> <span class="s2">&#34;/tmp/filename_foo&#34;</span><span class="p">}</span><span class="o">]</span>
</span></span><span class="line"><span class="cl">      <span class="n">assert</span> <span class="n">called</span> <span class="no">HTTPotion</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;http://src_foo&#34;</span><span class="p">,</span> <span class="o">[</span><span class="ss">timeout</span><span class="p">:</span> <span class="mi">30_000</span><span class="o">]</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">      <span class="n">assert</span> <span class="n">called</span> <span class="no">File</span><span class="o">.</span><span class="n">write!</span><span class="p">(</span><span class="s2">&#34;/tmp/filename_foo&#34;</span><span class="p">,</span> <span class="kp">nil</span><span class="p">)</span>
</span></span><span class="line"><span class="cl">    <span class="k">end</span>
</span></span><span class="line"><span class="cl">  <span class="k">end</span>
</span></span><span class="line"><span class="cl"><span class="k">end</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<h3>Conclusion<span class="hx:absolute hx:-mt-20" id="conclusion"></span>
    <a href="#conclusion" class="subheading-anchor" aria-label="Permalink for this section"></a></h3><p>This has been a very fun experience, albeit very short, and good enough to iron out what I have learned so far. Code like this makes me smile:</p>
<div class="hextra-code-block hx:relative hx:mt-6 hx:first:mt-0 hx:group/code">

<div><div class="highlight"><pre tabindex="0" class="chroma"><code class="language-ruby" data-lang="ruby"><span class="line"><span class="cl"><span class="o">[</span><span class="n">destination_file</span><span class="o">|</span><span class="n">_rest</span><span class="o">]</span> <span class="o">=</span> <span class="nb">String</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="n">file</span><span class="p">,</span> <span class="s2">&#34;/&#34;</span><span class="p">)</span> <span class="o">|&gt;</span> <span class="no">Enum</span><span class="o">.</span><span class="n">reverse</span></span></span></code></pre></div></div><div class="hextra-code-copy-btn-container hx:opacity-0 hx:transition hx:group-hover/code:opacity-100 hx:flex hx:gap-1 hx:absolute hx:m-[11px] hx:right-0 hx:top-0">
  <button
    class="hextra-code-copy-btn hx:group/copybtn hx:cursor-pointer hx:transition-all hx:active:opacity-50 hx:bg-primary-700/5 hx:border hx:border-black/5 hx:text-gray-600 hx:hover:text-gray-900 hx:rounded-md hx:p-1.5 hx:dark:bg-primary-300/10 hx:dark:border-white/10 hx:dark:text-gray-400 hx:dark:hover:text-gray-50"
    title="Copy code"
  >
    <div class="hextra-copy-icon hx:group-[.copied]/copybtn:hidden hx:pointer-events-none hx:h-4 hx:w-4"></div>
<div class="hextra-success-icon hx:hidden hx:group-[.copied]/copybtn:block hx:pointer-events-none hx:h-4 hx:w-4"></div>
  </button>
</div>
</div>
<p>The way I can pattern match to extract the head of a list is a different way of thinking. Then there is the other most important way of thinking: everything is a transformation chain, an application is a way to start from some input argument (such as a URL) and go step by step to &ldquo;transform&rdquo; it into a collection of PDF files, for example.</p>
<p>Instead of thinking about how to architect classes and objects, we start thinking about what the initial arguments are and what result we want to achieve, and go from there, one small transformation function at a time.</p>
<p>The Workflow module is an example. I actually started writing everything in a single large function in the CLI module. Then I refactored into smaller functions and chained them together to create the Workflow. Finally, I just moved all the functions into the Workflow module and called that from the CLI module.</p>
<p>Because of no global state, and thinking in smaller, isolated functions, both refactoring and test-driven development are much smoother than in OOP languages. This way of thinking is admittedly slow to get a grip on, but then it starts to feel very natural and it quickly steers your way of programming into leaner code.</p>
<p>And the dynamic aspects of both Erlang and Elixir make me feel right at home, just like having an &ldquo;improved Ruby&rdquo;.</p>
<p>The code of the downloader is all on <a href="https://github.com/akitaonrails/ex_manga_downloadr/blob/master/lib/ex_manga_downloadr/mangareader/index_page.ex"target="_blank" rel="noopener">Github, please fork it</a>.</p>
<p>I am eager to exercise more. I hope this motivates you to learn Elixir. And if you&rsquo;re already an advanced programmer in Elixir or Erlang, don&rsquo;t forget to comment below and even send me a Pull Request to improve this small exercise. I am still a beginner and there is a lot of room to learn more. All contributions are greatly appreciated.</p>
]]></content:encoded><category>elixir-and-erlang</category><category>anime-and-manga</category><category>automation</category></item></channel></rss>