<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[Tech Notes]]></title><description><![CDATA[Tech Notes]]></description><link>https://mevtho.com/</link><image><url>https://mevtho.com/favicon.png</url><title>Tech Notes</title><link>https://mevtho.com/</link></image><generator>Ghost 5.35</generator><lastBuildDate>Mon, 06 Apr 2026 12:11:58 GMT</lastBuildDate><atom:link href="https://mevtho.com/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Nginx Too Big Header]]></title><description><![CDATA[<p>I recently encountered an issue where one of the pages for <a href="https://allwin.co">allwin.co</a> resulted in a </p><blockquote class="kg-blockquote-alt">502 Bad Gateway error</blockquote><p>After looking at the nginx logs, I could see that it was due to that specific page processing ending in <strong>upstream sent too big header while reading response header from</strong></p>]]></description><link>https://mevtho.com/nginx-buffer-size/</link><guid isPermaLink="false">648144a08547f2529ed7469d</guid><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Thu, 08 Jun 2023 03:24:24 GMT</pubDate><content:encoded><![CDATA[<p>I recently encountered an issue where one of the pages for <a href="https://allwin.co">allwin.co</a> resulted in a </p><blockquote class="kg-blockquote-alt">502 Bad Gateway error</blockquote><p>After looking at the nginx logs, I could see that it was due to that specific page processing ending in <strong>upstream sent too big header while reading response header from upstream</strong>.</p><p>I understood the issue, but, I couldn&apos;t figure out &quot;why?&quot;. Why something happened on that page and not on others. Especially as things being simple, I didn&apos;t have to manipulate the http headers of the response at all.</p><p>I quickly found an answer as how to fix, adding the following lines to nginx configuration:</p><pre><code class="language-nginx">location ~ \.php$ { 
	# ...
	fastcgi_buffers 16 16k;
	fastcgi_buffer_size 32k;
    #...
}</code></pre><p>But still, why? why is it happening?</p><p>I could update the configuration blindly, but, acting on the production environment, I like to understand all configuration changes. As such, back to the question : why is it happening?</p><p>I found my answer looking at the headers from a different page. One that worked. The <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Link">Link header</a> ... I started the project using <strong>Laravel Breeze</strong> with <strong>Inertia</strong>, <strong>React</strong> and <strong>SSR enabled</strong>. By default, all js assets are broken down into their own files. And a link to each component file was present in the header... And for that specific page, it meant a long list bringing the header length over the limit.</p><p>There it is, the reason why ... so, configuration updated and I know why.</p><p>If you are in the same context, you may now know why as well. If you have the issue, look at the headers of a page that works what may make it too big in a different context.</p><p></p><p></p>]]></content:encoded></item><item><title><![CDATA[Hacking Mobile App APIs for Automation]]></title><description><![CDATA[Mobile Apps / games are nice, but I often wish the  experience would be better, more advanced. They are a one size fit all that could be improved. Time to hack a better way... ]]></description><link>https://mevtho.com/hacking-mobile-app-apis-for-automation/</link><guid isPermaLink="false">63f47b2223263dd710c86ae6</guid><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Tue, 21 Feb 2023 10:51:37 GMT</pubDate><media:content url="https://mevtho.com/content/images/2023/02/Screenshot-2023-02-21-at-16.23.36.png" medium="image"/><content:encoded><![CDATA[<img src="https://mevtho.com/content/images/2023/02/Screenshot-2023-02-21-at-16.23.36.png" alt="Hacking Mobile App APIs for Automation"><p>As mentioned in another article, <a href="https://mevtho.com/website-as-list/">Website As List</a>, I like to improve some of my activities building automations.</p><p>Recently, I have looked into hacking mobile apps and APIs for fun, let&apos;s dig deeper.</p><p><u>Disclaimer</u>: As with everything I do, I do it, the &apos;nice&apos; way. I am not trying to bring anything down or cheat the games or ... What I want to do is reuse some of existing things to build tools to enhance my personal experience. All that in an ethical way.</p><h2 id="context">Context</h2><blockquote class="kg-blockquote-alt">While the context is specific to what I wanna do, the techniques used can be applied to other ones too.</blockquote><p>During my spare time, for fun, I like to use the Panini Dunk mobile app. It is basically collecting basketball cards in an app. It is not really useful in any way, but it helps spend time on thing I enjoy.</p><p>However, there are some problems with it... Or things I don&apos;t necessarily like.</p><p>The main one is the UX. It is too slow, necessitate to many taps, isn&apos;t flexible to make it nice and there is no web app to make the experience better out of the phone.</p><p>Also, I am collecting / trying to gather Jason Kidd cards there, as my physical cards is as well, and ... I would rather not loose it if the app disappear at some point... (<a href="https://saasify.work/platform-risk/">https://saasify.work/platform-risk/</a>).</p><p>So, I started looking into it, could I access my account data and build some personal page and tools from it. Turns out, it is possible.</p><h2 id="tools-needed">Tools needed</h2><p>I strongly recommend using Postman (<a href="https://www.postman.com/">https://www.postman.com/</a>) an API client to help with most of the work.</p><h2 id="execution">Execution</h2><h3 id="what-do-i-want-accomplished">What do I want accomplished</h3><p>The most basic thing I want is two things:</p><ul><li>A page that can show my collection</li><li>An script that can alert me when a new Jason Kidd card is put on for auction</li></ul><h3 id="understanding-how-it-works">Understanding how it works</h3><p>The very first step in order to do any hacking work, is to understand what are the various components and how they work together.</p><p>In the case of the Panini app, it is rather simple. There is the mobile app on one side, which is what the user is interacting with and seems to be built in Unity, and then there is a server side component that host the data. The mobile app &lt;&gt; server side exchanges are done through a rest API using json.</p><p>Different calls going to different host. This understanding comes from the following section.</p><h3 id="capturing-the-calls">Capturing the calls</h3><p>First, given the context and the mechanics of the game, it was safe to assume that there were calls made between the app and servers somewhere. And given the tech context those days, it was also safe to assume that it was likely done through standard http calls.</p><p>The first step then to understand how it works was to see what get exchanged between the mobile app and the server. The easiest way to do that is to use Postman&apos;s proxy and capture the request.</p><p>This involves the following steps:</p><ul><li><a href="https://learning.postman.com/docs/sending-requests/capturing-request-data/capturing-http-requests/">Setting up and enabling the proxy in Postman</a></li><li>Configuring my phone&apos;s wifi connection to use Postman&apos;s proxy (and accepting the SSL certificates)</li><li>Starting the capture in Postman</li><li>Use the app to get to the content I want to see/access</li></ul><p>When you do that, if everything is setup as expected, and the app indeed uses http calls, you should be able to use the app as if there was no proxy, and all the request should start to appear in Postman.</p><p>Thankfully, my assumptions were correct and I can see all the calls I need</p><h3 id="understanding-calls-and-what-you-can-do">Understanding calls and what you can do</h3><p>The next step, then is to understand what happens, when and how.</p><p>One aspect is to go step by step in the mobile app and see what calls get made. It becomes easy as it is as if you were using any other rest API. It gives you the context in which things happen. With that and the API request in Postman (URL, body, ...) you should start having a good idea of how it works.</p><p>Another aspect is to start analyzing and tweaking the calls you are interested in in Postman and see how the server responds. Few things of interests are, the URLs and request used (GET, POST, ...) , how (if any) is authentication done, is there any request signature involved, any pagination ? , &#xA0;... Anything that can get you from the request the app did to what you wish the api can give you.</p><p>When you understand the API, you will have an idea of what&apos;s possible, can you achieve what you want and how, or possibly other ideas.</p><h3 id="building-pages-and-tools">Building pages and tools</h3><p>Unfortunately, the Panini API used is quite secure, it uses jwt tokens with encryption, but also requires a valid request signature to return data. That&apos;s quite annoying as it is hard to guess the key used.</p><p>For now, that means it reduces my ambitions and it means that my tools can&apos;t be too generic and I can&apos;t share or make them available to other if I want to. It&apos;s fine, I will stick with having those for me for now.</p><p>To build the tools, you can take advantage of the API call to source code feature in Postman. It can give a starting point. And then, it becomes like any other programming script. Get the data, and do something with it. </p><p>Here is my auction alert script, using <a href="https://pushover.net">pushover</a> for notifications.</p><figure class="kg-card kg-code-card"><pre><code class="language-php">&lt;?php

// Replace XXXXX with relevant values
$pushoverAppToken = &apos;XXXXX&apos;;
$pushoverKey = &apos;XXXXX&apos;;

$paniniAppId = &apos;XXXXX&apos;;
$paniniBearerToken = &quot;XXXXX&quot;;
$paniniNonce = &quot;XXXXX&quot;;
$paniniSignature = &quot;XXXXX&quot;;

$cacheKnownAuctionsFile = &apos;auctions-jason-kidd.txt&apos;;

//region Helpers and Grouping
function pushoverNotify(array $notificationData = []) {
    global $pushoverAppToken, $pushoverKey;

    $curl = curl_init();
    curl_setopt_array($curl, [
        CURLOPT_URL =&gt; &quot;https://api.pushover.net/1/messages.json&quot;,
        CURLOPT_POSTFIELDS =&gt; [
            &quot;token&quot; =&gt; $pushoverAppToken,
            &quot;user&quot; =&gt; $pushoverKey,
            ...$notificationData
        ],
        CURLOPT_RETURNTRANSFER =&gt; true
    ]);
    curl_exec($curl);
    curl_close($curl);
}

function auctionDescription($anAuction)
{
    return join(&quot; &quot;, [
        $anAuction-&gt;card-&gt;collection_name,
        $anAuction-&gt;card-&gt;group_name,
        empty($anAuction-&gt;card-&gt;card_limit) ? &quot;&quot; : &quot;/&quot;. $anAuction-&gt;card-&gt;card_limit
    ]);
}

function callPaniniAPI() {
    global $paniniAppId, $paniniBearerToken, $paniniNonce, $paniniSignature;

    $body = (object) [
        &quot;attributes&quot; =&gt; (object) [
            &quot;level&quot; =&gt; 0,
            &quot;keyword&quot; =&gt; &quot;Jason Kidd&quot;,
            &quot;albumNames&quot; =&gt; [],
            &quot;teams&quot; =&gt; [],
            &quot;collections&quot; =&gt; &quot;&quot;,
            &quot;collectionIds&quot; =&gt; [],
            &quot;groups&quot; =&gt; [],
            &quot;positions&quot; =&gt; [],
            &quot;sortBy&quot; =&gt; 1,
            &quot;sortOrder&quot; =&gt; 1,
            &quot;is_featured&quot; =&gt; false,
            &quot;is_L_N&quot; =&gt; false,
            &quot;filter_card_type&quot; =&gt; 0,
            &quot;usr_crds_cnt&quot; =&gt; 0,
            &quot;lock_type&quot; =&gt; [],
            &quot;exclude_card_ids&quot; =&gt; [],
            &quot;src_typ&quot; =&gt; &quot;&quot;,
            &quot;filterBy&quot; =&gt; 0,
            &quot;safe_cards&quot; =&gt; true,
            &quot;from_trade&quot; =&gt; false
        ]
    ];

    $bodyStr = json_encode($body);

    $headers = [
        &apos;host&apos; =&gt; &apos;userinventory.panininba.com&apos;,
        &apos;content-type&apos; =&gt; &apos;application/json&apos;,
        &apos;x-unity-version&apos; =&gt; &apos;2020.3.12f1&apos;,
        &apos;accept&apos; =&gt; &apos;*/*&apos;,
        &apos;authorization&apos; =&gt; &quot;Bearer $paniniBearerToken&quot;,
        &apos;nonce&apos; =&gt; $paniniNonce,
        &apos;app_version&apos; =&gt; &apos;2.3.0&apos;,
        &apos;accept-language&apos; =&gt; &apos;en-US,en;q=0.9&apos;,
        &apos;accept-encoding&apos; =&gt; &apos;gzip, deflate, br&apos;,
        &apos;signature&apos; =&gt; $paniniSignature,
        &apos;appid&apos; =&gt; $paniniAppId,
        &apos;content-length&apos; =&gt; strlen($bodyStr),
        &apos;user-agent&apos; =&gt; &apos;NBADunk/262 CFNetwork/1402.0.8 Darwin/22.2.0&apos;,
        &apos;connection&apos; =&gt; &apos;keep-alive&apos;,
        &apos;os_version&apos; =&gt; &apos;iOS 16.2&apos;,
        &apos;os_type&apos; =&gt; &apos;iOS&apos;
    ];

    $curl = curl_init();

    curl_setopt_array($curl, array(
        CURLOPT_URL =&gt; &apos;https://userinventory.panininba.com/v5/auction?featured=false&amp;l=30&amp;t=0&apos;,
        CURLOPT_RETURNTRANSFER =&gt; true,
        CURLOPT_ENCODING =&gt; &apos;&apos;,
        CURLOPT_MAXREDIRS =&gt; 10,
        CURLOPT_TIMEOUT =&gt; 0,
        CURLOPT_FOLLOWLOCATION =&gt; true,
        CURLOPT_HTTP_VERSION =&gt; CURL_HTTP_VERSION_1_1,
        CURLOPT_CUSTOMREQUEST =&gt; &apos;PUT&apos;,
        CURLOPT_POSTFIELDS =&gt; json_encode($body),
        CURLOPT_HTTPHEADER =&gt; array_map(fn($key, $value) =&gt; $key . &apos;:&apos; . $value, array_keys($headers), $headers)
    ));

    $response = curl_exec($curl);

    curl_close($curl);

    return $response;
}

//endregion

$response = callPaniniAPI();

// === Process the response
if ($response === false) {
    pushoverNotify([&quot;message&quot; =&gt; &quot;DUNK - Failed to retrieve auction data&quot;, &quot;priority&quot; =&gt; 1]);
    die();
}

$responseObj = json_decode($response);
if ($responseObj-&gt;status !== 200) {
    pushoverNotify([&quot;message&quot; =&gt; &quot;DUNK - Failed to retrieve auction data : {$responseObj-&gt;message}&quot;, &quot;priority&quot; =&gt; 1]);
    die();
}

$priorAuctions = [];
if (file_exists($cacheKnownAuctionsFile)) {
    $priorAuctions = json_decode(file_get_contents($cacheKnownAuctionsFile));
    unlink($cacheKnownAuctionsFile);
}

$allAuctions = $responseObj-&gt;data;
$currentAuctions = array_map(fn($anAuction) =&gt; $anAuction-&gt;_id, $allAuctions ?? []);
file_put_contents($cacheKnownAuctionsFile, json_encode($currentAuctions));


$newAuctions = array_diff($currentAuctions, $priorAuctions);
if (count($newAuctions) &gt; 0) {
    printf(&quot;Notifying of %d new auctions\n&quot;, count($newAuctions));
    pushoverNotify([
        &quot;title&quot; =&gt; &quot;DUNK - New auctions for Jason Kidd&quot;,
        &quot;html&quot; =&gt; 1,
        &quot;message&quot; =&gt; implode(&quot;\n&quot;, [
            count($newAuctions) . &quot; new auctions&quot;,
            ... array_map(fn($anAuction) =&gt; &quot;&lt;a href=&apos;{$anAuction-&gt;card-&gt;image_url}&apos;&gt;&quot;.auctionDescription($anAuction).&quot;&lt;/a&gt;&quot;,
                array_filter($allAuctions, fn($anAuction) =&gt; array_search($anAuction-&gt;_id, $newAuctions) !== false)
            )
        ])
    ]);
} else {
    printf(&quot;No new auction\n&quot;);
}</code></pre><figcaption>Send pushover notification on new panini dunk auction</figcaption></figure><!--kg-card-begin: html--><iframe src="https://dunk.mevtho.com/" height="360" title="Main Collection">

</iframe><!--kg-card-end: html--><div class="kg-card kg-button-card kg-align-center"><a href="https://dunk.mevtho.com&quot;&gt;Access Page" class="kg-btn kg-btn-accent">Show Page</a></div><h2 id="going-further">Going further</h2><p>One step that I am still struggling on, and I will make an update when I solve the issue, is to generate the required signature for the request. I think I have identified how it is calculated but there may be an encryption key that I am still missing and would have to crack... </p><p>In order to do so, my first step what to get the android app .apk file and see whether I could reverse engineer it back to as close as the source code as possible, hoping to find something that looked like and encryption key, algorithm, ... anything to help. No success so far, but I am also in unknown territory and I need to learn how everything work there.</p><p>Hopefully I can find a way soon...</p><p>That would allow more advanced tools and automations to be built, especially to parameterize the API calls. I wouldn&apos;t have to rely on the proxy and making manual calls thourgh the app anymore.</p><p>Ideally though, Panini could either build a web app for it or give access to some API to make it easier. Unfortunately, with Fanatics getting the exclusive license for NBA cards (<a href="https://www.forbes.com/sites/tommybeer/2021/08/23/report-fanatics-strikes-again-set-to-become-exclusive-licensee-for-nba-trading-cards">read more</a>), I don&apos;t see it happening anytime.</p>]]></content:encoded></item><item><title><![CDATA[Don't start a search for my local tld]]></title><description><![CDATA[For local web development purposes, I use the Firefox web browser and laravel valet. Here's how to make sure your local domain opens your website in Firefox, not search it on the internet.]]></description><link>https://mevtho.com/dont-search-my-local-domain/</link><guid isPermaLink="false">63d21c5c23263dd710c86ac0</guid><category><![CDATA[How-To]]></category><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Thu, 26 Jan 2023 06:33:20 GMT</pubDate><content:encoded><![CDATA[<p>For local web development purposes, I use the Firefox web browser and laravel valet. It easily gives a local url to use for your project.</p><p>I configured the url to use the <code>.loc</code> tld, mainly because I prefer that to the other options, it reminds of localhost while being shorter.</p><p>One of my issue with Firefox has been that for those domains, unless I use valet secure to enable https, instead of opening my website, it would start a google search for the text.</p><p>Typing &apos;mevtho.loc&apos; would result in a new google search for &apos;mevtho.loc&apos;. That forced me to manually type the full URL, including http. So, &apos;http://mevtho.loc&apos; would bring me to the website.</p><p>I have finally taken the time and figured out how to prevent this behaviour :</p><ol><li>Head to firefox settings, using <code>about:config</code> in the URL bar</li><li>Type the following setting string : <code>browser.fixup.domainsuffixwhitelist.&lt;tld&gt;</code>, replacing &lt;tld&gt; by what you want to use. In my case : <code>browser.fixup.domainsuffixwhitelist.loc</code> and set the value to boolean <code>true</code>.</li><li>Open a new tab and try your URL, it should now work as expected, not starting any google search.</li></ol>]]></content:encoded></item><item><title><![CDATA[Website as list]]></title><description><![CDATA[<p>&#x2003;Weird design, data spread around, cumbersome display, annoying pagination with few items and lots of pages, frequency of access, ease of filtering, ... are some of the lot of reasons why nowadays I like to view web content not as they are intended by the website they come from, but</p>]]></description><link>https://mevtho.com/website-as-list/</link><guid isPermaLink="false">6335314f8528f79c823a8df3</guid><category><![CDATA[How-To]]></category><category><![CDATA[php]]></category><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Thu, 29 Sep 2022 08:24:32 GMT</pubDate><media:content url="https://images.unsplash.com/photo-1579532582937-16c108930bf6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDExfHxzcHJhdHQlMjBzaGFyZXN8ZW58MHx8fHwxNjY0NDM5Nzcw&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" medium="image"/><content:encoded><![CDATA[<img src="https://images.unsplash.com/photo-1579532582937-16c108930bf6?crop=entropy&amp;cs=tinysrgb&amp;fit=max&amp;fm=jpg&amp;ixid=MnwxMTc3M3wwfDF8c2VhcmNofDExfHxzcHJhdHQlMjBzaGFyZXN8ZW58MHx8fHwxNjY0NDM5Nzcw&amp;ixlib=rb-1.2.1&amp;q=80&amp;w=2000" alt="Website as list"><p>&#x2003;Weird design, data spread around, cumbersome display, annoying pagination with few items and lots of pages, frequency of access, ease of filtering, ... are some of the lot of reasons why nowadays I like to view web content not as they are intended by the website they come from, but as a simple list.</p><p>&#x2003;That&apos;s why I have ended up building a collection of quick scripts and snippets that gets me what I want. Here are the strategies used.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">The code snippets come from various scripts and/or have been created only to illustrate my explanations. Because they are for private use, I build them as quickly as I can without necessarily caring much about improving them. The end result is what matters. Let me know if you would like to see a more formalized / standardized script.</div></div><h2 id="retrieving-the-data">Retrieving the data</h2><h3 id="how-the-website-gets-its-content">How the website gets its content&#x2003;</h3><p>&#x2003;The first step of the process of building your list is to understand how the data gets from the server to your browser to be rendered. Nowadays, there are usually two ways this happens. Either as the initial web page request, or as an asynchronous call later.</p><p>&#x2003;To determine how it is done, head to the network tab in your web browser and refresh the page.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://mevtho.com/content/images/2022/09/Screenshot-2022-09-29-at-13.06.00.png" class="kg-image" alt="Website as list" loading="lazy" width="1024" height="281" srcset="https://mevtho.com/content/images/size/w600/2022/09/Screenshot-2022-09-29-at-13.06.00.png 600w, https://mevtho.com/content/images/size/w1000/2022/09/Screenshot-2022-09-29-at-13.06.00.png 1000w, https://mevtho.com/content/images/2022/09/Screenshot-2022-09-29-at-13.06.00.png 1024w" sizes="(min-width: 720px) 720px"><figcaption>The browser developer tools shows all the calls between your browser and the internet</figcaption></figure><p>&#x2003;Then head to the html and xhr tabs looking for the one that sends you the data you need. The developer tools allow you to preview the content response of each request. Often, you can expect it to be an XHR call when you see a small delay between when the page gets displayed on your screen and when the data gets rendered.</p><h3 id="reproducing-the-call">Reproducing the call</h3><p>&#x2003;Once you know which call gets you the data you want, the first step is to be able to reproduce it manually, outside of the web browsing environment. For that, I use postman (<a href="https://www.postman.com">https://www.postman.com</a>) that helps test requests. </p><p>&#x2003;I won&apos;t get into much details here on how to reproduce the call as it is very dependent on the website. The main thing is that it is important to be able to reproduce the call outside of the web browser so that the request is as standalone as possible. The browser may set headers / cookies / ... automatically that you want to identify and reproduce later on.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">If the request is too complicated to reproduce, depends on some complicated authentication process, CSRF checks, cookies, ... for example, you can look into browser automation with tools like Selenium, PhantomJS, ... that allows reproducing what the website does. If that&apos;s not enough, tools like <a href="https://scrapingbee.com">ScrappingBee</a> offers a way to do it in a more efficient and reliable way (but are not free). Once you get the response, you can follow the next steps below.</div></div><p>&#x2003;The good thing with Postman is that once you get the call working, it is very easy to extract a working code from it. Click on the &lt;/&gt; button on the sidebar to view a code you can reuse. I usually use &quot;PHP - curl&quot; as I am familiar with it, it works and is independent from any package.</p><p>&#x2003;The code looks more or less like that depending on what&apos;s needed to retrieve the page.</p><figure class="kg-card kg-code-card"><pre><code class="language-php">$curl = curl_init();

curl_setopt_array($curl, array(
  CURLOPT_URL =&gt; &apos;&lt;THE URL&gt;&apos;,
  CURLOPT_RETURNTRANSFER =&gt; true,
  CURLOPT_ENCODING =&gt; &apos;&apos;,
  CURLOPT_MAXREDIRS =&gt; 10,
  CURLOPT_TIMEOUT =&gt; 0,
  CURLOPT_FOLLOWLOCATION =&gt; true,
  CURLOPT_HTTP_VERSION =&gt; CURL_HTTP_VERSION_1_1,
  CURLOPT_CUSTOMREQUEST =&gt; &apos;GET&apos;,
));

$response = curl_exec($curl);

curl_close($curl);
echo $response;</code></pre><figcaption>Postman gives a working code serving as the base of the script</figcaption></figure><h3 id="setting-up-a-caching-strategy">Setting up a caching strategy</h3><p>&#x2003;Once I get that code, the first thing I do is set up some caching, I want to be respectful of the website as much as possible. </p><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Depending on what you do, you may be hitting it with a load of requests in a very short time, please be considerate. Caching doesn&apos;t need much.</div></div><p>&#x2003;Here is what I use, it is not perfect, but it works and ensure I am not asking for too much from the server, especially when testing the building of the list.</p><pre><code class="language-php">&lt;?php

// Extracting the URL from the curl array
$url = &quot;&lt;THE_URL&gt;&quot;;

// Building a file to store the response content from the URL name, here also using the date if I am expecting daily changes to the data.
$file = &quot;cache/&quot; . md5(date(&quot;Ymd&quot;) . $url). &quot;.json&quot;;

// Then it is simply a matter of getting the content from the file 
// instead of calling the server if the file exists.
if (file_exists($file)) {
    $response = file_get_contents($file);
} else {
    $response = // Postman code to call server using $url
    
    // We create the file with the response we got from the server
    file_put_contents($file, $response);
}</code></pre><h2 id="getting-what-you-need">Getting what you need</h2><h3></h3><h3 id="from-a-json-response">From a json response</h3><p>&#x2003;That is the ideal scenario, the server gives you something you can directly interact with in a convenient way.</p><pre><code class="language-php">$content = json_decode($response);</code></pre><p>&#x2003;And you are ready to interact with the data as you would with a normal php object.</p><h3 id="from-html">From html</h3><p>&#x2003;That is usually more annoying. The main reason being that the html contains a lot of of useless elements about the rendering of the page. The response was made to be processed by a web browser, not by your code. You&apos;ll have to look into making things work by inspecting and interacting with the DOM to build your own objects containing the data.</p><p>&#x2003;You may get issues with namespace. I managed to work around those in the past with :</p><pre><code class="language-php">function applyNamespace($expression)
{
    return join(&apos;/ns:&apos;, explode(&quot;/&quot;, $expression));
}</code></pre><p>Wrapping each xpath query (that can be obtained through the developer tools) into a call to applyNamespace</p><pre><code class="language-php">$doc = new DOMDocument();
$doc-&gt;loadHTML($response);

$xpath = new \DOMXpath($dom);
$ns = $dom-&gt;documentElement-&gt;namespaceURI;
$xpath-&gt;registerNamespace(&quot;ns&quot;, $ns);
        
$element = $xpath-&gt;query($this-&gt;applyNamespace(&quot;&lt;SOME_XPATH_QUERY&gt;&quot;)); </code></pre><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><strong>Tip: </strong>Add <code>libxml_use_internal_errors(true);</code> to the top of your script to avoid random warnings when loading the HTML.&#xA0;</div></div><h2 id="retrieving-multiple-pages">Retrieving multiple pages</h2><p>&#x2003;One of the main reasons I build the list pages for myself is to not have to have to go through multiple pages manually to find the right one. So, in most cases, I need to also automate that pagination process. It is easy enough.</p><p>&#x2003;First step is to identify how to pagination is done on the website.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text"><strong>Tip: </strong>When doing the step of reproducing the call above in postman, do it on page 2. It is very likely that once you have identified how the second page gets retrieved, getting the first one becomes a matter of replacing the values with 0 or 1.</div></div><p>&#x2003;Then move your calls into a loop. </p><div class="kg-card kg-callout-card kg-callout-card-red"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Same as with caching, be considerate of the server, if you need to retrieve more than a couple of pages, add some backoff when retrieve the data. A simple <code>sleep(random_int(2, 12))</code> before <code>curl_exec</code> above won&apos;t slow you down much but would go a long way ensuring the server can stay available while also possible helping avoiding some detection mechanism that would block your access.</div></div><p>&#x2003;For the loop, you&apos;ll need an ending condition. There are multiple ways of handling it depending on what you want. Typically one of those :</p><ul><li>End after a specific number of records or pages have been returned</li><li>End when the data returned doesn&apos;t match what you want anymore</li><li>End when a 404 Not found error code has been received</li><li>End when there is no more record to get</li></ul><p>&#x2003;Most of the time I go with the last one as it is the most reliable one that gives all of the data. The first one is great if there are way too many records, and you can sort the data returned to what you need.</p><p>&#x2003;It usually gives something along the lines of :</p><pre><code class="language-php">$allRecords = [];
do {
    $url = &quot;&lt;SOME_URL&gt;?page=$page&quot;;
    
    // Retrieve the data for $url as above (with the caching)
    
    // Whatever is needed to get the records into $recordsForCall
    $recordsForCall = [];
    
    $allRecords = array_merge($allRecords, $recordsForCall);
} while($recordsForCall &gt; 0);</code></pre><h2 id="viewing-the-list">Viewing the list</h2><p>&#x2003;At this stage, most of the work is done, all you need is a way to display that list. I use 2 different ways depending on what I want to do with it :</p><ul><li>Display as html in the browser, if only to see the listing or if there isn&apos;t that much content</li><li>Export to CSV when I need to play with the data such as having some filtering or there are a lot of records / datapoints that make it not convenient to show in the browser.</li></ul><h3 id="as-html">As html</h3><p>Goal being to just dump the data, It is usually quite raw like</p><pre><code class="language-html">&lt;html&gt;
&lt;head&gt;&lt;/head&gt;
&lt;body&gt;
&lt;table&gt;
    &lt;thead&gt;
    &lt;tr&gt;
        &lt;th&gt;Column 1&lt;/th&gt;
        &lt;th&gt;Column 2&lt;/th&gt;
        &lt;th&gt;Column 3&lt;/th&gt;
    &lt;/tr&gt;
    &lt;/thead&gt;
    &lt;tbody&gt;
    &lt;?php
        foreach ($rows as $row) {
            sprintf(
                &quot;&lt;tr&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;td&gt;%s&lt;/td&gt;&lt;/tr&gt;&quot;,
                $row-&gt;column1, 
                $row-&gt;column2, 
                $row-&gt;column3
            ); 
        }
    ?&gt;
    &lt;/tbody&gt;
&lt;/table&gt;
&lt;/body&gt;
&lt;/html&gt;</code></pre><p>&#x2003;Then, put on an accessible web server, I just open the page in the browser t access the list.</p><h3 id="as-spreadsheet">As spreadsheet</h3><p>The easiest is to directly build a file from PHP, in the simplest case where you can assume all record have the same data, for example :</p><figure class="kg-card kg-code-card"><pre><code class="language-php">$output = &quot;some_file.csv&quot;;
$columns = array_keys((array)$rows[0] ?? [])

// Open stream
$o = fopen($output, &quot;w&quot;);

// Add header line
fputcsv($o, $columns);
foreach ($rows as $row) {
    // Add row
    fputcsv($o, lineCsv((array)$row));
}

// Close stream
fclose($o);</code></pre><figcaption>You can adjust $column to match what you want</figcaption></figure><p>Where <code>lineCsv</code> is </p><figure class="kg-card kg-code-card"><pre><code class="language-php">function lineCsv($row, $columns)
{
    $return = [];
    foreach ($columns as $column) {
        $return[$column] = $row[$column] ?? &quot;&quot;;
    }
    return $return;
}</code></pre><figcaption>lineCSV ensures that the data always come in the same order provided $columns stays constant</figcaption></figure><p>I then run the script using <code>php -f script.php</code> that generate a file that can be opened in any spreadsheet software afterwards.</p><h2 id="going-further">Going further</h2><p>&#x2003;Using this techniques, it can be easy for you to add your own filters to the code to match exactly what you want, build your own aggregator from multiple website. Those steps are intended to give you an idea on how to proceed. It won&apos;t be a one size fits all. If you need something reliable, maybe build something using tools like scrapingbee that would offer both reliability on retrieving the data, but also a common interface you can build on.</p><div class="kg-card kg-callout-card kg-callout-card-blue"><div class="kg-callout-emoji">&#x1F4A1;</div><div class="kg-callout-text">Using PHP and Laravel to write your script / code, there is now a faster way. You can copy the curl code from the developer tools and then use this tool : <a href="https://laravelshift.com/convert-curl-to-http">https://laravelshift.com/convert-curl-to-http</a> to convert the command line to a code directly usable in Laravel.</div></div>]]></content:encoded></item><item><title><![CDATA[Quickly add a favicon to your website]]></title><description><![CDATA[Make your website memorable with a dedicated icon.]]></description><link>https://mevtho.com/favicon/</link><guid isPermaLink="false">62960903a509b79113535cb6</guid><category><![CDATA[How-To]]></category><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Tue, 31 May 2022 12:41:24 GMT</pubDate><content:encoded><![CDATA[<p>	Favicons... in my opinion, it is something that often gets overlooked but is an easy and quick way to make your website stand out and be more memorable.</p><p>	What are they ? It is the icon that represents your website. It shows up in your browser&apos;s tab bar, helping users to quickly locate the tab you want to get to.</p><p> &#xA0; &#xA0;They can also show on iphone bookmark shortcuts on your users home screen.</p><p>	Here are 2 ways to set an icon for your website. </p><p><strong>	1 - Favicon.ico</strong></p><p>	That&apos;s probably the easiest way, assuming you have a .ico file available. By default, browsers will try to access a file at the root of your domain named favicon.ico. If it exists, that will be your icon.</p><p><strong>	2- Page tags</strong></p><p>	That&apos;s where my preference is. Using tags allow for more flexibility in terms of files and options.</p><p>	To achieve that, my preference is to use the website <a href="https://favicon.io/">https://favicon.io/</a>, a favicon generator. You first select the file that you want to use. It also offer generating the icon from an emoticon (as below). From that file, it is going to generate an archive file that contains your file in various sizes and format to add to your website.</p><p>	The second step is to add the code in the header of your page or template.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://mevtho.com/content/images/2022/05/IMG_3DDA20253E99-1-3.jpeg" class="kg-image" alt loading="lazy" width="100" height="84"><figcaption>The Smooth Commute icon</figcaption></figure><p></p><p>	You can also play with favicons, changing it based on events happening on your website. A notification, message to your user could trigger updating to alert the user of an update.</p>]]></content:encoded></item><item><title><![CDATA[Podyt]]></title><description><![CDATA[Because all youtube videos aren't worth watching, I send them straight to my podcast player.]]></description><link>https://mevtho.com/podyt/</link><guid isPermaLink="false">62943fefa509b79113535b53</guid><category><![CDATA[Freeing Code]]></category><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Tue, 31 May 2022 03:30:18 GMT</pubDate><media:content url="https://mevtho.com/content/images/2022/05/github-cover.png" medium="image"/><content:encoded><![CDATA[<img src="https://mevtho.com/content/images/2022/05/github-cover.png" alt="Podyt"><p>==&gt; <a href="https://github.com/mevtho/podyt">https://github.com/mevtho/podyt</a></p><p>	Last released is a full SaaS style application. Written with Laravel / PHP. </p><p>	This was inspired by having to &quot;watch&quot; youtube videos that really didn&apos;t have anything visual to them. To make it short, youtube videos that had no point being watched.</p><p>	Being an avid podcast listener, I quickly realised it would be nice to be able to have those videos as a podcast delivered straight to my phone, instead to have to deal with youtube hassle.</p><p>	The way it works is by being able to follow youtube playlists and create an rss feed that can be added to any podcast player. Any new video added to the playlist gets added to the feed.</p><p>	The app allows managing multiple feeds with multiple and is still missing basic features like deleting / managing the feed itself, but, it wasn&apos;t really needed. I manage the feed data from the podcast app.</p>]]></content:encoded></item><item><title><![CDATA[HK Commute]]></title><description><![CDATA[One-tap quick glance at the daily commute schedule]]></description><link>https://mevtho.com/hk-commute/</link><guid isPermaLink="false">62944008a509b79113535b57</guid><category><![CDATA[Freeing Code]]></category><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Mon, 30 May 2022 11:02:18 GMT</pubDate><media:content url="https://mevtho.com/content/images/2022/05/jeremy-bezanger-KvCmgqqkbro-unsplash.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://mevtho.com/content/images/2022/05/jeremy-bezanger-KvCmgqqkbro-unsplash.jpg" alt="HK Commute"><p><a href="https://github.com/mevtho/commutehk/">https://github.com/mevtho/commutehk/</a></p><p>	Commuting ... For the past few months, one of my main activities has been to check when is the next / best bus to commute with. </p><p>	The frequency is quite good as several routes work. The bus stop is right below the building. Reaching it takes 2 to 3 minutes.</p><p>	However, it is nice to know when a bus comes. It is easier to adjust the timing and know if it is necessary to rush or not. </p><p>	For a long time, I have been using the default application provided by the Hong-Kong transportation system. It works, kind of, it gives the next buses around, all the routes, and only the next buses. In short, finding the relevant information involves a lot of scrolling, clicking, ... through a non-intuitive interface. To add to the matter, it displays some ads and force a click to access the list. Small annoyances, but, over time, they all add up...</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://mevtho.com/content/images/2022/05/IMG_B60AF4FAE5F4-1-1.jpeg" class="kg-image" alt="HK Commute" loading="lazy" width="277" height="600"><figcaption>The app I meant to replace</figcaption></figure><p>	Couldn&apos;t I use google or any other, ... possibly, but, I am not convinced nor found it more useful to be honest. I still prefer that initial app.</p><p>	This changed when I learnt about the Open Data coming from the Hong Kong government (<a href="https://data.gov.hk/en/">https://data.gov.hk/en/</a>). It is possible to get the same data, the ETA of buses and metro at a specific stop through an API.</p><p>	So of course I wrote my own web page that focuses only on my route. It gathers all the information I need in seconds. With as little effort as possible I can determine which bus I should target. It also gives me more information. Despite knowing that the predicted time down to the second won&apos;t be accurate. I find it useful to know if it is expected at the beginning of the minute or at the end of it. 8:45:03 is quite different to 8:45:54 in the current context.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://mevtho.com/content/images/2022/05/Screenshot-2022-05-30-at-18.46.58-2.png" class="kg-image" alt="HK Commute" loading="lazy" width="353" height="602"><figcaption>The mobile view interface</figcaption></figure><p>	The great thing is, now. I can glance at all the information I need in a matter of seconds. </p><p>	In 2 or 3 taps, I get access to the one way or the other as well as either the time of the bus or how long to wait. No need to do the calculation anymore ...</p><p>	A bookmark to the page created later, and with a single tap on the screen I am able to get what I need.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://mevtho.com/content/images/2022/05/IMG_3DDA20253E99-1-1.jpeg" class="kg-image" alt="HK Commute" loading="lazy" width="100" height="84"><figcaption>A shortcut for quick access</figcaption></figure><p>	Important note, if you are looking to make a bookmark shortcut, make sure to create an icon for your page,especially if it takes some time to load. By default, on iphone, a screenshot of the page will be displayed. If for any reason your page doesn&apos;t show anything within the expected time frame, you&apos;ll end up with a blank square... If your page often changes, the display will also often change... Make it more static with a dedicated icon. It&apos;s easy ... </p><p>	Now, the display is still not the best. After few days of usage, I realized that I didn&apos;t need to have the data by bus line... All I need is to know when is the next bus that I can realistically use... A one column display, gathering all the data as one line per incoming bus would be much more efficient. Well, that&apos;s next on the list. When it becomes too annoying as is.</p>]]></content:encoded></item><item><title><![CDATA[Freeing my code]]></title><description><![CDATA[Most of my code sits on hard drives, either unused or used in a very limited capacity. I am freeing what I can for all to consume.]]></description><link>https://mevtho.com/freeing-my-code/</link><guid isPermaLink="false">62943fe1a509b79113535b4f</guid><category><![CDATA[Freeing Code]]></category><dc:creator><![CDATA[Thomas]]></dc:creator><pubDate>Mon, 30 May 2022 07:50:00 GMT</pubDate><media:content url="https://mevtho.com/content/images/2022/05/source-g87fbb59eb_640.jpg" medium="image"/><content:encoded><![CDATA[<img src="https://mevtho.com/content/images/2022/05/source-g87fbb59eb_640.jpg" alt="Freeing my code"><p>For most of the 2000s&apos; until today, I have written code almost everyday in various capacity, first as a hobby, then part of my education to become a software engineer and then professionally. Most of that code is currently sitting on hard drives, not being used in any form.</p><p>I have decided to slowly release most of this, making it available to anyone to view or use. Some with the story behind, the context and maybe some tutorial / explanations, some without.</p><p>While I make it available, I don&apos;t intend to maintain or support any of this. So, the code will be provided as-is, maybe with few clean-ups and enhancements as I see fit, but not much more. Feel free to use it, reference it, learn from my mistakes, ... Hopefully that can be useful to some of you.</p><p>Note that most of the code will be old and will be using syntax techniques and ideas that are outdated or wouldn&apos;t be suitable today. Also, it worked in the context it was used in. For example, don&apos;t expect a piece of code I wrote for some personal automation to be usable in a widely available production environment without doing your own testing and due diligence.</p>]]></content:encoded></item></channel></rss>