<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title>blog.dotcs.me</title>
  <link href="https://blog.dotcs.me/feeds/index.xml" rel="self" type="application/atom+xml"/>
  <link href="https://blog.dotcs.me/" rel="alternate" type="text/html"/>
  <updated>2024-12-18T18:34:21.301Z</updated>
  <author>
    <name>dotcs</name>
  </author>
  <id>https://blog.dotcs.me/feeds/index.xml</id>
  <entry>
    <title>Blending Databases with FastAPI and SQLAlchemy</title>
    <published>2024-12-18T18:00:00Z</published>
    <updated>2024-12-18T18:00:00Z</updated>
    <id>https://blog.dotcs.me/posts/fastapi-sqlalchemy-blend-databases</id>
    <link href="https://blog.dotcs.me/posts/fastapi-sqlalchemy-blend-databases"/>
    <summary>This post will show you how to blend multiple databases in a FastAPI application using SQLAlchemy.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/fastapi-sqlalchemy-blend-databases">&lt;p&gt;In this post, I will show how to blend multiple databases in a FastAPI application using SQLAlchemy.
This is a common use case when multiple databases are being used and one wants to interact with them transparently through a single database session.&lt;/p&gt;
&lt;p&gt;An example where this could make sense is having a common database for user data and separate databases with sensitive, maybe immutable, customer data that should not be mixed and where access should be restricted based on the tenant.&lt;/p&gt;
&lt;h2 id="introduction"&gt;&lt;a class="markdownIt-Anchor" href="#introduction"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Introduction&lt;/h2&gt;
&lt;p&gt;FastAPI is a modern web framework for building APIs with Python 3.6+ based on standard Python type hints.
SQLAlchemy is a SQL toolkit and Object-Relational Mapping (ORM) library for Python.&lt;/p&gt;
&lt;p&gt;The goal is to create a FastAPI application that connects to two databases: a SQLite database and another SQLite database.
But the same principles can be applied to any other database supported by SQLAlchemy.
SQLAlchemy will be used to interact with the databases and FastAPI is used to expose the data through an API.
Two base classes will be created to handle the database sessions and models.
The FastAPI application will be created with two endpoints to query the data from each database.&lt;/p&gt;
&lt;h2 id="code"&gt;&lt;a class="markdownIt-Anchor" href="#code"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Code&lt;/h2&gt;
&lt;p&gt;You can find the complete code for this post in my &lt;a href="https://github.com/dotcs/fastapi-blend-db-demo"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The magic happens in the &lt;code&gt;VirtualSession&lt;/code&gt; class, which configures two database sessions, which are created through FastAPI's &lt;code&gt;sessionmaker&lt;/code&gt; function.
We mimic the behavior of a single session by creating a virtual session that can be used to interact with both databases.
By proxying the session methods, such as &lt;a href="https://github.com/dotcs/fastapi-blend-db-demo/blob/a1570b05c6ce1fd10c7e1d485ad798e5a77bc209/fastapi_blend_db/app.py#L77-L84"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;&lt;code&gt;query&lt;/code&gt;&lt;/a&gt; or &lt;a href="https://github.com/dotcs/fastapi-blend-db-demo/blob/a1570b05c6ce1fd10c7e1d485ad798e5a77bc209/fastapi_blend_db/app.py#L86-L92"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;&lt;code&gt;add&lt;/code&gt;&lt;/a&gt;, we can interact with both databases transparently.&lt;/p&gt;
&lt;p&gt;By making use of FastAPI's dependency injection system, we can inject the virtual session into our endpoints and interact with both databases through a single session.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://github.com/dotcs/fastapi-blend-db-demo/blob/main/fastapi_blend_db/tests/test_app.py"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;Integration tests&lt;/a&gt; are included to demonstrate how to interact with the databases through the session using the FastAPI test client.&lt;/p&gt;
&lt;h2 id="conclusion"&gt;&lt;a class="markdownIt-Anchor" href="#conclusion"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, we have seen how to blend multiple databases in a FastAPI application using SQLAlchemy.
This is a powerful feature that allows you to interact with multiple databases through a single session transparently.
It is flexible and can be extended to support more databases or different database types by adding more database sessions and models.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="fastapi"/>
    <category term="python"/>
    <category term="sqlalchemy"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Combining FastAPI Dependency Injection with Service and Repository Layers</title>
    <published>2024-11-11T21:50:16+01:00</published>
    <updated>2024-11-11T21:50:16+01:00</updated>
    <id>https://blog.dotcs.me/posts/fastapi-dependency-injection-x-layers</id>
    <link href="https://blog.dotcs.me/posts/fastapi-dependency-injection-x-layers"/>
    <summary>Learn how to integrate FastAPI's DI system across multiple layers of your application to build a robust and testable application.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/fastapi-dependency-injection-x-layers">&lt;p&gt;FastAPI has a wonderful Dependency Injection (DI) system that works reasonably well at the controller level.
However, to build a robust and testable application, it is helpful to make use of a dependency injection chain that spans multiple layers of the application.
This approach not only promotes a clean architecture but also significantly improves the test situation.&lt;/p&gt;
&lt;h2 id="starting-point"&gt;&lt;a class="markdownIt-Anchor" href="#starting-point"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Starting Point&lt;/h2&gt;
&lt;p&gt;Typically, a FastAPI application consists of a controller layer (the FastAPI endpoints), a service layer and often a repository layer.
While FastAPI provides a way to inject dependencies into the controller layer, it does not provide a way to inject dependencies into the service or repository layers.&lt;/p&gt;
&lt;p&gt;But this does not mean that we cannot use dependency injection across multiple layers.
By building a dependency injection chain that spans multiple layers, we can inject dependencies into the service and repository layers as well.&lt;/p&gt;
&lt;p&gt;Let's see how we can do this and get our hands dirty with some code.&lt;/p&gt;
&lt;p&gt;A typical application might look like this:&lt;/p&gt;
&lt;div class="mermaid"&gt;graph LR
    A[Controller] --&amp;gt; B[Service]
    B --&amp;gt; C[Repository]
    C --&amp;gt; D[Database]
&lt;/div&gt;&lt;p&gt;But often we have application settings that might be used across the application in all layers, which let the application look like this:&lt;/p&gt;
&lt;div class="mermaid"&gt;graph TD
subgraph Z[&amp;quot; &amp;quot;]
direction LR
    A[Controller] --&amp;gt; B[Service]
    B --&amp;gt; C[Repository]
    C --&amp;gt; D[Database]
end
    E[Settings]
    E --&amp;gt; A
    E --&amp;gt; B
    E --&amp;gt; C
&lt;/div&gt;&lt;p&gt;Sometimes we also have a logger that we want to inject into all layers:&lt;/p&gt;
&lt;div class="mermaid"&gt;graph TD
subgraph Z[&amp;quot; &amp;quot;]
direction LR
    A[Controller] --&amp;gt; B[Service]
    B --&amp;gt; C[Repository]
    C --&amp;gt; D[Database]
end
E[Settings]
E --&amp;gt; A
E --&amp;gt; B
E --&amp;gt; C
F[Logger]
F --&amp;gt; A
F --&amp;gt; B
F --&amp;gt; C
&lt;/div&gt;&lt;p&gt;And many more dependencies might be possible.
Some of them could be shared across all layers, some of them might be shared only between some layers, and some of them might be specific to a single layer.&lt;/p&gt;
&lt;h2 id="building-the-dependency-injection-chain"&gt;&lt;a class="markdownIt-Anchor" href="#building-the-dependency-injection-chain"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Building the Dependency Injection Chain&lt;/h2&gt;
&lt;p&gt;To build a dependency injection chain that spans multiple layers, we can use Python's &lt;code&gt;Annotated&lt;/code&gt; type and the &lt;code&gt;Depends&lt;/code&gt; class from FastAPI.&lt;/p&gt;
&lt;p&gt;Let's start by defining a &lt;code&gt;Settings&lt;/code&gt; class that holds our application settings by inheriting from Pydantic &lt;code&gt;BaseSettings&lt;/code&gt; class.
This helps to ensure that the settings are correctly typed and validated and can be easily injected into other classes.
&lt;em&gt;(Also it helps to load the settings from environment variables or configuration files, which is typically why I like to use it in my applications.)&lt;/em&gt; 😉&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# file: settings.py&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; pydantic &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; BaseSettings

&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;Settings&lt;/span&gt;(&lt;span class="hljs-params"&gt;BaseSettings&lt;/span&gt;):&lt;/span&gt;
    app_name: &lt;span class="hljs-built_in"&gt;str&lt;/span&gt; = &lt;span class="hljs-string"&gt;&amp;quot;My App&amp;quot;&lt;/span&gt;
    db_connection_string: &lt;span class="hljs-built_in"&gt;str&lt;/span&gt; = &lt;span class="hljs-string"&gt;&amp;quot;sqlite:///:memory:&amp;quot;&lt;/span&gt;
    debug: &lt;span class="hljs-built_in"&gt;bool&lt;/span&gt; = &lt;span class="hljs-literal"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next, let us define the three layers of our application: the controller, service, and repository layers.&lt;/p&gt;
&lt;p&gt;The repository layer is responsible for fetching data from the database.
In this example, we will keep it simple and return some sample data.
In a real-world application, this would be a database query, often by using an ORM like SQLAlchemy.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# file: repository.py&lt;/span&gt;
&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;Repository&lt;/span&gt;:&lt;/span&gt;
    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;__init__&lt;/span&gt;(&lt;span class="hljs-params"&gt;self, settings: Settings, db: Database&lt;/span&gt;):&lt;/span&gt;
        self.settings = settings
        self.db = db   &lt;span class="hljs-comment"&gt;# just to show that we can inject multiple dependencies&lt;/span&gt;

    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_sample_data&lt;/span&gt;(&lt;span class="hljs-params"&gt;self&lt;/span&gt;):&lt;/span&gt;
        &lt;span class="hljs-comment"&gt;# usually this would be a database query&lt;/span&gt;
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; [{&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Alice&amp;quot;&lt;/span&gt;}, {&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Bob&amp;quot;&lt;/span&gt;}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that we have not used FastAPI's &lt;code&gt;Depends&lt;/code&gt; here.
This is because the repository layer is not controlled by FastAPI.
Instead we will inject the &lt;code&gt;Settings&lt;/code&gt; and &lt;code&gt;Database&lt;/code&gt; objects into the repository layer by using the normal dependency injection pattern, meaning we will pass them as arguments to the constructor.&lt;/p&gt;
&lt;p&gt;The service layer is responsible for business logic.
In this example, we will keep it simple and return the application name in uppercase.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# file: service.py&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; fastapi &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Depends

&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;Service&lt;/span&gt;:&lt;/span&gt;
    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;__init__&lt;/span&gt;(&lt;span class="hljs-params"&gt;self, settings: Settings, repository: Repository&lt;/span&gt;):&lt;/span&gt;
        self.settings = settings
        self.repository = repository

    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;app_name_upper&lt;/span&gt;(&lt;span class="hljs-params"&gt;self&lt;/span&gt;):&lt;/span&gt;
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; self.settings.app_name.upper()

    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_sample_data_with_upper_names&lt;/span&gt;(&lt;span class="hljs-params"&gt;self&lt;/span&gt;):&lt;/span&gt;
        data = self.repository.get_sample_data()
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; [{&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: item[&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;].upper()} &lt;span class="hljs-keyword"&gt;for&lt;/span&gt; item &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; data]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Before we define the controller layer, let us write the dependency injection functions which FastAPI will use to inject the dependencies into the service and repository layers.
These functions are needed by the FastAPI dependency injection system to resolve the dependencies.
Their purpose is to instantiate the objects.
&lt;a href="https://fastapi.tiangolo.com/tutorial/dependencies/#simple-and-powerful"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;FastAPI allows to let dependencies depend on other dependencies&lt;/a&gt;, which allows to build a dependency chain.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# file: dependencies.py&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; typing &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Annotated

&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.settings &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Settings
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.database &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Database
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.repository &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Repository

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_settings&lt;/span&gt;():&lt;/span&gt;
    &lt;span class="hljs-string"&gt;&amp;quot;&amp;quot;&amp;quot;Returns the application settings.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; Settings()

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_db&lt;/span&gt;(&lt;span class="hljs-params"&gt;settings: Annotated[Settings, Depends(&lt;span class="hljs-params"&gt;get_settings&lt;/span&gt;)]&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-string"&gt;&amp;quot;&amp;quot;&amp;quot;Returns the database connection.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; Database(connection_string=settings.db_connection_string).connect()

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_repository&lt;/span&gt;(&lt;span class="hljs-params"&gt;settings: Annotated[Settings, Depends(&lt;span class="hljs-params"&gt;get_settings&lt;/span&gt;)], db: Annotated[Database, Depends(&lt;span class="hljs-params"&gt;get_db&lt;/span&gt;)]&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; Repository(settings, db)

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_service&lt;/span&gt;(&lt;span class="hljs-params"&gt;settings: Annotated[Settings, Depends(&lt;span class="hljs-params"&gt;get_settings&lt;/span&gt;), repository: Annotated[Repository, Depends(&lt;span class="hljs-params"&gt;get_repository&lt;/span&gt;)]&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-string"&gt;&amp;quot;&amp;quot;&amp;quot;Returns the service layer.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; Service(settings, repository)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally, we define the controller layer, which consists of the FastAPI endpoints.
We inject the &lt;code&gt;Service&lt;/code&gt; class into the controller layer using the &lt;code&gt;Depends&lt;/code&gt; class from FastAPI.&lt;/p&gt;
&lt;p&gt;Note that we do not need to inject the &lt;code&gt;Repository&lt;/code&gt; class into the controller layer because it is already injected into the &lt;code&gt;Service&lt;/code&gt; class, which is then injected into the controller layer.
Also there is no need to retrieve and pass the database connection to the service, because the repository is responsible for that.&lt;/p&gt;
&lt;p&gt;FastAPI will automatically resolve the dependency chain and resolve the dependencies in the correct order.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# app.py&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; fastapi &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; FastAPI
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; typing &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Annotated

&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.dependencies &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; get_service

app = FastAPI()

&lt;span class="hljs-meta"&gt;@app.get(&amp;quot;/&amp;quot;)&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;read_root&lt;/span&gt;(&lt;span class="hljs-params"&gt;service: Annotated[Service, Depends(&lt;span class="hljs-params"&gt;get_service&lt;/span&gt;)]&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; {&lt;span class="hljs-string"&gt;&amp;quot;app_name&amp;quot;&lt;/span&gt;: service.app_name_upper()}

&lt;span class="hljs-meta"&gt;@app.get(&amp;quot;/sample_data&amp;quot;)&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;read_sample_data&lt;/span&gt;(&lt;span class="hljs-params"&gt;service: Annotated[Service, Depends(&lt;span class="hljs-params"&gt;get_service&lt;/span&gt;)]&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; service.get_sample_data_with_upper_names()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Note that from the controller layer, we only inject the &lt;code&gt;Service&lt;/code&gt; class.
If needed, we could also inject the &lt;code&gt;Settings&lt;/code&gt; class or any other dependencies that are meant to be used in all layers.
But there is no need to inject the &lt;code&gt;Repository&lt;/code&gt; class or the &lt;code&gt;Database&lt;/code&gt; class into the controller layer!&lt;/p&gt;
&lt;h2 id="testing"&gt;&lt;a class="markdownIt-Anchor" href="#testing"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Testing&lt;/h2&gt;
&lt;p&gt;This significantly simplifies testing.&lt;/p&gt;
&lt;p&gt;For example, to test the &lt;code&gt;Service&lt;/code&gt; class, we can easily mock the &lt;code&gt;Repository&lt;/code&gt; class and the &lt;code&gt;Settings&lt;/code&gt; class.
This is generally possible because of the dependency injection pattern we have used for initializing the &lt;code&gt;Service&lt;/code&gt; class.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# test_service.py&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.service &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Service
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.repository &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Repository
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.settings &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Settings

&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;MockSettings&lt;/span&gt;(&lt;span class="hljs-params"&gt;Settings&lt;/span&gt;):&lt;/span&gt;
    app_name = &lt;span class="hljs-string"&gt;&amp;quot;My App under test&amp;quot;&lt;/span&gt;

&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;MockRepository&lt;/span&gt;(&lt;span class="hljs-params"&gt;Repository&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_sample_data&lt;/span&gt;(&lt;span class="hljs-params"&gt;self&lt;/span&gt;):&lt;/span&gt;
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; [{&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Foo&amp;quot;&lt;/span&gt;}, {&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Bar&amp;quot;&lt;/span&gt;}]

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;test_app_name_upper&lt;/span&gt;():&lt;/span&gt;
    service = Service(MockSettings(), MockRepository())
    &lt;span class="hljs-keyword"&gt;assert&lt;/span&gt; service.app_name_upper() == &lt;span class="hljs-string"&gt;&amp;quot;MY APP UNDER TEST&amp;quot;&lt;/span&gt;

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;test_get_sample_data_with_upper_names&lt;/span&gt;():&lt;/span&gt;
    service = Service(MockSettings(), MockRepository())
    &lt;span class="hljs-keyword"&gt;assert&lt;/span&gt; service.get_sample_data_with_upper_names() == [{&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;FOO&amp;quot;&lt;/span&gt;}, {&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;BAR&amp;quot;&lt;/span&gt;}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;But the real power of this approach becomes apparent when testing the controller layer.&lt;/p&gt;
&lt;p&gt;Because we have used the &lt;code&gt;Depends&lt;/code&gt; class from FastAPI to inject the &lt;code&gt;Service&lt;/code&gt; class into the controller layer and have configured everything as a dependency chain, we can easily override the dependency injection functions to inject mock objects at any level.&lt;/p&gt;
&lt;p&gt;The following example shows how to override the &lt;code&gt;get_repository&lt;/code&gt; function to inject a &lt;code&gt;MockRepository&lt;/code&gt; object into the controller layer.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# test_controller.py&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; fastapi.testclient &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; TestClient

&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.app &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; app
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.repository &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Repository
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; myapp.dependencies &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; get_settings, get_repository

&lt;span class="hljs-class"&gt;&lt;span class="hljs-keyword"&gt;class&lt;/span&gt; &lt;span class="hljs-title"&gt;MockRepository&lt;/span&gt;(&lt;span class="hljs-params"&gt;Repository&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;get_sample_data&lt;/span&gt;(&lt;span class="hljs-params"&gt;self&lt;/span&gt;):&lt;/span&gt;
        &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; [{&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Foo&amp;quot;&lt;/span&gt;}, {&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Bar&amp;quot;&lt;/span&gt;}]

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;mock_get_repository&lt;/span&gt;(&lt;span class="hljs-params"&gt;settings: Annotations[Settings, Depends(&lt;span class="hljs-params"&gt;get_settings&lt;/span&gt;)]&lt;/span&gt;):&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Return a mock repository instead of the real one.&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Also, we don&amp;#x27;t need the database connection here, so we can pass None.&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;return&lt;/span&gt; MockRepository(settings, &lt;span class="hljs-literal"&gt;None&lt;/span&gt;)

&lt;span class="hljs-function"&gt;&lt;span class="hljs-keyword"&gt;def&lt;/span&gt; &lt;span class="hljs-title"&gt;test_read_root&lt;/span&gt;():&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Arrange&lt;/span&gt;
    &lt;span class="hljs-comment"&gt;# Override any dependencies that need to be mocked&lt;/span&gt;
    app.dependency_overrides[get_repository] = mock_get_repository

    client = TestClient(app)

    &lt;span class="hljs-comment"&gt;# Act&lt;/span&gt;
    response = client.get(&lt;span class="hljs-string"&gt;&amp;quot;/sample_data&amp;quot;&lt;/span&gt;)

    &lt;span class="hljs-comment"&gt;# Assert&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;assert&lt;/span&gt; response.status_code == &lt;span class="hljs-number"&gt;200&lt;/span&gt;
    &lt;span class="hljs-keyword"&gt;assert&lt;/span&gt; response.json() == [{&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;FOO&amp;quot;&lt;/span&gt;}, {&lt;span class="hljs-string"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;BAR&amp;quot;&lt;/span&gt;}]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This approach allows us to easily test the controller layer by injecting mock objects at any level of the dependency chain.&lt;/p&gt;
&lt;p&gt;Note that we only need to override the dependency injection functions that we want to mock.
This makes the tests more focused and easier to understand.
In this case the full application logic can be tested without the need to start a real database or to mock the database connection by just mocking the repository layer.&lt;/p&gt;
&lt;h2 id="vizualizing-the-dependency-injection-chain"&gt;&lt;a class="markdownIt-Anchor" href="#vizualizing-the-dependency-injection-chain"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Vizualizing the Dependency Injection Chain&lt;/h2&gt;
&lt;p&gt;If the application is built like this and gets more complex over time because of many dependencies, it can be helpful to visualize the dependency injection chain.
Unfortunately I was not able to find a tool that can automatically generate a diagram from the code, so I wrote a Python library that can do this and published it on PyPI as an open-source project.&lt;/p&gt;
&lt;p&gt;The library works by inspecting the application code and generating a graph in the DOT language, which can then be rendered to an image using Graphviz.
Alternatively the library provides to output mermaid diagrams, which can be used in markdown files, e.g., on GitHub.&lt;/p&gt;
&lt;p&gt;The library is called &lt;a href="https://pypi.org/project/fastapi-di-viz/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;fastapi-di-viz&lt;/a&gt; and can be installed into a project with &lt;code&gt;pip install fastapi-di-viz&lt;/code&gt;.
It provides a CLI tool that allows to inspect any FastAPI application.
See the &lt;a href="https://github.com/dotcs/fastapi-di-viz/"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;README&lt;/a&gt; for more details on how this works.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="architecture"/>
    <category term="dependency-injection"/>
    <category term="fastapi"/>
    <category term="python"/>
    <category term="tech"/>
    <category term="testing"/>
  </entry>
  <entry>
    <title>Crawler for German Bank Insitutes</title>
    <published>2023-01-23T20:33:00+01:00</published>
    <updated>2023-01-23T20:33:00+01:00</updated>
    <id>https://blog.dotcs.me/posts/crawl-german-bank-institutes</id>
    <link href="https://blog.dotcs.me/posts/crawl-german-bank-institutes"/>
    <summary>I wrote a crawler to extract core data from two popular German bank instituts, namely Sparkasse and Volksbank to support the bank.green initiative.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/crawl-german-bank-institutes">&lt;p&gt;Recently I stumbled across the &lt;a href="https://bank.green"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;bank.green&lt;/a&gt; project, which aims to add some transparency on how banks invest the money of their investors.
I really like the project idea since following the money typically seems to have a high chance of success.
I do not think I fully understood how they want to generate those insights for most bank institutes world-wide, but still it is worth trying in my opinion.&lt;/p&gt;
&lt;p&gt;A quick search showed that a lot German banks are missing in their list.
While I was able to find the &lt;a href="https://gls.de"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;GLS bank&lt;/a&gt;, the bank of my trust, I missed a lot other institues - especially the &lt;a href="https://sparkasse.de"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Sparkasse&lt;/a&gt; and &lt;a href="https://vr.de"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Volksbank&lt;/a&gt; institutes which are very popular in Germany.&lt;/p&gt;
&lt;p&gt;To help the project, I wanted to contribute the necessary core data for German bank institutes.
So I started writing a web-crawler based on &lt;a href="https://scrapy.org"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;scrapy&lt;/a&gt;, which crawls all the institutes and generates a rather large JSON file, with the extracted data.&lt;/p&gt;
&lt;p&gt;My crawling tests have shown that the structure of the Sparkasse is much easier to crawl than the Volksbank institutions.
It seems that Sparkasse institutes use a common website engine, while Volksbank take a freer approach and give their institutes more freedom in structuring their imprint pages.&lt;/p&gt;
&lt;p&gt;Nevertheless I found a way to cover the most relevant data and provide it to the &lt;a href="https://bank.green"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;bank.green&lt;/a&gt; project.&lt;/p&gt;
&lt;p&gt;The result looks like this for Sparkasse institutes&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;[
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.sparkasse-nienburg.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.sparkasse-nienburg.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Nienburg&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Nienburg\nAnstalt des \u00f6ffentlichen Rechts\nGoetheplatz 4\n31582\u00a0Nienburg&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;25650106&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;NOLADE21NIB&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HR A 21724 beim Amtsgericht Walsrode&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE116159984&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4950219690&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4950219696969&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@sparkasse-nienburg.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.sparkasse-vorderpfalz.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.sparkasse-vorderpfalz.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Vorderpfalz&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Vorderpfalz\nAnstalt des \u00f6ffentlichen Rechts\nLudwigstra\u00dfe 52\n67059\u00a0Ludwigshafen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;54550010&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;LUHSDE6AXXX&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HRA 3647 beim Amtsgericht Ludwigshafen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE149138080&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4962159920&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+496215992865992&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;kontakt@sparkasse-vorderpfalz.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.ksk-tuebingen.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.ksk-tuebingen.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Kreissparkasse T\u00fcbingen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Kreissparkasse T\u00fcbingen\nAnstalt des \u00f6ffentlichen Rechts\nM\u00fchlbach\u00e4ckerstra\u00dfe 2\n72072\u00a0T\u00fcbingen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;64150020&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;SOLADES1TUB&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HRA 381312 beim Registergericht Stuttgart&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE146889408&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4970712050&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+497071205105&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@ksk-tuebingen.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.kreissparkasse-schwalm-eder.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.kreissparkasse-schwalm-eder.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Kreissparkasse Schwalm-Eder&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Kreissparkasse Schwalm-Eder\nAnstalt des \u00f6ffentlichen Rechts\nSparkassenplatz 1\n34212\u00a0Melsungen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;52052154&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HELADEF1MEG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HR A 14161 beim Amtsgericht Fritzlar&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE113056386&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4956617070&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4956617073100&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@kskse.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.ssk-cuxhaven.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.ssk-cuxhaven.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Stadtsparkasse Cuxhaven&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Stadtsparkasse Cuxhaven\nAnstalt des \u00f6ffentlichen Rechts\nRohdestra\u00dfe 6\n27472\u00a0Cuxhaven&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;24150001&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;BRLADE21CUX&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HRA 110595 beim Amtsgericht Tostedt&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE115168565&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4947211090&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+494721109276&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;rechnungseingang@ssk-cuxhaven.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.spk-vorpommern.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.spk-vorpommern.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Vorpommern&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Vorpommern\nAnstalt \u00d6ffentlichen Rechts\nAn der Sparkasse 1\n17489\u00a0Greifswald&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;15050500&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;NOLADE21GRW&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Handelsregister Stralsund HRA 1291&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE811671292&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4938345577888&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4938345577239&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@spk-vorpommern.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.ostsaechsische-sparkasse-dresden.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.ostsaechsische-sparkasse-dresden.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Osts\u00e4chsische Sparkasse Dresden&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Osts\u00e4chsische Sparkasse Dresden\nAnstalt des \u00d6ffentlichen Rechts\nG\u00fcntzplatz 5\n01307\u00a0Dresden&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;85050300&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;OSDDDE81XXX&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HRA 4000  beim Amtsgericht Dresden&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE140135071&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+493514550&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;e-mail@sparkasse-dresden.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.mbs.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.mbs.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Mittelbrandenburgische Sparkasse in Potsdam&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Mittelbrandenburgische Sparkasse in Potsdam\nAnstalt des \u00f6ffentlichen Rechts\nSaarmunder Str. 61\n14478\u00a0Potsdam&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;16050000&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;WELADED1PMB&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;HRA 2432 P beim Amtsgericht Potsdam&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE138408302&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49331898989&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49331898985&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;kontakt@mbs.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.sparkasse-opr.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.sparkasse-opr.de/de/home/toolbar/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Ostprignitz-Ruppin&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Sparkasse Ostprignitz-Ruppin\nAnstalt des \u00f6ffentlichen Rechts\nFontaneplatz 1\n16816\u00a0Neuruppin&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;16050202&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;WELADED1OPR&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;A 1037 beim Amtsgericht Neuruppin&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE138672917&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+493391810&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49339181292222&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@sparkasse-opr.de&amp;quot;&lt;/span&gt;},
...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;and like this for Volskbank institutes&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;[
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.vb-isun.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.vb-isun.de/service/rechtliche-hinweise/impressum_OSOGS.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank in Schaumburg und Nienburg eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Klosterstr. 30\n31737 Rinteln&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;25591413&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODEF1BCK&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE116160038&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49572495145300&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49575140589&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@vb-isun.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-kleverland.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-kleverland.de/service/rechtliche-hinweise/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank Kleverland eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Minoritenstr. 2\n47533 Kleve&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;32460422&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODED1KLL&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE120050936&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4928218080&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@volksbank-kleverland.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.leipziger-volksbank.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.leipziger-volksbank.de/service/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Leipziger Volksbank eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Schillerstr. 3\n04109 Leipzig&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;86095604&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODEF1LVB&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE141508765&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4934169790&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+493416979106&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Kontakt@leipziger-volksbank.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.vr-genobank.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.vr-genobank.de/service/rechtliche-hinweise/impressum_OSOGS.nolayer.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;VR GenoBank DonauWald eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Raiffeisenstrasse 1\n94234 Viechtach&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;74190000&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODEF1DGV&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE131459282&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49992284010&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+499942944966&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;online@vr-genobank.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-plochingen.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-plochingen.de/service/rechtliche-hinweise/impressum_OSOGS.nolayer.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank Plochingen eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Am Fischbrunnen 8 \n73207 Plochingen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;61191310&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODES1VBP&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE145341772&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49715398250&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+497153706146&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;ezv@volksbank-plochingen.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-eg.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-eg.de/service/rechtliche-hinweise/impressum_OSOGS.nolayer.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;M\u00fcnsterstr. 34\n48231 Warendorf&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;41262501&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODEM1AHL&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE126731251&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+492581570&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49258157122&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;kundenservicecenter@volksbank-eg.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.vbidr.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.vbidr.de/service/rechtliche-hinweise/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank in der Region eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Herrenberger Str. 1-5\n72070 T\u00fcbingen&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;60391310&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODES1VBH&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE145047512&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4970329400&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+4970329401193&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@vbidr.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-stuttgart.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-stuttgart.de/service/rechtliche-hinweise/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank Stuttgart eG&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Daimlerstra\u00dfe 129\n70372 Stuttgart&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;60090100&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;VOBADESS&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE147325720&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+497111810&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+497111812497&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;info@volksbank-stuttgart.de&amp;quot;&lt;/span&gt;},
{&lt;span class="hljs-attr"&gt;&amp;quot;url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-syke.de&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;imprint_url&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://www.volksbank-syke.de/service/rechtliche-hinweise/impressum.html&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Volksbank eG, Syke&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;address&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Bremer Str. 28\n27211 Bassum&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;routing_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;29167624&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;bic&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;GENODEF1SHR&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;company_register_number&amp;quot;&lt;/span&gt;: &lt;span class="hljs-literal"&gt;null&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;vat_id&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;DE116638071&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;phone&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49424185858&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;telefax&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;+49424185859&amp;quot;&lt;/span&gt;, &lt;span class="hljs-attr"&gt;&amp;quot;email&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;kundenservice@volksbank-syke.de&amp;quot;&lt;/span&gt;},
...
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;At the moment the code is not yet published.
I am considering doing so later on but for the moment I only plan to release the full blown JSON files through some static file serving, e.g., a GitHub gist or so.
Stay tuned.
If you are interested in the data set or any questions related to it, please reach out to me.&lt;/p&gt;
&lt;p&gt;Learnings along the way:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Sometimes it helps to play with the User Agent string.
Using &lt;code&gt;curl/7.72.0&lt;/code&gt; opened some doors that otherwise would be closed.&lt;/li&gt;
&lt;li&gt;Sometimes it makes sense to slow down crawling in order to not DOS servers.
Still pages might put measurements in place to prevent too many requests from one IP address.
In my case this could be mitigated by writing a custom middleware that replaced the original &lt;code&gt;RetryMiddleware&lt;/code&gt; and forced a 2min break if a &lt;code&gt;503 Service Unavailable&lt;/code&gt; error was found.&lt;/li&gt;
&lt;li&gt;Writing web-crawlers to extract data based on the DOM nodes paths and CSS classes or xpaths is brittle.
Maybe it would help to train a ML model to extract the data on its own.
The model might be much more stable against changes on the website that might occur at any time.&lt;/li&gt;
&lt;/ul&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="data-extraction"/>
    <category term="data-pipeline"/>
    <category term="web-crawler"/>
  </entry>
  <entry>
    <title>Copy large files to Nextcloud</title>
    <published>2021-01-02T16:49:22+01:00</published>
    <updated>2021-01-02T19:35:57+01:00</updated>
    <id>https://blog.dotcs.me/posts/nextcloud-copy-large-files</id>
    <link href="https://blog.dotcs.me/posts/nextcloud-copy-large-files"/>
    <summary>Copying large files to Nextcloud can be time consuming as a lot overhead is involved in passing the files through the LAMP stack. This blog post shows how files can be copied directly in the underlying file system and how Nextcloud's caches can be invalidated to inform Nextcloud about the new files.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/nextcloud-copy-large-files">&lt;p&gt;I'm running my own Nextcloud instance in a &lt;a href="https://hub.docker.com/_/nextcloud/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;docker container&lt;/a&gt; with the &lt;code&gt;data&lt;/code&gt; directory mounted from a local folder on my disk.
Unfortuantely it's not very satisfying to use the web UI or using a &lt;a href="https://docs.nextcloud.com/server/16/user_manual/files/access_webdav.html"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;davfs2 mount&lt;/a&gt; of the disk to copy large files to the Nextcloud instance.
This is because files must be passed through the full &lt;a href="https://en.wikipedia.org/wiki/LAMP_%28software_bundle%29"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;LAMP stack&lt;/a&gt; which involves a lot overhead and slows down any large file uploads.&lt;/p&gt;
&lt;p&gt;An alternative to upload files via the browser or WebDAV is to copy them directly into the user data folder and then force Nextcloud to sync the content with its internal file cache and database entries.&lt;/p&gt;
&lt;p&gt;Say our Nextcloud instance data lives in &lt;code&gt;/opt/nextcloud/data&lt;/code&gt; and our source files live in &lt;code&gt;/tmp/stage&lt;/code&gt; on the server.
Further let the Nextcloud username be &lt;code&gt;my-user&lt;/code&gt; and say the source data should be copied to &lt;code&gt;~/my-folder&lt;/code&gt; in Nextcloud.&lt;/p&gt;
&lt;p&gt;Copying the data would then look like this:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# copy files&lt;/span&gt;
rsync -avP /tmp/stage/ /opt/nextcloud/data/my-user/files/my-folder/
&lt;span class="hljs-comment"&gt;# fix permissions&lt;/span&gt;
chmod www-data:www-data /opt/nextcloud/data/my-user/files/my-folder
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's necessary to fix the permissions since the &lt;a href="https://github.com/nextcloud/docker/blob/b23910be9215f8338aee419007feb70cdacb7741/20.0/apache/entrypoint.sh#L16"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;default user&lt;/a&gt; in the &lt;a href="https://hub.docker.com/_/nextcloud/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;official Nextcloud docker images&lt;/a&gt; is the &lt;code&gt;www-data&lt;/code&gt; user.
See also the &lt;a href="https://docs.nextcloud.com/server/stable/admin_manual/configuration_server/occ_command.html#file-operations"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Nextcloud documentation&lt;/a&gt;.
Details:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; docker-compose &lt;span class="hljs-built_in"&gt;exec&lt;/span&gt; nextcloud id www-data&lt;/span&gt;
uid=33(www-data) gid=33(www-data) groups=33(www-data)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the files have been copied, Nextcloud must be forced to re-index all files within this directory:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; docker-compose &lt;span class="hljs-built_in"&gt;exec&lt;/span&gt; -u www-data nextcloud ./occ files:scan --path=&lt;span class="hljs-string"&gt;&amp;quot;/my-user/files/my-folder&amp;quot;&lt;/span&gt;&lt;/span&gt;
Starting scan for user 1 out of 1 (my-user)
+---------+-------+--------------+
| Folders | Files | Elapsed time |
+---------+-------+--------------+
| 6       | 354   | 00:00:01     |
+---------+-------+--------------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The internal database and caches have been updated and your files will pop up in the web UI afterwards.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="nextcloud"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Privacy and Audible Audiobooks</title>
    <published>2020-12-30T22:46:05+01:00</published>
    <updated>2021-01-07T11:38:05+01:00</updated>
    <id>https://blog.dotcs.me/posts/audible-audiobooks-privacy</id>
    <link href="https://blog.dotcs.me/posts/audible-audiobooks-privacy"/>
    <summary>This article mentions privacy concerns of the Amazon Audible platform and discusses how to prevent a lock-in into Amazon's apps in order to gain more privacy.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/audible-audiobooks-privacy">&lt;p&gt;Audible has a lot books available as audiobooks and I think a price of about 10 EUR per book is more than justified.
But unfortunately Audible locks people into their own ecosystem.
Users must use the official Audible app(s) to download and listen to the purchased books.
To my knowledge third party software is not supported.
This has a lot to do with digital right managment (DRM) of course, but the situation also plays into Amazon's cards.
By locking the people into a certain app they can analyze how people listen to the audiobooks.
For example they could answer the following questions about you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Which are the audiobooks that you listen to?&lt;/li&gt;
&lt;li&gt;Which audiobooks have you started to hear? Which of them have you finished? Which have you rejected?&lt;/li&gt;
&lt;li&gt;Which chapters have you re-read? Which parts have you skipped?&lt;/li&gt;
&lt;li&gt;and so on&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;From their privacy information details:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Automatic information we collect and analyze include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;[...]&lt;/li&gt;
&lt;li&gt;content interaction information, such as &lt;strong&gt;content downloads, streams, playback details&lt;/strong&gt;, &lt;strong&gt;listening behavior like start and stop points&lt;/strong&gt;, &lt;strong&gt;listening duration&lt;/strong&gt;, reading behavior on your Kindle devices and apps (so we can provide our WhisperSync for Voice functionality &lt;strong&gt;and award badges based on your listening&lt;/strong&gt; and also calculate royalties), and network details for streaming and download quality, including information about your internet service provider;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;small&gt;&lt;a href="https://help.audible.com/s/article/audible-privacy-information?language=en_US"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Audible Privacy Information&lt;/a&gt;, relevant text has been highlighted by me&lt;/small&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I'm wondering if those award badges are mainly there so that Amazon can argue about any privacy concerns that people might have.
At least for me they are not important at all, I would prefer a tracking-free Audible variant and would gladly waive the badges for this.&lt;/p&gt;
&lt;p&gt;While it's not possible to prevent Amazon from tracking which Audiobooks are bought, it is possible to stop them from tracking HOW you read them.
Let's see how this can be done.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Disclaimer&lt;/strong&gt;:
This approach breaks the DRM protection of the Audiobooks.
Please make sure that this is legal in your country for the intended purpose.&lt;/p&gt;
&lt;h2 id="downloading-audiobooks"&gt;&lt;a class="markdownIt-Anchor" href="#downloading-audiobooks"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Downloading Audiobooks&lt;/h2&gt;
&lt;p&gt;Audiobooks that you have bought from Audible can be downloaded from your &lt;a href="https://www.audible.de/library/titles"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;library&lt;/a&gt;.
Execute this script in your &lt;a href="https://developer.mozilla.org/en-US/docs/Tools/Browser_Console"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;browser's console&lt;/a&gt; to copy all relevant download links into the clipboard.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; links = &lt;span class="hljs-built_in"&gt;Array&lt;/span&gt;.from(&lt;span class="hljs-built_in"&gt;document&lt;/span&gt;.querySelectorAll(&lt;span class="hljs-string"&gt;&amp;#x27;.adbl-lib-action-download &amp;gt; a[href^=&amp;quot;https://cds.audible.de/download&amp;quot;]&amp;#x27;&lt;/span&gt;))
    .map(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;el&lt;/span&gt; =&amp;gt;&lt;/span&gt; el.attributes[&lt;span class="hljs-string"&gt;&amp;#x27;href&amp;#x27;&lt;/span&gt;].nodeValue);
&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; titles = &lt;span class="hljs-built_in"&gt;Array&lt;/span&gt;.from(&lt;span class="hljs-built_in"&gt;document&lt;/span&gt;.querySelectorAll(&lt;span class="hljs-string"&gt;&amp;#x27;#adbl-library-content-main .bc-list a.bc-link[href^=&amp;quot;/pd&amp;quot;]&amp;#x27;&lt;/span&gt;))
    .map(&lt;span class="hljs-function"&gt;&lt;span class="hljs-params"&gt;el&lt;/span&gt; =&amp;gt;&lt;/span&gt; el.innerText);

&lt;span class="hljs-keyword"&gt;let&lt;/span&gt; data = links.map(&lt;span class="hljs-function"&gt;(&lt;span class="hljs-params"&gt;href, i&lt;/span&gt;) =&amp;gt;&lt;/span&gt; ({ href, &lt;span class="hljs-attr"&gt;title&lt;/span&gt;: titles[i]}));
copy(data);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This creates a valid JSON object in your clipboard which contains a list of records with the keys &lt;code&gt;href&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt; for each audiobook.
&lt;strong&gt;Don't share the document with anyone as the download links contain your private key.&lt;/strong&gt;&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;wl-paste &amp;gt; /tmp/audible.json
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here I'm using the &lt;a href="https://github.com/bugaevc/wl-clipboard"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;wl-paste&lt;/a&gt; (wayland only) command to get the content from the system's clipboard.
If you're using X11 you might want to replace &lt;code&gt;wl-paste&lt;/code&gt; with &lt;code&gt;xsel&lt;/code&gt; or whatever you have installed.
In a second I will also use &lt;a href="https://stedolan.github.io/jq/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;jq&lt;/a&gt; to filter the JSON file, make sure to install it also.&lt;/p&gt;
&lt;p&gt;If everything worked out, the temporary file you have created should look like this:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;[
  {
    &lt;span class="hljs-attr"&gt;&amp;quot;href&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;https://cds.audible.de/download?asin=B08KQJ469F&amp;amp;cust_id=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&amp;amp;codec=LC_128_44100_Stereo&amp;amp;source=Audible&amp;amp;type=AUDI&amp;quot;&lt;/span&gt;,
    &lt;span class="hljs-attr"&gt;&amp;quot;title&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Stille Nacht in der Provence&amp;quot;&lt;/span&gt;
  },
  &lt;span class="hljs-comment"&gt;// ....&lt;/span&gt;
]
&lt;/code&gt;&lt;/pre&gt;
&lt;small&gt;
&lt;p&gt;Note that the param &lt;code&gt;asin&lt;/code&gt; in the link refers to the book and &lt;code&gt;cust_id&lt;/code&gt; is a customer specific identifier that gives access to the book and should not be shared with anyone.&lt;/p&gt;
&lt;/small&gt;
&lt;p&gt;Now let's kick off &lt;code&gt;wget&lt;/code&gt; and download our audiobooks.
Please note that audiobooks are large (~1-2GB per file).
Make sure that you have enough storage available.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;mkdir -p /tmp/audible &amp;amp;&amp;amp; &lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; &lt;span class="hljs-variable"&gt;$_&lt;/span&gt;

jq -c &lt;span class="hljs-string"&gt;&amp;#x27;.[]&amp;#x27;&lt;/span&gt; /tmp/audible.json | &lt;span class="hljs-keyword"&gt;while&lt;/span&gt; &lt;span class="hljs-built_in"&gt;read&lt;/span&gt; i; &lt;span class="hljs-keyword"&gt;do&lt;/span&gt;
    href=$(&lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-variable"&gt;$i&lt;/span&gt; | jq -r &lt;span class="hljs-string"&gt;&amp;#x27;.href&amp;#x27;&lt;/span&gt;)
    title=$(&lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-variable"&gt;$i&lt;/span&gt; | jq -r &lt;span class="hljs-string"&gt;&amp;#x27;.title&amp;#x27;&lt;/span&gt;)
    wget -O &lt;span class="hljs-string"&gt;&amp;quot;&lt;span class="hljs-variable"&gt;$title&lt;/span&gt;.aax&amp;quot;&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;&lt;span class="hljs-variable"&gt;$href&lt;/span&gt;&amp;quot;&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;small&gt;
&lt;p&gt;In case you want to download the audiobooks in chunks you can also use &lt;a href="https://www.systutorials.com/docs/linux/man/1-jq/#lbBX"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;jq ranges&lt;/a&gt;, e.g. &lt;code&gt;jq -c '.[:3]'&lt;/code&gt;.&lt;/p&gt;
&lt;/small&gt;
&lt;p&gt;This will place all your audiobooks as &lt;code&gt;*.aax&lt;/code&gt; files in the &lt;code&gt;/tmp/audible&lt;/code&gt; folder.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note: If you have a lot audiobooks you might want to consider a different approach that parallelizes the download queries.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="convert-from-aax-to-m4a"&gt;&lt;a class="markdownIt-Anchor" href="#convert-from-aax-to-m4a"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Convert from aax to m4a&lt;/h2&gt;
&lt;p&gt;The files contain a DRM protection which prevents users from sharing the files with others.
In this step you will remove the DRM protection.
&lt;strong&gt;Please make sure that this step is allowed by law in your country before proceeding.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To remove the DRM protection &lt;a href="https://github.com/ryanfb/docker_inaudible_rainbowcrack"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;this respository&lt;/a&gt; can be used.
It contains a docker file that comes pre-installed with all dependencies to break the DRM encryption and decrypt the files.
Make sure to have &lt;a href="https://docs.docker.com/get-docker/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;docker&lt;/a&gt; pre-installed to be able to follow the next steps.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;mkdir -p /tmp/inaudible &amp;amp;&amp;amp; &lt;span class="hljs-variable"&gt;$_&lt;/span&gt;
git &lt;span class="hljs-built_in"&gt;clone&lt;/span&gt; https://github.com/ryanfb/docker_inaudible_rainbowcrack .
docker build -t inaudible .

&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; /tmp/audible
docker run --rm -v $(&lt;span class="hljs-built_in"&gt;pwd&lt;/span&gt;):/data inaudible
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This will read in all aax files, restore the encryption key via brute-force and then decrypt all aax files.
The result will be one m4a and one png artwork file for each aax file.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Note that this will take additional storage on your disk - approximately the same amount that is needed for the aax files.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="listen-on-android"&gt;&lt;a class="markdownIt-Anchor" href="#listen-on-android"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Listen on Android&lt;/h2&gt;
&lt;p&gt;To listen to m4a files from an Android device I can highly recommend the &lt;a href="https://f-droid.org/en/packages/de.ph1b.audiobook/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Voice&lt;/a&gt; app from the F-Droid store.
The interface is super clean, the application remembers the playback location and simply does its job - without tracking the user's behavior.
Kudos to Paul Woitaschek for this piece of software!&lt;/p&gt;
&lt;p&gt;Upload both, the m4a and png artwork file, to a folder on your device and add this folder to the list of folders that are scanned by Voice and you're done.&lt;/p&gt;
&lt;h2 id="mission-completed"&gt;&lt;a class="markdownIt-Anchor" href="#mission-completed"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Mission completed 🥳&lt;/h2&gt;
&lt;p&gt;So these few steps allow you to create a private copy of the audiobooks that you own.
By using m4a files it's up to you to choose what player you want to use.
Using an open source player such as &lt;a href="https://f-droid.org/en/packages/de.ph1b.audiobook/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Voice&lt;/a&gt; helps to be sure that noone is tracking how you read your books.
Let's take our privacy back - step by step.&lt;/p&gt;
&lt;p&gt;Oh and since award badges seem to be relevant these days, here is your nerds-love-privacy badge&lt;/p&gt;
&lt;p&gt;&lt;span style="font-size: 4em;"&gt;🤓&lt;/span&gt;&lt;/p&gt;
&lt;p&gt;Have fun with it or throw it away, I don't care.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;:
Oh, and if you're interested in an ETL pipeline that integrates well with a Nextcloud instance to serve the &lt;code&gt;m4a&lt;/code&gt; files, have a look into &lt;a href="https://git.home.dotcs.me/dotcs/audible-nextcloud-etl"&gt;this repository&lt;/a&gt;.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="drm"/>
    <category term="linux"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Pi-hole User Group Management</title>
    <published>2020-11-18T08:10:34+01:00</published>
    <updated>2020-11-18T08:10:34+01:00</updated>
    <id>https://blog.dotcs.me/posts/pihole-group-mgmt</id>
    <link href="https://blog.dotcs.me/posts/pihole-group-mgmt"/>
    <summary>In this post I discuss how a Pi-hole can be controlled so that different rules apply to different groups of devices in the local network.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/pihole-group-mgmt">&lt;p&gt;At home I have a &lt;a href="https://pi-hole.net/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Pi-hole&lt;/a&gt; in service which acts as my central DNS provider for all devices in my network.
This means that by default all devices share the same filter lists, so effectively ads are blocked on all devices.&lt;/p&gt;
&lt;p&gt;This setup is most likely the setup most people need and prefer.
For our home it's not working, because some devices should see clear and unfiltered DNS entries.
Let's see how we can exclude certain devices from the filter lists.&lt;/p&gt;
&lt;h2 id="pi-hole-group-management"&gt;&lt;a class="markdownIt-Anchor" href="#pi-hole-group-management"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Pi-hole Group Management&lt;/h2&gt;
&lt;p&gt;Since &lt;a href="https://pi-hole.net/2020/05/10/pi-hole-v5-0-is-here/#page-content"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Pi-hole v5.0&lt;/a&gt;, which has been released in 2020-05, groups can be configured.
Known devices can be put into groups which can have different filter lists assigned to them.
The group management can be found under &lt;code&gt;http(s)://my-pi.hole/admin/groups.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In my setup I created a group for all my devices which I named &lt;code&gt;dotcs&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/pihole-admin-groups.png" alt="Pi-hole Admin: Group management" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Pi-hole Admin: Group management&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;By default this group does nothing.
First devices need to be assigned to the group.
Those settings can be found under &lt;code&gt;http(s)://my-pi.hole/admin/groups-clients.php&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/pihole-admin-devices.png" alt="Pi-hole Admin: Device management" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Pi-hole Admin: Device management&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;After that the adlists need to be configured and attached to the group.
In my case I removed all of the lists from the &lt;code&gt;default&lt;/code&gt; group and attached them only to the &lt;code&gt;dotcs&lt;/code&gt; group.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/pihole-admin-adlists.png" alt="Pi-hole Admin: Adlist management" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Pi-hole Admin: Adlist management&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;With this configuration all devices listed in the &lt;code&gt;dotcs&lt;/code&gt; group see filtered DNS entries whereas all other devices see unfiltered DNS entries.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to the Pi-hole developers for this awesome feature and please consider &lt;a href="https://pi-hole.net/donate/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;donating to the project&lt;/a&gt; if you like it.&lt;/em&gt;&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="notes"/>
    <category term="pihole"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>How to deploy a Next.js application to GitHub pages</title>
    <published>2020-11-16T22:56:02+01:00</published>
    <updated>2020-11-17T08:40:38+01:00</updated>
    <id>https://blog.dotcs.me/posts/nextjs-github-pages</id>
    <link href="https://blog.dotcs.me/posts/nextjs-github-pages"/>
    <summary>In this post I discuss which tweaks are necessary to deploy a Next.js project exported as static HTML files to GitHub pages.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/nextjs-github-pages">&lt;p&gt;&lt;a href="https://nextjs.org"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Next.js&lt;/a&gt; is becoming very popular - for good reasons.
To me it's one of the best frameworks out there to create all kinds of web pages that are based on &lt;a href="https://reactjs.org"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;React&lt;/a&gt;.
Since Next.js does allow for full server-side rendering and &lt;a href="https://nextjs.org/docs/advanced-features/static-html-export"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;static html exports&lt;/a&gt;, pages can be rendered completely on the server and delivered to the client as dump static HTML pages with a little bit of CSS and JavaScript where needed.
This makes them perfectly suitable to be hosted on &lt;a href="https://pages.github.com"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;GitHub pages&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My first thought was: Yeah, easy let's do this quickly.
But not so fast!
I've leared that there are a few things that require tweaks.&lt;/p&gt;
&lt;h2 id="use-github-actions-to-build-and-export-nextjs-pages"&gt;&lt;a class="markdownIt-Anchor" href="#use-github-actions-to-build-and-export-nextjs-pages"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Use GitHub Actions to build and export Next.js pages&lt;/h2&gt;
&lt;p&gt;I use GitHub actions to build the static pages.
The corresponding project setting is set to deliver pages from the &lt;code&gt;gh-pages&lt;/code&gt; branch.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/github-pages-settings.png" alt="GitHub Pages settings to deliver from branch gh-pages" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;GitHub Pages settings to deliver from branch gh-pages&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;The corresponding &lt;code&gt;.github/workflows/gh-pages.yml&lt;/code&gt; is simple.
Note that the &lt;code&gt;npm run export&lt;/code&gt; script has an enviroment variable &lt;code&gt;DEPLOY_TARGET: gh-pages&lt;/code&gt; attached to it.
This env variable will be used in the second step.
In the deploy step the branch is set to &lt;code&gt;gh-pages&lt;/code&gt; and we deliver results from the &lt;code&gt;out&lt;/code&gt; folder, which is the default target folder for the &lt;code&gt;next export&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;Also note that the pipelie creates the (empty) file &lt;code&gt;out/.nojekyll&lt;/code&gt;.
This is necessary to bypass Jekyll processing on GitHub pages as mentioned &lt;a href="https://github.blog/2009-12-29-bypassing-jekyll-on-github-pages/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;here&lt;/a&gt;.
Otherwise folders that start with an underscore are ignored, but Next.js puts merged assets, e.g. CSS and JS files, into a folder &lt;code&gt;_next&lt;/code&gt;. 😵&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Build&lt;/span&gt; &lt;span class="hljs-string"&gt;and&lt;/span&gt; &lt;span class="hljs-string"&gt;Deploy&lt;/span&gt;
&lt;span class="hljs-attr"&gt;on:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;push:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;branches:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;master&lt;/span&gt;
&lt;span class="hljs-attr"&gt;jobs:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;build-and-deploy:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;runs-on:&lt;/span&gt; &lt;span class="hljs-string"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;steps:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Checkout&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;actions/checkout@v2.3.1&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;with:&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;persist-credentials:&lt;/span&gt; &lt;span class="hljs-literal"&gt;false&lt;/span&gt;

      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Install&lt;/span&gt; &lt;span class="hljs-string"&gt;and&lt;/span&gt; &lt;span class="hljs-string"&gt;Build&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;actions/setup-node@v1&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;run:&lt;/span&gt; &lt;span class="hljs-string"&gt;npm&lt;/span&gt; &lt;span class="hljs-string"&gt;install&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;run:&lt;/span&gt; &lt;span class="hljs-string"&gt;npm&lt;/span&gt; &lt;span class="hljs-string"&gt;run&lt;/span&gt; &lt;span class="hljs-string"&gt;build&lt;/span&gt;     &lt;span class="hljs-comment"&gt;# runs `next build`&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;run:&lt;/span&gt; &lt;span class="hljs-string"&gt;npm&lt;/span&gt; &lt;span class="hljs-string"&gt;run&lt;/span&gt; &lt;span class="hljs-string"&gt;export&lt;/span&gt;    &lt;span class="hljs-comment"&gt;# runs `next export`&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;env:&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;CI:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;DEPLOY_TARGET:&lt;/span&gt; &lt;span class="hljs-string"&gt;gh-pages&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;run:&lt;/span&gt; &lt;span class="hljs-string"&gt;touch&lt;/span&gt; &lt;span class="hljs-string"&gt;out/.nojekyll&lt;/span&gt;

      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-attr"&gt;name:&lt;/span&gt; &lt;span class="hljs-string"&gt;Deploy&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;uses:&lt;/span&gt; &lt;span class="hljs-string"&gt;JamesIves/github-pages-deploy-action@3.7.1&lt;/span&gt;
        &lt;span class="hljs-attr"&gt;with:&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;GITHUB_TOKEN:&lt;/span&gt; &lt;span class="hljs-string"&gt;${{&lt;/span&gt; &lt;span class="hljs-string"&gt;secrets.GITHUB_TOKEN&lt;/span&gt; &lt;span class="hljs-string"&gt;}}&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;BRANCH:&lt;/span&gt; &lt;span class="hljs-string"&gt;gh-pages&lt;/span&gt;  &lt;span class="hljs-comment"&gt;# The branch the action should deploy to.&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;FOLDER:&lt;/span&gt; &lt;span class="hljs-string"&gt;out&lt;/span&gt;       &lt;span class="hljs-comment"&gt;# The folder the action should deploy.&lt;/span&gt;
          &lt;span class="hljs-attr"&gt;CLEAN:&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;       &lt;span class="hljs-comment"&gt;# Automatically remove deleted files from the deploy branch&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="rewrite-paths-to-static-files"&gt;&lt;a class="markdownIt-Anchor" href="#rewrite-paths-to-static-files"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Rewrite paths to static files&lt;/h2&gt;
&lt;p&gt;When serving websites via &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt; any static assets must point to &lt;code&gt;&amp;lt;username&amp;gt;.github.io/&amp;lt;projectname&amp;gt;&lt;/code&gt; with &lt;code&gt;&amp;lt;projectname&amp;gt;&lt;/code&gt; being the name of the repository (typically again &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;).
GitHub Pages rewrites some paths internally, so that they &lt;strong&gt;appear&lt;/strong&gt; to come from &lt;code&gt;&amp;lt;username&amp;gt;.github.io&lt;/code&gt;, but things will not work out if the &lt;code&gt;&amp;lt;projectname&amp;gt;&lt;/code&gt; part is missing.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;next export&lt;/code&gt; allows to set a &lt;code&gt;assetPrefix&lt;/code&gt;, which tweaking the URL paths and re-write them from &lt;code&gt;/&lt;/code&gt; to &lt;code&gt;/&amp;lt;assetPrefix&amp;gt;/&lt;/code&gt;.
This can be configured in the project's &lt;code&gt;next.config.js&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;const&lt;/span&gt; ghPages = process.env.DEPLOY_TARGET === &lt;span class="hljs-string"&gt;&amp;#x27;gh-pages&amp;#x27;&lt;/span&gt;;

&lt;span class="hljs-built_in"&gt;module&lt;/span&gt;.exports = {
  &lt;span class="hljs-attr"&gt;assetPrefix&lt;/span&gt;: ghPages ? &lt;span class="hljs-string"&gt;&amp;#x27;/dotcs.github.io/&amp;#x27;&lt;/span&gt; : &lt;span class="hljs-string"&gt;&amp;#x27;&amp;#x27;&lt;/span&gt;   &lt;span class="hljs-comment"&gt;// customize this value&lt;/span&gt;
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this line in place we detect if we're executed by the GitHub Action runner and re-write paths accordingly.&lt;/p&gt;
&lt;h2 id="success"&gt;&lt;a class="markdownIt-Anchor" href="#success"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Success&lt;/h2&gt;
&lt;p&gt;With those tweaks Next.js pages can be hosted via GitHub pages easily.
You just need to be aware of them. 😉&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="next.js"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Migrate from i3 to sway</title>
    <published>2020-11-15T09:52:00Z</published>
    <updated>2020-11-28T21:27:00+01:00</updated>
    <id>https://blog.dotcs.me/posts/migrate-from-i3-to-sway</id>
    <link href="https://blog.dotcs.me/posts/migrate-from-i3-to-sway"/>
    <summary>This post describes my migration from i3 (X.org Server) to Sway (Wayland).</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/migrate-from-i3-to-sway">&lt;p&gt;I recently changed from the Linux display server protocol &lt;a href="https://en.wikipedia.org/wiki/X.Org_Server"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;X.org Server&lt;/a&gt; to &lt;a href="https://en.wikipedia.org/wiki/Wayland_(display_server_protocol)"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Wayland&lt;/a&gt; on one of my machines.
My favorite window manager &lt;a href="https://i3wm.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;i3&lt;/a&gt; is not compatible with Wayland though, so I had to search for an alternative.
Luckily there is a drop-in replacement for i3 available which is named &lt;a href="https://swaywm.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Sway&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Sway is very similar to i3, it even uses the same syntax for the config, so the change required only a few tweaks which I will now talk about.
I can also recommend to read the &lt;a href="https://wiki.archlinux.org/index.php/Sway"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Sway article on the Arch Wiki&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this let's take a brief moment to say &amp;quot;Thank you&amp;quot; to all contributors of Sway and Wayland and maintainers of the corresponding packages in the various distros out there. ♥&lt;/p&gt;
&lt;h2 id="installation"&gt;&lt;a class="markdownIt-Anchor" href="#installation"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Installation&lt;/h2&gt;
&lt;p&gt;Installation of sway under Arch Linux is easy.
It's as simple as:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;yay -S sway
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="copy-configuration"&gt;&lt;a class="markdownIt-Anchor" href="#copy-configuration"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Copy configuration&lt;/h2&gt;
&lt;p&gt;Sway searches it's configuration in &lt;code&gt;~/.config/sway/config&lt;/code&gt; so I copied my i3 config there.
All changes go into this config file.&lt;/p&gt;
&lt;h2 id="adjust-configuration"&gt;&lt;a class="markdownIt-Anchor" href="#adjust-configuration"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Adjust configuration&lt;/h2&gt;
&lt;p&gt;After the configuration has been copied use a separate TTY to try our sway and its configuration.
Remember that &lt;code&gt;&amp;lt;SUPER&amp;gt; + SHIFT + c&lt;/code&gt; does reload your configuration which is quite handy for trying things out.
Also &lt;code&gt;swaymsg&lt;/code&gt; can be used to send messages to sway, so for example the following command would set the scaling 2 for all output devices.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;swaymsg output &amp;quot;*&amp;quot; scale 2
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="screen-output-resolution"&gt;&lt;a class="markdownIt-Anchor" href="#screen-output-resolution"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Screen output resolution&lt;/h3&gt;
&lt;p&gt;I'm working with a HiDPI screen which was detected correctly in Sway.
But I found the scaling factor of 2, which is the default in HiDPI screens, too much.
Although it's not recommended to use float values here, I found a scaling factor of 1.3 to fit my needs, so I added this line to my config:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;# Screen scaling (default is 2)
output eDP-1 scale 1.3
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To list connected displays use &lt;code&gt;swaymsg -t get_outputs&lt;/code&gt; which in my case now (after the change) shows as:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; swaymsg -t get_outputs   &lt;/span&gt;
Output eDP-1 &amp;#x27;Apple Computer Inc Color LCD 0x00000000&amp;#x27; (focused)
  Current mode: 2560x1600 @ 59.972000 Hz
  Position: 0,0
  Scale factor: 1.300000
  Scale filter: linear
  Subpixel hinting: unknown
  Transform: normal
  Workspace: 4
  Max render time: off
  Adaptive sync: disabled
  Available modes:
    2560x1600 @ 59.972000 Hz
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="background-images-with-automatic-rotation"&gt;&lt;a class="markdownIt-Anchor" href="#background-images-with-automatic-rotation"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Background images with automatic rotation&lt;/h3&gt;
&lt;p&gt;I like to have randomly chosen background images that change every now and then.
Sway makes it easy to implement this.
I added the following line to my config which executes a script during start.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;exec --no-startup-id ~/.local/scripts/sway-rotate-bg-image.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The script itself doesn't do much.
It lists all files in a certain folder, shuffles the names and selects one of them.
It updates the background on all attached displays image via &lt;code&gt;swaymsg&lt;/code&gt; and then pauses for 300 seconds (=5min) before the next iteration.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;#!/usr/bin/env bash&lt;/span&gt;
IMG_DIR=~/wallpapers
&lt;span class="hljs-keyword"&gt;while&lt;/span&gt; &lt;span class="hljs-literal"&gt;true&lt;/span&gt;;
&lt;span class="hljs-keyword"&gt;do&lt;/span&gt;
    IMG=`ls -t1 &lt;span class="hljs-variable"&gt;$IMG_DIR&lt;/span&gt;/* | shuf | head -n 1`
    swaymsg output &lt;span class="hljs-string"&gt;&amp;quot;*&amp;quot;&lt;/span&gt; &lt;span class="hljs-built_in"&gt;bg&lt;/span&gt; &lt;span class="hljs-variable"&gt;$IMG&lt;/span&gt; fill; sleep 300
&lt;span class="hljs-keyword"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="detect-idle-state-and-lock-screen"&gt;&lt;a class="markdownIt-Anchor" href="#detect-idle-state-and-lock-screen"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Detect idle state and lock screen&lt;/h3&gt;
&lt;p&gt;To automatically lock the screen there are luckily two programs available that do the heavy-lifting.
They are called swayidle and swaylock and can be installed in Arch via:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;yay -S swayidle swaylock
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following script is taken from the &lt;code&gt;swayidle&lt;/code&gt; manpage:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This will lock your screen after 300 seconds of inactivity, then turn off your displays after another 300 seconds, and turn your screens back on when resumed. It will also lock your screen before your computer goes to sleep.&lt;/p&gt;
&lt;p&gt;To make sure swayidle waits for swaylock to lock the screen before it releases the inhibition lock, the -w options is used in swayidle, and -f in swaylock.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;exec --no-startup-id swayidle -w \
    timeout 300 'swaylock -f -c 000000' \
    timeout 600 'swaymsg &amp;quot;output * dpms off&amp;quot;' \
    resume 'swaymsg &amp;quot;output * dpms on&amp;quot;' \
    before-sleep 'swaylock -f -c 000000'
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="screen-backlight-keys"&gt;&lt;a class="markdownIt-Anchor" href="#screen-backlight-keys"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Screen backlight keys&lt;/h3&gt;
&lt;p&gt;In my i3 setup I used &lt;code&gt;xbacklight&lt;/code&gt; to change the backlight of my screen.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;bindsym XF86MonBrightnessUp exec xbacklight +10
bindsym XF86MonBrightnessDown exec xbacklight -10
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This of course won't work anymore in Wayland, so I replaced the old configuration with &lt;code&gt;brightnessctl&lt;/code&gt; which needs to be installed first:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;yay -S brightnessctl
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The syntax of &lt;code&gt;brightnessctrl&lt;/code&gt; differs slightly:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;bindsym XF86MonBrightnessDown exec brightnessctl set 5%-
bindsym XF86MonBrightnessUp exec brightnessctl set +5%
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="keyboard-bindings"&gt;&lt;a class="markdownIt-Anchor" href="#keyboard-bindings"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Keyboard bindings&lt;/h3&gt;
&lt;p&gt;For me one key on the keyboard is completely superfluous: capslock.
In my systems I either deactivate this key or map it to ESC if possible.
Having ESC closer to the home row is benefitial to me - especially when working a lot with vim.
This can done with xkb_options.&lt;/p&gt;
&lt;p&gt;Another interesting setting is the key repeat delay and rate, which can be tuned also quite easily.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;input &amp;quot;type:keyboard&amp;quot; {
    # Capslock key should work as escape key
    xkb_options caps:escape

    repeat_delay 350
    repeat_rate 45
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="ssh-agent"&gt;&lt;a class="markdownIt-Anchor" href="#ssh-agent"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; SSH Agent&lt;/h3&gt;
&lt;p&gt;To automatically start the SSH agent I use a systemd service which is placed in &lt;code&gt;~/.config/systemd/user/ssh-agent.service&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-section"&gt;[Unit]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Description&lt;/span&gt;=SSH key agent

&lt;span class="hljs-section"&gt;[Service]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Type&lt;/span&gt;=simple
&lt;span class="hljs-attr"&gt;Environment&lt;/span&gt;=SSH_AUTH_SOCK=%t/ssh-agent.socket
&lt;span class="hljs-comment"&gt;# DISPLAY required for ssh-askpass to work&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Environment&lt;/span&gt;=DISPLAY=:&lt;span class="hljs-number"&gt;0&lt;/span&gt;
&lt;span class="hljs-attr"&gt;ExecStart&lt;/span&gt;=/usr/bin/ssh-agent -D -a &lt;span class="hljs-variable"&gt;$SSH_AUTH_SOCK&lt;/span&gt;

&lt;span class="hljs-section"&gt;[Install]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;WantedBy&lt;/span&gt;=default.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Also configure the pam environment in &lt;code&gt;~/.pam_environment&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;SSH_AUTH_SOCK DEFAULT=&lt;span class="hljs-string"&gt;&amp;quot;&lt;span class="hljs-variable"&gt;${XDG_RUNTIME_DIR}&lt;/span&gt;/ssh-agent.socket&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then start the service and enable it for future system starts: &lt;code&gt;systemctl --user enable --now ssh-agent.service&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="exit-script"&gt;&lt;a class="markdownIt-Anchor" href="#exit-script"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Exit script&lt;/h3&gt;
&lt;p&gt;Finally I updated my exit script that allows me to lock my screen, suspend, shutdown or reboot my machine, etc.&lt;/p&gt;
&lt;p&gt;The Sway configuration looks as this&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;set $mode_system System (1) lock, (e) logout, (s) suspend, (h) hibernate, (r) reboot, (d) shutdown
mode &amp;quot;$mode_system&amp;quot; {
    bindsym l exec --no-startup-id ~/.local/scripts/sway-exit.sh lock, mode &amp;quot;default&amp;quot;
    bindsym e exec --no-startup-id ~/.local/scripts/sway-exit.sh logout, mode &amp;quot;default&amp;quot;
    bindsym s exec --no-startup-id ~/.local/scripts/sway-exit.sh suspend, mode &amp;quot;default&amp;quot;
    bindsym h exec --no-startup-id ~/.local/scripts/sway-exit.sh hibernate, mode &amp;quot;default&amp;quot;
    bindsym r exec --no-startup-id ~/.local/scripts/sway-exit.sh reboot, mode &amp;quot;default&amp;quot;
    bindsym d exec --no-startup-id ~/.local/scripts/sway-exit.sh shutdown, mode &amp;quot;default&amp;quot;
    # back to normal: Enter or Escape
    bindsym Return mode &amp;quot;default&amp;quot;
    bindsym Escape mode &amp;quot;default&amp;quot;
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;which utilizes the script located in &lt;code&gt;~/.local/scripts/sway-exit.sh&lt;/code&gt;&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;#!/bin/sh&lt;/span&gt;
&lt;span class="hljs-function"&gt;&lt;span class="hljs-title"&gt;lock&lt;/span&gt;&lt;/span&gt;() {
    swaylock -f -c 000000
}

&lt;span class="hljs-keyword"&gt;case&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;&lt;span class="hljs-variable"&gt;$1&lt;/span&gt;&amp;quot;&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt;
    lock)
        lock
        ;;
    &lt;span class="hljs-built_in"&gt;logout&lt;/span&gt;)
        swaymsg &lt;span class="hljs-built_in"&gt;exit&lt;/span&gt;
        ;;
    &lt;span class="hljs-built_in"&gt;suspend&lt;/span&gt;)
        lock &amp;amp;&amp;amp; systemctl &lt;span class="hljs-built_in"&gt;suspend&lt;/span&gt;
        ;;
    hibernate)
        lock &amp;amp;&amp;amp; systemctl hibernate
        ;;
    reboot)
        systemctl reboot
        ;;
    shutdown)
        systemctl poweroff
        ;;
    *)
        &lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;Usage: &lt;span class="hljs-variable"&gt;$0&lt;/span&gt; {lock|logout|suspend|hibernate|reboot|shutdown}&amp;quot;&lt;/span&gt;
        &lt;span class="hljs-built_in"&gt;exit&lt;/span&gt; 2
&lt;span class="hljs-keyword"&gt;esac&lt;/span&gt;

&lt;span class="hljs-built_in"&gt;exit&lt;/span&gt; 0
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="notifications"&gt;&lt;a class="markdownIt-Anchor" href="#notifications"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Notifications&lt;/h3&gt;
&lt;p&gt;In i3 I used &lt;a href="https://dunst-project.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;dunst&lt;/a&gt; to handle notifications.
Unfortunately it's not &lt;a href="https://github.com/dunst-project/dunst/issues/264"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;yet ready&lt;/a&gt; to work with wayland.
I found &lt;a href="https://github.com/emersion/mako"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;mako&lt;/a&gt; to be a good replacement.&lt;br&gt;
Using &lt;code&gt;mako&lt;/code&gt; is dead simple.
After installation an additional line &lt;code&gt;exec mako&lt;/code&gt; in the sway config file does the job.&lt;/p&gt;
&lt;h3 id="printer"&gt;&lt;a class="markdownIt-Anchor" href="#printer"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Printer&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://wiki.archlinux.org/index.php/CUPS"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;CUPS&lt;/a&gt; was not properly started after the switch.
What helped was to create the following configuration in &lt;code&gt;/etc/systemd/system/cups.socket&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-section"&gt;[Unit]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Description&lt;/span&gt;=CUPS Printing Service Sockets

&lt;span class="hljs-section"&gt;[Socket]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;ListenStream&lt;/span&gt;=/var/run/cups/cups.sock
&lt;span class="hljs-attr"&gt;ListenStream&lt;/span&gt;=&lt;span class="hljs-number"&gt;0.0&lt;/span&gt;.&lt;span class="hljs-number"&gt;0.0&lt;/span&gt;:&lt;span class="hljs-number"&gt;631&lt;/span&gt;
&lt;span class="hljs-attr"&gt;ListenDatagram&lt;/span&gt;=&lt;span class="hljs-number"&gt;0.0&lt;/span&gt;.&lt;span class="hljs-number"&gt;0.0&lt;/span&gt;:&lt;span class="hljs-number"&gt;631&lt;/span&gt;
&lt;span class="hljs-attr"&gt;BindIPv6Only&lt;/span&gt;=ipv6-&lt;span class="hljs-literal"&gt;on&lt;/span&gt;ly

&lt;span class="hljs-section"&gt;[Install]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;WantedBy&lt;/span&gt;=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then enable/start the service with &lt;code&gt;systemctl enable --now cups.service&lt;/code&gt; and make sure it started properly:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; systemctl status cups.service&lt;/span&gt;
● cups.service - CUPS Scheduler
     Loaded: loaded (/usr/lib/systemd/system/cups.service; enabled; vendor preset: disabled)
     Active: active (running) since Sat 2020-11-28 21:21:45 CET; 4min 31s ago
TriggeredBy: ● cups.socket
             ● cups.path
       Docs: man:cupsd(8)
   Main PID: 55543 (cupsd)
     Status: &amp;quot;Scheduler is running...&amp;quot;
      Tasks: 1 (limit: 19070)
     Memory: 5.2M
     CGroup: /system.slice/cups.service
             └─55543 /usr/bin/cupsd -l

Nov 28 21:21:45 mali systemd[1]: Starting CUPS Scheduler...
Nov 28 21:21:45 mali systemd[1]: Started CUPS Scheduler.
&lt;/code&gt;&lt;/pre&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
  </entry>
  <entry>
    <title>Automate tasks with docker-compose and systemd</title>
    <published>2020-05-22T15:57:00Z</published>
    <updated>2020-05-22T15:57:00Z</updated>
    <id>https://blog.dotcs.me/posts/automate-tasks-with-systemd</id>
    <link href="https://blog.dotcs.me/posts/automate-tasks-with-systemd"/>
    <summary>This article shows how tasks that have been encapsulated in Docker containers can be controlled by systemd timers. In case of an error a separate systemd service is triggered which sends a mail which contains the last view lines from the journal log.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/automate-tasks-with-systemd">&lt;p&gt;I have several tasks encapsulated in Docker containers which I need to trigger on a regular basis.
The scheduling can be done with systemd.
Both tools together have proven to be a quite powerful combination.&lt;/p&gt;
&lt;h2 id="example-backup-service"&gt;&lt;a class="markdownIt-Anchor" href="#example-backup-service"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Example: Backup service&lt;/h2&gt;
&lt;p&gt;Let's assume we have a backup service that lives in &lt;code&gt;/opt/backup&lt;/code&gt;.
This folder contains a &lt;code&gt;docker-compose.yaml&lt;/code&gt; file which contains the configuration of a backup service.&lt;/p&gt;
&lt;p&gt;We need one systemd unit for the service and one for the timer.
Both are shown below.
The setup is pretty straight forward.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# /etc/systemd/system/backup.service&lt;/span&gt;
&lt;span class="hljs-section"&gt;[Unit]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Description&lt;/span&gt;=Backup service
&lt;span class="hljs-attr"&gt;Requires&lt;/span&gt;=docker.service
&lt;span class="hljs-attr"&gt;After&lt;/span&gt;=docker.service

&lt;span class="hljs-comment"&gt;# Send mail in case of an error&lt;/span&gt;
&lt;span class="hljs-attr"&gt;OnFailure&lt;/span&gt;=status-email-user@%n.service

&lt;span class="hljs-section"&gt;[Service]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;WorkingDirectory&lt;/span&gt;=/opt/backup

&lt;span class="hljs-attr"&gt;ExecStart&lt;/span&gt;=/usr/local/bin/docker-compose up

&lt;span class="hljs-section"&gt;[Install]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;WantedBy&lt;/span&gt;=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# /etc/systemd/system/backup.timer&lt;/span&gt;
&lt;span class="hljs-section"&gt;[Unit]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Description&lt;/span&gt;=Timer for backup service
&lt;span class="hljs-attr"&gt;Requires&lt;/span&gt;=backup.service

&lt;span class="hljs-section"&gt;[Timer]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Unit&lt;/span&gt;=backup.service

&lt;span class="hljs-comment"&gt;# Time to wait after booting before we run first time&lt;/span&gt;
&lt;span class="hljs-attr"&gt;OnBootSec&lt;/span&gt;=&lt;span class="hljs-number"&gt;10&lt;/span&gt;min

&lt;span class="hljs-comment"&gt;# Define a calendar event (see `man systemd.time`)&lt;/span&gt;
&lt;span class="hljs-attr"&gt;OnCalendar&lt;/span&gt;=*-*-* &lt;span class="hljs-number"&gt;03&lt;/span&gt;:&lt;span class="hljs-number"&gt;00&lt;/span&gt;:&lt;span class="hljs-number"&gt;00&lt;/span&gt; Europe/Berlin

&lt;span class="hljs-section"&gt;[Install]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;WantedBy&lt;/span&gt;=multi-user.target
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The service unit has a &lt;code&gt;OnFailure&lt;/code&gt; hook which runs a separate systemd service in case the exit code of the service is non-zero.
This idea comes from the &lt;a href="https://wiki.archlinux.org/index.php/Systemd/Timers#As_a_cron_replacement"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Arch Wiki&lt;/a&gt;.&lt;br&gt;
The status email service can be configured in a generic way such that it can be re-used in other systemd services as well.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# /etc/systemd/system/status-email-user@.service&lt;/span&gt;
&lt;span class="hljs-section"&gt;[Unit]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Description&lt;/span&gt;=Status Email for %i to user

&lt;span class="hljs-section"&gt;[Service]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Type&lt;/span&gt;=&lt;span class="hljs-literal"&gt;on&lt;/span&gt;eshot
&lt;span class="hljs-attr"&gt;ExecStart&lt;/span&gt;=/usr/local/bin/systemd-email email@example.com %i
&lt;span class="hljs-attr"&gt;User&lt;/span&gt;=nobody
&lt;span class="hljs-attr"&gt;Group&lt;/span&gt;=systemd-journal
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;/usr/local/bin/systemd-email&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;#!/bin/sh&lt;/span&gt;

/usr/bin/mail -Ssendwait -t &amp;lt;&amp;lt;&lt;span class="hljs-string"&gt;ERRMAIL
To: $1
From: Monitor (systemd) &amp;lt;alert@example.tld&amp;gt;
Subject: $2
Content-Transfer-Encoding: 8bit
Content-Type: text/plain; charset=UTF-8

$(systemctl status --full &amp;quot;$2&amp;quot;)
ERRMAIL&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally the timer needs to be started/enabled as usual:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;systemctl start backup.timer
systemctl &lt;span class="hljs-built_in"&gt;enable&lt;/span&gt; backup.timer
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="show-logs"&gt;&lt;a class="markdownIt-Anchor" href="#show-logs"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Show logs&lt;/h2&gt;
&lt;p&gt;Logs can be read with &lt;code&gt;journalctl&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;journalctl -b -u backup.service
&lt;/code&gt;&lt;/pre&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Nextcloud: Migrate from SQLite to PostgreSQL</title>
    <published>2020-05-11T11:28:00Z</published>
    <updated>2020-05-11T11:28:00Z</updated>
    <id>https://blog.dotcs.me/posts/nextcloud-migrate-sqlite-to-postgesql</id>
    <link href="https://blog.dotcs.me/posts/nextcloud-migrate-sqlite-to-postgesql"/>
    <summary>This article describes how to migrate a Docker based Nextcloud instance from SQLite to PostgeSQL.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/nextcloud-migrate-sqlite-to-postgesql">&lt;p&gt;I recently changed the database in my personal Nextcloud instance.
Migrating databases is quite simple and took about an hour in my instance (~70GB of data).
I noticed a huge improvement in speed when using the PostgreSQL database – especially when it comes to concurrent access of multiple users.&lt;/p&gt;
&lt;p&gt;I'm running Nextcloud in a Docker container, so this post describes the procedure when using Docker containers.
It might be slightly different in cases where Nextcloud has been directly installed to the server.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;I have done the migration in Nextcloud 18.0.4. Older or newer versions might differ, so please be careful.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="step-0-preparations"&gt;&lt;a class="markdownIt-Anchor" href="#step-0-preparations"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 0: Preparations&lt;/h2&gt;
&lt;p&gt;Make sure to backup old data – especially the SQLite database file.
In case the migration will fail your original database will be left untouched, but still it's good to have a backup in case things are messed up accidentally.&lt;/p&gt;
&lt;h2 id="step-1-stop-routing-traffic-to-the-nextcloud-instance"&gt;&lt;a class="markdownIt-Anchor" href="#step-1-stop-routing-traffic-to-the-nextcloud-instance"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 1: Stop routing traffic to the Nextcloud instance&lt;/h2&gt;
&lt;p&gt;Make sure to stop any traffic to the instance.
In my case I disabled the rules in the reverse proxy which sits in front of my Nextcloud application, so that the instance does not receive any traffic and thus updates/modifications during the migration phase.&lt;/p&gt;
&lt;h2 id="step-2-add-database-to-docker-composeyaml"&gt;&lt;a class="markdownIt-Anchor" href="#step-2-add-database-to-docker-composeyaml"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 2: Add database to &lt;code&gt;docker-compose.yaml&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Let's first add a database to our &lt;code&gt;docker-compose.yaml&lt;/code&gt;.
The sample below shows the absolute minimum configuration to run a Nextcloud instance with a PostgreSQL database.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-attr"&gt;services:&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;nextcloud:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;image:&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;nextcloud:18.0.4&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;ports:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;8080:80&amp;quot;&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;volumes:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;./data:/var/www/html&lt;/span&gt;
  &lt;span class="hljs-attr"&gt;db:&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;image:&lt;/span&gt; &lt;span class="hljs-string"&gt;postgres:9.6&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;environment:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;POSTGRES_USER=ncuser&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;POSTGRES_PASSWORD=secret&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;POSTGRES_DB=ncdb&lt;/span&gt;
    &lt;span class="hljs-attr"&gt;volumes:&lt;/span&gt;
      &lt;span class="hljs-bullet"&gt;-&lt;/span&gt; &lt;span class="hljs-string"&gt;./db:/var/lib/postgresql/data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the &lt;code&gt;db&lt;/code&gt; service has been added make sure to start the database by running &lt;code&gt;docker-compose up&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id="step-3-run-migration"&gt;&lt;a class="markdownIt-Anchor" href="#step-3-run-migration"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 3: Run migration&lt;/h2&gt;
&lt;p&gt;Now comes the database migration.
Fortunately the Nextcloud developers provide a tool for that which can be used to essentially make this a one-liner.&lt;/p&gt;
&lt;p&gt;First connect to the running &lt;code&gt;nextcloud&lt;/code&gt; instance as user &lt;code&gt;www-data&lt;/code&gt;&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; docker-compose &lt;span class="hljs-built_in"&gt;exec&lt;/span&gt; -u www-data nextcloud bash&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;then run the database conversion script&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; ./occ db:convert-type --port 5432 --all-apps --clear-schema pgsql ncuser db ncdb&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The arguments &lt;code&gt;ncuser&lt;/code&gt; (database user) and &lt;code&gt;ncdb&lt;/code&gt; (database name) might differ depending on your setup.
The tool will interactively ask for your password and once it has access start the migration.
Both flags &lt;code&gt;--all-apps&lt;/code&gt; and &lt;code&gt;--clear-schema&lt;/code&gt; are optional.
The first one define if database tables of deactivated apps should be migrated or not.
The second one drops tables in the target database in case they have been existing before the migration.&lt;/p&gt;
&lt;p&gt;This might take a while.
For me it took under an hour for a Nextcloud instance that has a size of about 70GB.&lt;/p&gt;
&lt;h2 id="step-4-confirm-that-config-has-been-updated"&gt;&lt;a class="markdownIt-Anchor" href="#step-4-confirm-that-config-has-been-updated"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 4: Confirm that config has been updated&lt;/h2&gt;
&lt;p&gt;After the database has been migrated successfully check that the config file has been updated properly.
It should list &lt;code&gt;dbtype&lt;/code&gt; as &lt;code&gt;pgsql&lt;/code&gt; and have values for the properties &lt;code&gt;dbname&lt;/code&gt;, &lt;code&gt;dbhost&lt;/code&gt;, &lt;code&gt;dbuser&lt;/code&gt; and &lt;code&gt;dbpassword&lt;/code&gt;.
This can be checked with the following command:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; docker-compose &lt;span class="hljs-built_in"&gt;exec&lt;/span&gt; -u www-data nextcloud bash -c &lt;span class="hljs-string"&gt;&amp;quot;cat /var/www/html/config/config.php | grep db[a-z]&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Restart the service once so that the new config is read and applied.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; docker-compose restart&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="step-5-start-routing-traffic-to-the-nextcloud-instance"&gt;&lt;a class="markdownIt-Anchor" href="#step-5-start-routing-traffic-to-the-nextcloud-instance"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 5: Start routing traffic to the Nextcloud instance&lt;/h2&gt;
&lt;p&gt;You can now start to route traffic again to the instance.
Now the PostgreSQL database will be used instead of the SQLite database file.
Well done! :)&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="nextcloud"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Webcam Support in Arch Linux on Macbooks</title>
    <published>2020-04-01T18:07:00Z</published>
    <updated>2020-04-01T18:07:00Z</updated>
    <id>https://blog.dotcs.me/posts/macbook-webcam-arch</id>
    <link href="https://blog.dotcs.me/posts/macbook-webcam-arch"/>
    <summary>Learn how to install a kernel module that allows to use the Apple Facetime HD camera in your Arch or Manjaro Linux installation.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/macbook-webcam-arch">&lt;p&gt;I'm running &lt;a href="https://manjaro.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Manjaro Linux&lt;/a&gt; (an &lt;a href="https://www.archlinux.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Arch Linux&lt;/a&gt; derivative) on my MacbookPro12,1 (Retina, 13-inch, Early 2015).&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/macbook-pro.png" alt="Image source: support.apple.com, 2020/04/01" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Image source: support.apple.com, 2020/04/01&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;I was astonished how many things just worked out-of-the-box.
Kudos to the Arch and Manjaro teams for their great work!&lt;/p&gt;
&lt;p&gt;Unfortunately one thing that did not work out-of-the-box is the webcam.
It requires a driver that is not part of the official kernel but must be installed separately.&lt;br&gt;
The code is in the &lt;a href="https://aur.archlinux.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Arch User Repository (AUR)&lt;/a&gt;.
I use &lt;a href="https://github.com/Jguer/yay"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;yay&lt;/a&gt; to install packages from AUR which I can highly recommend and which I will use in this article.&lt;/p&gt;
&lt;p&gt;Let's get our hands dirty!&lt;/p&gt;
&lt;p&gt;First let's get information about all installed kernels and see if the headers are installed for all of them:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; pacman -Q | grep -E &lt;span class="hljs-string"&gt;&amp;#x27;linux[0-9]+&amp;#x27;&lt;/span&gt;  &lt;span class="hljs-comment"&gt;# see which kernels are installed&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;For each installed linux kernel there should be a &lt;code&gt;-headers&lt;/code&gt; file next to it, e.g.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;linux419 4.19.113-1
linux419-headers 4.19.113-1
linux54 5.4.28-1
linux54-headers 5.4.28-1
linux55 5.5.13-1
linux55-headers 5.5.13-1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the headers are missing, make sure to install them:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; yay -S linux-headers   &lt;span class="hljs-comment"&gt;# select the headers that are missing&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then let's get the &lt;a href="https://aur.archlinux.org/packages/bcwc-pcie-git/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;driver&lt;/a&gt; and install it from AUR:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; yay -S bcwc-pcie-git&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the installation we need to load the kernel module:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; modprobe facetimehd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You can test if the video source has been identified correctly by executing &lt;code&gt;v4l2-ctl --list-devices&lt;/code&gt;.
If everything worked out, the output should be:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;Apple Facetime HD (PCI:0000:02:00.0):
	/dev/video0
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If the output is instead &lt;code&gt;No /dev/video0 device&lt;/code&gt;, then make sure to unload the kernel module &lt;code&gt;bdc_pci&lt;/code&gt; as described in the &lt;a href="https://github.com/patjak/bcwc_pcie/wiki#known-issues"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;wiki&lt;/a&gt;.
This can be done by running the following commands:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;modprobe -r facetimehd  &lt;span class="hljs-comment"&gt;# temporary remove facetimehd module&lt;/span&gt;
modprobe -r bdc_pci     &lt;span class="hljs-comment"&gt;# remove disturbing kernel module&lt;/span&gt;

&lt;span class="hljs-comment"&gt;# make sure that the disturbing kernel module is blacklisted&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# and cannot be loaded as a dependency of another module.&lt;/span&gt;
&lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;blacklist bdc_pci\ninstall bdc_pci /bin/false&amp;quot;&lt;/span&gt; &amp;gt; /etc/modprobe.d/bcwc-pcie.conf

modprobe facetimehd     &lt;span class="hljs-comment"&gt;# load the facetimehd module again&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The webcam should work now.
You can test it on any website that requires a webcam, such as &lt;a href="https://meet.jit.si/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;meet.jit.si&lt;/a&gt;.
Have fun!&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>OfflineIMAP + systemd timer</title>
    <published>2020-04-01T14:38:00Z</published>
    <updated>2020-04-01T14:38:00Z</updated>
    <id>https://blog.dotcs.me/posts/offlineimap-systemd-timer</id>
    <link href="https://blog.dotcs.me/posts/offlineimap-systemd-timer"/>
    <summary>Learn how to setup a systemd timer that regularly fetches new mails from an IMAP server with OfflineIMAP.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/offlineimap-systemd-timer">&lt;p&gt;I use &lt;a href="http://www.mutt.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;mutt&lt;/a&gt; to manage my emails and access them from the terminal.
To sync the mails between the IMAP server and mutt I use &lt;a href="https://www.offlineimap.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;OfflineIMAP&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To regularly check for new mails I use a &lt;a href="https://www.freedesktop.org/software/systemd/man/systemd.timer.html"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;systemd timer&lt;/a&gt;.
The necessary files can be found &lt;a href="https://github.com/OfflineIMAP/offlineimap/tree/master/contrib/systemd"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;in the official repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let's clone the repository first and copy all necessary files to &lt;code&gt;/etc/systemd/user&lt;/code&gt;.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; /tmp
git &lt;span class="hljs-built_in"&gt;clone&lt;/span&gt; --depth 1 git@github.com:OfflineIMAP/offlineimap.git  &lt;span class="hljs-comment"&gt;# a shallow copy is enough&lt;/span&gt;
&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; offlineimap/contrib/systemd
sudo cp *.{service,timer} /etc/systemd/user/
systemctl --user daemon-reload
&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; /tmp &amp;amp;&amp;amp; rm -rf /tmp/offlineimap
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;By default the update interval of the timer is set to 15min.
I preferred an update interval of 5 min, which can be easily accomplished.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; systemctl --user edit offlineimap-oneshot.timer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this file values can be overwritten as such:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-section"&gt;[Timer]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;OnUnitInactiveSec&lt;/span&gt;=&lt;span class="hljs-number"&gt;5&lt;/span&gt;min
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally it's time to enable and start the timer.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; systemctl --user &lt;span class="hljs-built_in"&gt;enable&lt;/span&gt; offlineimap-oneshot.timer &lt;span class="hljs-comment"&gt;# autostart at boot&lt;/span&gt;&lt;/span&gt;
&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; systemctl --user start offlineimap-oneshot.timer  &lt;span class="hljs-comment"&gt;# start now&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's check if the timer is up and running.
&lt;code&gt;systemctl --user status offlineimap-oneshot.timer&lt;/code&gt; should show that it is up and running and tell how many time is left until the next run:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;● offlineimap-oneshot.timer - Offlineimap Query Timer
     Loaded: loaded (/usr/lib/systemd/user/offlineimap-oneshot.timer; enabled; vendor preset: enabled)
    Drop-In: /home/dotcs/.config/systemd/user/offlineimap-oneshot.timer.d
             └─override.conf
     Active: active (waiting) since Wed 2020-04-01 16:27:20 CEST; 8min ago
    Trigger: Wed 2020-04-01 16:37:34 CEST; 1min 55s left
   Triggers: ● offlineimap-oneshot.service
&lt;/code&gt;&lt;/pre&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>i3 + termite + custom themes = &lt;3</title>
    <published>2020-04-01T09:36:00Z</published>
    <updated>2020-04-01T09:36:00Z</updated>
    <id>https://blog.dotcs.me/posts/i3-termite-theme</id>
    <link href="https://blog.dotcs.me/posts/i3-termite-theme"/>
    <summary>i3 + termite is awesome. Having a simple theme changer is even better. Learn how to set up custom themes and apply them with a simple keystroke.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/i3-termite-theme">&lt;p&gt;I love the sun – especially in Spring when temperatures are not too hot and taking a sunbath is a real pleasure. And I love working in the sun, which is often a challenge because of the poor contrast - especially if, like me, you like to use dark themes.&lt;/p&gt;
&lt;p&gt;Since I recently changed to &lt;a href="https://i3wm.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;i3&lt;/a&gt; as my window manager, I use &lt;a href="https://github.com/thestinger/termite"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;termite&lt;/a&gt; as my default terminal which has by default no simple way to switch themes – at least not to my knowledge. But working in the sun requires a light theme, so I decided to work on a simple theme changer that is also integrated in i3.&lt;/p&gt;
&lt;p&gt;So let's get to work.&lt;/p&gt;
&lt;p&gt;First we need custom color themes. I have taken the two themes from &lt;a href="https://github.com/alpha-omega/termite-colors-solarized"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;this GitHub repository&lt;/a&gt; and put them in &lt;code&gt;~/.config/termite/themes&lt;/code&gt;.
Of course you can also define custom themes and put them there. The theme files should have the following format:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-section"&gt;[colors]&lt;/span&gt;
foreground = &lt;span class="hljs-comment"&gt;#000000&lt;/span&gt;
background = &lt;span class="hljs-comment"&gt;#ffffff&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# and so on&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then copy your current &lt;code&gt;~/.config/termite/config&lt;/code&gt; to &lt;code&gt;~/.config/termite/config.base&lt;/code&gt; and remove the &lt;code&gt;colors&lt;/code&gt; section.&lt;/p&gt;
&lt;p&gt;We will now need a way to append a theme file to the &lt;code&gt;config.base&lt;/code&gt; file. I've created a simple script that does this job:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;#!/usr/bin/env bash&lt;/span&gt;

USAGE=&lt;span class="hljs-string"&gt;&amp;quot;&lt;span class="hljs-variable"&gt;$0&lt;/span&gt; light|dark&amp;quot;&lt;/span&gt;

TERMITE_CONF_FOLDER=~/.config/termite
THEME=&lt;span class="hljs-variable"&gt;$1&lt;/span&gt;

&lt;span class="hljs-function"&gt;&lt;span class="hljs-title"&gt;change_theme&lt;/span&gt;&lt;/span&gt;() {
    &lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-string"&gt;&amp;quot;# THIS FILE HAS BEEN AUTOGENERATED. DO NOT CHANGE MANUALLY!&amp;quot;&lt;/span&gt; | \
        cat - /tmp/termite-switch-theme.hint &lt;span class="hljs-variable"&gt;$TERMITE_CONF_FOLDER&lt;/span&gt;/config.base &lt;span class="hljs-variable"&gt;$TERMITE_CONF_FOLDER&lt;/span&gt;/themes/&lt;span class="hljs-variable"&gt;$1&lt;/span&gt; &amp;gt; &lt;span class="hljs-variable"&gt;$TERMITE_CONF_FOLDER&lt;/span&gt;/config
    killall -USR1 termite || &lt;span class="hljs-literal"&gt;true&lt;/span&gt;
}

&lt;span class="hljs-keyword"&gt;case&lt;/span&gt; &lt;span class="hljs-variable"&gt;$THEME&lt;/span&gt; &lt;span class="hljs-keyword"&gt;in&lt;/span&gt;
    light|solarized-light) change_theme solarized-light ;;
    dark|solarized-dark) change_theme solarized-dark ;;
    *) &lt;span class="hljs-built_in"&gt;echo&lt;/span&gt; &lt;span class="hljs-variable"&gt;$USAGE&lt;/span&gt; ;;
&lt;span class="hljs-keyword"&gt;esac&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Put this script somewhere and make it executable. In my case I've saved the file in &lt;code&gt;~/.local/scripts/termite-switch-theme.sh&lt;/code&gt;. You should be able to switch themes now by calling the script, e.g. &lt;code&gt;~/.local/scripts/termite-switch-theme.sh&lt;/code&gt; light.&lt;/p&gt;
&lt;p&gt;This is already great, but for a perfect integration with i3 put these lines into your &lt;code&gt;~/.config/i3/config&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;set $mode_term_theme Terminal theme (l) light, (d) dark
mode &amp;quot;$mode_term_theme&amp;quot; {
    # choose which theme should be applied by pressing l (light) or d (dark)
    bindsym l exec --no-startup-id &amp;quot;~/.local/scripts/termite-switch-theme.sh light&amp;quot;, mode &amp;quot;default&amp;quot;
    bindsym d exec --no-startup-id &amp;quot;~/.local/scripts/termite-switch-theme.sh dark&amp;quot;, mode &amp;quot;default&amp;quot;
    # back to normal: Enter or Escape
    bindsym Return mode &amp;quot;default&amp;quot;
    bindsym Escape mode &amp;quot;default&amp;quot;
}
bindsym $mod+t mode &amp;quot;$mode_term_theme&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally reload the i3 config with &lt;code&gt;mod+shift+c&lt;/code&gt;. Now you can change themes with &lt;code&gt;mod+t&lt;/code&gt; and choose which theme should be applied (light or dark). All running terminals will immediately reflect your theme changes. Have fun!&lt;/p&gt;
&lt;h2 id="base16-themes"&gt;&lt;a class="markdownIt-Anchor" href="#base16-themes"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Base16 Themes&lt;/h2&gt;
&lt;p&gt;If you want to have even more themes to choose from take a look at the wonderful repository &lt;a href="https://github.com/khamer/base16-termite"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;khamer/base16-termite&lt;/a&gt;. Those themes are working out-of-the-box just as described above.&lt;/p&gt;
&lt;h2 id="acknowledgements"&gt;&lt;a class="markdownIt-Anchor" href="#acknowledgements"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Acknowledgements&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;Thanks to &lt;a href="https://github.com/BarbUk"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;@BarbUk&lt;/a&gt; for &lt;a href="https://github.com/thestinger/termite/issues/730"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;helping me find the correct signal&lt;/a&gt; to send to running termite instances.&lt;/li&gt;
&lt;li&gt;Thanks to &lt;a href="https://nils-braun.github.io/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;@nils-braun&lt;/a&gt; for reviewing this article.&lt;/li&gt;
&lt;/ul&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Spotify Desktop App with HiDPI Scaling in GNOME 3</title>
    <published>2019-11-12T16:45:00Z</published>
    <updated>2019-11-12T16:45:00Z</updated>
    <id>https://blog.dotcs.me/posts/spotify-desktop-high-dpi-scaling</id>
    <link href="https://blog.dotcs.me/posts/spotify-desktop-high-dpi-scaling"/>
    <summary>Spotify Desktop seems to have issues with HiDPI screens in Linux. This post shows how to fix it.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/spotify-desktop-high-dpi-scaling">&lt;p&gt;When using a HiDPI screen, such as the MacBook Pro with Retina display, Spotify Desktop may not automatically detect the correct scaling factor.
I have noticed issues in my Manjaro installation that runs with GNOME 3 Desktop.
Spotify has been installed from the AUR package repository.&lt;br&gt;
The wrong scaling factor leads to very small font size that is hard to read.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; spotify --version&lt;/span&gt;
Spotify version 1.1.10.546.ge08ef575, Copyright (c) 2019, Spotify Ltd
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Fortunately there is an option to force Spotify Desktop to use a (hardcoded) scaling factor.
Use the following command and vary the scaling factor in order to find the correct factor first:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; spotify --force-device-scale-factor=2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;After the correct value has been found, adjust the desktop entry that GNOME uses to when starting the application via its launcher.
The file lives in &lt;code&gt;/usr/share/applications/spotify.desktop&lt;/code&gt;:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-section"&gt;[Desktop Entry]&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Type&lt;/span&gt;=Application
&lt;span class="hljs-attr"&gt;Name&lt;/span&gt;=Spotify
&lt;span class="hljs-attr"&gt;GenericName&lt;/span&gt;=Music Player
&lt;span class="hljs-attr"&gt;Icon&lt;/span&gt;=spotify-client
&lt;span class="hljs-attr"&gt;TryExec&lt;/span&gt;=spotify
&lt;span class="hljs-attr"&gt;Exec&lt;/span&gt;=spotify --force-device-scale-factor=&lt;span class="hljs-number"&gt;2&lt;/span&gt; %U
&lt;span class="hljs-attr"&gt;Terminal&lt;/span&gt;=&lt;span class="hljs-literal"&gt;false&lt;/span&gt;
&lt;span class="hljs-attr"&gt;MimeType&lt;/span&gt;=x-scheme-handler/spotify&lt;span class="hljs-comment"&gt;;&lt;/span&gt;
&lt;span class="hljs-attr"&gt;Categories&lt;/span&gt;=Audio&lt;span class="hljs-comment"&gt;;Music;Player;AudioVideo;&lt;/span&gt;
&lt;span class="hljs-attr"&gt;StartupWMClass&lt;/span&gt;=spotify
&lt;/code&gt;&lt;/pre&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>An "open" command in Linux</title>
    <published>2019-11-11T21:03:00Z</published>
    <updated>2019-11-11T21:03:00Z</updated>
    <id>https://blog.dotcs.me/posts/open-command-in-linux</id>
    <link href="https://blog.dotcs.me/posts/open-command-in-linux"/>
    <summary>This article explains how to configure a command "open" in Linux that behaves similar to the command that macOS provides.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/open-command-in-linux">&lt;p&gt;As a previous macOS user I am used to call &lt;code&gt;open &amp;lt;anything&amp;gt;&lt;/code&gt; in a terminal to open the current file or folder with the standard tool.
So &lt;code&gt;open .&lt;/code&gt; would open the current folder while &lt;code&gt;open file.pdf&lt;/code&gt; would open the PDF in the default PDF viewer.&lt;/p&gt;
&lt;p&gt;Today I learned that &lt;code&gt;xdg-open&lt;/code&gt; serves the same purpose on the Linux desktop. So I configured an alias, to have the same behavior on both systems:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-built_in"&gt;alias&lt;/span&gt; open=&lt;span class="hljs-string"&gt;&amp;quot;xdg-open &amp;amp;&amp;gt;/dev/null&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="query-and-set-defaults"&gt;&lt;a class="markdownIt-Anchor" href="#query-and-set-defaults"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Query and set defaults&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;xdg-open&lt;/code&gt; is part of &lt;a href="https://wiki.archlinux.org/index.php/Xdg-utils"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;&lt;code&gt;xdg-utils&lt;/code&gt;&lt;/a&gt;, which &lt;a href="https://freedesktop.org/wiki/Software/xdg-utils/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;describes itself&lt;/a&gt; as&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;xdg-utils is a set of tools that allows applications to easily integrate with the desktop environment of the user, regardless of the specific desktop environment that the user runs.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's super convenient to change the default application for a given file, say a PDF.
Let's change the default program:&lt;/p&gt;
&lt;p&gt;First let's assume we don't know the mime-type of the PDF, which is &lt;code&gt;application/pdf&lt;/code&gt;.
We can find this type by query all known filetypes:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; xdg-mime query filetype /path/to/a/file.pdf &lt;/span&gt;
application/pdf
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's see which default application is currently configured for this filetype:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; xdg-mime query default application/pdf&lt;/span&gt;
chromium.desktop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If we don't want to use chromium to read PDFs, but for example &lt;a href="https://pwmt.org/projects/zathura/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;zathura&lt;/a&gt; we can change the default application as follows:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;$&lt;/span&gt;&lt;span class="bash"&gt; find /usr/share/applications/ -name &lt;span class="hljs-string"&gt;&amp;#x27;*zathura*&amp;#x27;&lt;/span&gt;&lt;/span&gt;
/usr/share/applications/org.pwmt.zathura-pdf-mupdf.desktop
/usr/share/applications/org.pwmt.zathura.desktop
&lt;span class="hljs-meta"&gt;
$&lt;/span&gt;&lt;span class="bash"&gt; xdg-mime default org.pwmt.zathura.desktop application/pdf&lt;/span&gt;
&lt;span class="hljs-meta"&gt;
$&lt;/span&gt;&lt;span class="bash"&gt; xdg-mime query default application/pdf&lt;/span&gt;
org.pwmt.zathura.desktop
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now PDF files are opened with zathura when using our new &lt;code&gt;open&lt;/code&gt; command as defined above.&lt;/p&gt;
&lt;p&gt;A lot programs bring their own &lt;code&gt;.desktop&lt;/code&gt; file(s). Files are listed in &lt;code&gt;/usr/share/appliations&lt;/code&gt; and can be overridden or suplemented with files located in &lt;code&gt;~/.local/share/applications&lt;/code&gt;. The specification of those files &lt;a href="https://specifications.freedesktop.org/desktop-entry-spec/desktop-entry-spec-latest.html#recognized-keys"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;can be found here&lt;/a&gt;.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>GNOME 3: Change wallpaper periodically</title>
    <published>2019-11-10T18:52:00Z</published>
    <updated>2019-11-10T18:52:00Z</updated>
    <id>https://blog.dotcs.me/posts/gnome3-change-wallpapers-automatically</id>
    <link href="https://blog.dotcs.me/posts/gnome3-change-wallpapers-automatically"/>
    <summary>This post describes how wallpapers can be automatically changed in GNOME 3 with a simple script and a cronjob.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/gnome3-change-wallpapers-automatically">&lt;p&gt;If you are using GNOME 3 and you want to periodically change your desktop background using images in a folder, this is for you.&lt;/p&gt;
&lt;p&gt;Put this script somewhere and make it executable.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-meta"&gt;#!/usr/bin/env bash&lt;/span&gt;

&lt;span class="hljs-comment"&gt;# You need to adjust this path&lt;/span&gt;
WALLPAPER_FOLDER=/path/to/your/wallpaper/folder

&lt;span class="hljs-comment"&gt;# https://askubuntu.com/a/1073769/277761&lt;/span&gt;
PID=$(pgrep gnome-session | tail -n1)
&lt;span class="hljs-built_in"&gt;export&lt;/span&gt; DBUS_SESSION_BUS_ADDRESS=$(grep -z DBUS_SESSION_BUS_ADDRESS /proc/&lt;span class="hljs-variable"&gt;$PID&lt;/span&gt;/environ|cut -d= -f2-)

FILE_PATH=`ls &lt;span class="hljs-variable"&gt;$WALLPAPER_FOLDER&lt;/span&gt; | shuf -n 1`
FILE_URI=&lt;span class="hljs-string"&gt;&amp;quot;file://&lt;span class="hljs-variable"&gt;$WALLPAPER_FOLDER&lt;/span&gt;/&lt;span class="hljs-variable"&gt;$FILE_PATH&lt;/span&gt;&amp;quot;&lt;/span&gt;
/usr/bin/gsettings &lt;span class="hljs-built_in"&gt;set&lt;/span&gt; org.gnome.desktop.background picture-uri &lt;span class="hljs-variable"&gt;$FILE_URI&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then create a cronjob that executes this script periodically, e.g. by using this line:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;*/10 * * * * /path/to/your/file.sh
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And you're done. Have fun!&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="linux"/>
    <category term="notes"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Leaving Google's Mail Services behind</title>
    <published>2019-11-01T13:55:00Z</published>
    <updated>2019-11-01T13:55:00Z</updated>
    <id>https://blog.dotcs.me/posts/leaving-gmail-behind</id>
    <link href="https://blog.dotcs.me/posts/leaving-gmail-behind"/>
    <summary>I decided to leave Google's e-mail services behind. This post describes how I changed to another provider and how I use my domain at the same time so that I can switch between e-mail providers more flexibly in the future.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/leaving-gmail-behind">&lt;p&gt;I decided to leave Google's e-mail services behind. This post describes how I changed to another provider and how I use my domain at the same time so that I can switch between e-mail providers more flexibly in the future.&lt;/p&gt;
&lt;p&gt;A year ago I decided to leave Google and their e-mail services behind. The reason that lead to this decision was that Google decided to kill Inbox - my absolute favorite e-mail service. They did it although many people loved Inbox and after operating the service for about 4 years.&lt;br&gt;
Inbox was simply fabulous. It had a clean web based UI, came with powerful features, such as auto-grouping related e-mails into a thread, marking e-mails as done, so that one could establish a zero-inbox pattern, a very well working Android app, and so on. Thinking about the shutdown of Google Inbox still makes me sad. Google killed the better of their e-mail services and forced users migrate back to GMail, the much more bloated and annoying variant of the two services. In my opinion Google should have killed GMail instead and migrate their users to Inbox. But they have decided differently any many people followed their decision - many, but not me.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Since last year I have had quite a journey to switch e-mail providers. I decided to never use an e-mail address again that is not tied so a domain I do own – simply because it's a mess to change the e-mail address for hundreds of accounts.&lt;br&gt;
Let me explain my steps to migrate to another e-mail service.&lt;/p&gt;
&lt;h2 id="step-1-choose-an-alternative"&gt;&lt;a class="markdownIt-Anchor" href="#step-1-choose-an-alternative"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 1: Choose an alternative&lt;/h2&gt;
&lt;p&gt;There are countless alternatives on the market – many of those belong to larger companies, such as Microsoft.
I did not want to move from one web-giant to another, so I decided to use a smaller provider.
First I thought about hosting e-mails on my own server, but I quickly got rid of this idea because I cannot guarantee uptime and hosting an own e-mail server comes with its own difficulties.&lt;br&gt;
And I wanted a mail provider that does cost money.
Why you ask? Because hosting e-mails and providing a proper e-mail service is serious business.
E-mails contain a lot of personal and sensitive information.
If the service is free then users are the product – Inbox was no exception to this, GMail is neither.
In my opinion it is worthwhile to think very carefully about this topic, because most mails are not encrypted, often they contain sensitive information, such as information about accounts, bank credits, shopping information that helps to create an exact profile of the user.
E-mails are a paradise for data scientists.&lt;/p&gt;
&lt;p&gt;What I needed instead was an e-mail provider that fulfills these requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Reliability&lt;/strong&gt;:
The service should have a history in which it worked flawlessly.
No profile building, no ads: I want to have a service that does not scan my e-mails (automatically) and display ads based on the content.
I don't want to see any ads at all.
I do not want the e-mail provider to use my data for a kind of profile which is then sold to third parties – even if this is only done via an advertising network.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Secure authentication&lt;/strong&gt;:
I want the service to provide a two-factor authentication.
This way an attacker does not only need my username and password combination, but also needs to have physical access to my second device.
This should be standard nowadays.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Custom domain&lt;/strong&gt;:
I want to use a custom domain, so the mail provider must provide support for e-mail aliases.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GDPR compliance&lt;/strong&gt;:
Since we have this wonderful EU General Data Protection Regulation (GDPR) in place, I want the service to be hosted in such a way that it is compliant with GDPR.
Actually most services are compliant, but I favor services that are also hosted in the EU.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the end I came up with two providers that looked very interesting: &lt;a href="https://mailbox.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;mailbox.org&lt;/a&gt; and &lt;a href="https://posteo.de/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;posteo.de&lt;/a&gt;.
In my opinion both providers are absolutely worth their money.
One of the main differences is how custom domains are handled.
While Posteo does not allow for custom domains by default, Mailbox is fine with it.
Posteo has a good point of not allowing it.
Their FAQ explains why they have decided against custom domains:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Can I use Posteo with my own domains?&lt;/p&gt;
&lt;p&gt;No. We are an email provider with a particular, privacy-oriented model – and this is not compatible with incorporating own domains. [...]
Even if only the MX record pointed to us, we would still need to store the assignment of the domain in your Posteo account as  user information.
Thus we would possess your user information and be required to give it out.
For this reason, we have decided not  to offer this possibility and instead to use data economy. [...]
In order to be able to read replies to these messages, you need to set up forwarding to Posteo for the external address.&lt;/p&gt;
&lt;p&gt;Source: https://posteo.de/en/site/faq&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;A year ago I have decided to use Posteo.
Nowadays I think it would have been better to go with Mailbox, because of the custom domain problem.
I am not so sure if Mailbox allowed setting up custom domains a year ago, but since Mailbox &lt;a href="https://kb.mailbox.org/display/BMBOKBEN/Using+e-mail+addresses+of+your+domain"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;natively allows for custom domains nowadays&lt;/a&gt;, I guess it makes much more sense to use their offering.
I will outline below why this is important.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/posteo.png" alt="Image of the posteo landing page" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Image of the posteo landing page&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Creating an account is straight-forward, I will not explain how to do that.
Maybe it is worth mentioning that Posteo does provide a large amount of anonymity.
It does allow to create new accounts without the need to enter any personal details.
Also the payment does work anonymously.&lt;/p&gt;
&lt;h2 id="step-2-setup-with-custom-domain"&gt;&lt;a class="markdownIt-Anchor" href="#step-2-setup-with-custom-domain"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 2: Setup with custom domain&lt;/h2&gt;
&lt;p&gt;As mentioned in the requirements I want to use a custom domain for my mails.
Doing so has several advantages and one major disadvantage.&lt;br&gt;
Advantages are that you can have as many e-mail addresses as you like.
&lt;any-name&gt;@yourdomain.com is possible, so you have a lot of freedom here.
Another advantage is that those mail addresses are independent of the mail provider, which means that I can, at some point in time, if I am no longer satisfied with Posteo, simply change the e-mail provider and do not have to inform others that my mail address has changed.
Isn't that great?! I mean changing e-mail addresses is a tedious task – it took me several days to update my e-mail address in all the services that I use.&lt;br&gt;
Instead of changing mail addresses one could also forward e-mails from one provider to the other.
Although it would be simple and can be configured in most e-mail services, including GMail.
Why? Because it would still route mails via Google – all my mails.
And Google will for sure analyze those mails.
So I would not have won anything.
Instead I take the burden once in my life, change my mail address and then stick to it.
This is possible – but only if the domain is yours.
Which brings me to the major disadvantage: You have to own this domain.
Forever – or at least as long as somebody sends mail to this domain that should be kept private.
&lt;strong&gt;You must make sure, that this domain never gets lost.&lt;/strong&gt;
Most domain providers do provide options here, so please check carefully.
If you lose the domain, it could be registered by someone else who sets their own MX records and receives mails intended only for your eyes.
So be warned!&lt;/p&gt;
&lt;h2 id="step-3-set-up-e-mail-forwarding"&gt;&lt;a class="markdownIt-Anchor" href="#step-3-set-up-e-mail-forwarding"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 3: Set up e-mail forwarding&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;This step is only needed if the mail provider does not allow for custom domains (e.g. Posteo). In case the mail provider does allow for custom domains (e.g. Mailbox) this step is not necessary.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I have bought my domain at &lt;a href="https://godaddy.com/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;GoDaddy&lt;/a&gt;, one of the very large domain registrars out there.
GoDaddy offers up to 100 e-mail forwarding configurations per domain for free.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/godaddy-email-forwarding.png" alt="GoDaddy offers 100 e-mail forwarding configurations for free." class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;GoDaddy offers 100 e-mail forwarding configurations for free.&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Configuration is simple.
Just configure the e-mail address, that the service should listen to, e.g. foobar@domain.tld and enter the mail address that it should be forwarded to, e.g. mymailaddress@posteo.de.
This way you can configure up to 100 different e-mail addresses that can later be used as aliases for your one e-mail address at Posteo.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/godaddy-email-forwarding-2.png" alt="Configuration dialog at GoDaddy" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Configuration dialog at GoDaddy&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;DNS records are typically managed by the registrar itself – in my case GoDaddy.
I manage my DNS records via Cloudflare, but this is absolutely not necessary.
The following is still valid for GoDaddy or any other service that is used to manage DNS entries.
The UI might look different though.&lt;/p&gt;
&lt;h2 id="step-4-set-up-mx-spf-and-dmarc-records"&gt;&lt;a class="markdownIt-Anchor" href="#step-4-set-up-mx-spf-and-dmarc-records"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 4: Set up MX, SPF and DMARC records&lt;/h2&gt;
&lt;p&gt;The missing pieces are now setting up &lt;a href="https://en.wikipedia.org/wiki/MX_record"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;MX records&lt;/a&gt;, &lt;a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;SPF&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/DMARC"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;DMARC&lt;/a&gt;.
The first one is necessary so that other e-mail servers know which inbox server they should use to deliver the messages to.
SPF and DMARC are necessary, so that sent mails can pass the SPAM rules of various mail providers.&lt;/p&gt;
&lt;p&gt;Let's talk about the MX records first.
Each e-mail provider does have some servers that are responsible for receiving mails from other services.
Since Posteo does not allow for custom domains I have to use the inbox servers from GoDaddy and forward mails to Posteo.
If you go with Mailbox, please enter the Mailbox MX records instead.&lt;/p&gt;
&lt;p&gt;To find out GoDaddy's MX records go to the Workspace Control Center, this is where you can configure your e-mail records for your domain.
Then go to Tools → Server Settings.
Here you can see the MX records and their priorities that are to be set up.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/godaddy-mailserver-mx.png" alt="MX records as needed to work with GoDaddy." class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;MX records as needed to work with GoDaddy.&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Finally use these MX records to change the DNS records of your domain:&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/godaddy-mailserver-settings.png" alt="DNS settings: MX records and SPF and DMARC settings." class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;DNS settings: MX records and SPF and DMARC settings.&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;The image above also shows the &lt;a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;SPF&lt;/a&gt; record, which I set to &lt;code&gt;v=spf1 include:posteo.de -all&lt;/code&gt;.
We basically inherit the SPF record of posteo.de and advice that other mail providers should strictly reject messages that do not pass the SPF test (&lt;code&gt;-all&lt;/code&gt; flag).&lt;br&gt;
In case you are interested in SPF record configuration and want to learn more about SPF records, I can recommend &lt;a href="https://www.spfwizard.net/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;spfwizard.net&lt;/a&gt;.
Posteo recommends at least to have the &lt;code&gt;include:posteo.de&lt;/code&gt; part, their recommendation can be found &lt;a href="https://posteo.de/site/postmaster"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/spf-generator.png" alt="Configuration as shown by spfwizard.net." class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Configuration as shown by spfwizard.net.&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Please note that if you use Mailbox, you might want to use the SPF record &lt;code&gt;v=spf1 include:mailbox.org&lt;/code&gt; as described in their help pages.&lt;/p&gt;
&lt;p&gt;I also configured &lt;a href="https://en.wikipedia.org/wiki/DMARC"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;DMARC&lt;/a&gt; with the value &lt;code&gt;v=DMARC1; p=quarantine; rua=mailto:DMARC-report@dotcs.me; ruf=mailto:DMARC-forensic@dotcs.me; pct=100&lt;/code&gt;.
A generator for the DMARC format can be found &lt;a href="https://mxtoolbox.com/DMARCRecordGenerator.aspx"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I have not configured DKIM, because to my knowledge it is not possible in combination with Posteo as the mail provider.&lt;/p&gt;
&lt;h2 id="step-5-final-checks"&gt;&lt;a class="markdownIt-Anchor" href="#step-5-final-checks"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Step 5: Final checks&lt;/h2&gt;
&lt;p&gt;Last but not least, I want to make sure that our mails are delivered properly.
What I found quite useful is to send a mail to GMail and check the mail headers there.
The good news first: mails are delivered properly.
Neither are they rejected, nor marked as SPAM.
Our configuration seems to work – strike! Let's check in more detail if SPF and DMARC worked as expected.&lt;/p&gt;
&lt;p&gt;The relevant part of the header is this one:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;Authentication-Results: mx.google.com;
    spf=pass (google.com: domain of fantasymail@dotcs.me designates 185.67.36.142 as permitted sender) smtp.mailfrom=fantasymail@dotcs.me;
    dmarc=pass (p=QUARANTINE sp=QUARANTINE dis=NONE) header.from=example.com
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We see that both SPF as well as DMARC tests passed. I have used that setup for quite some time now and I had never experienced any issues so far.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Evaluate data from Komoot with Elasticsearch and Kibana</title>
    <published>2017-06-11T12:00:00Z</published>
    <updated>2017-06-11T12:00:00Z</updated>
    <id>https://blog.dotcs.me/posts/komoot-analysis-with-kibana</id>
    <link href="https://blog.dotcs.me/posts/komoot-analysis-with-kibana"/>
    <summary>In my spare time I like to travel by bike. As a data enthusiast it's no question to track my trips using services such as Komoot. Besides the data analysis methods that these companies provide on their websites or apps, they are always limited in what kind of analysis they provide to their customers. In this post I discuss how to extract data from Komoot and evaluate the data with Elasticsearch.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/komoot-analysis-with-kibana">&lt;p&gt;In my spare time I like to travel by bike.
As a data enthusiast it's no question to track my trips using services such as &lt;a href="https://www.komoot.de/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Komoot&lt;/a&gt;.
Besides the data analysis methods that these companies provide on their websites or apps, they are always limited in what kind of analysis they provide to their customers.
Most of the time it is not enough tough.
Having access to the raw data allows to do further analysis and implement highly customized charts.&lt;/p&gt;
&lt;h2 id="table-of-content"&gt;&lt;a class="markdownIt-Anchor" href="#table-of-content"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Table of content&lt;/h2&gt;
&lt;p&gt;For convenience this post is divided in five sections:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="#preconditions"&gt;Preconditons&lt;/a&gt;&lt;br&gt;
In this section the necessary tools are explained and installed on your developer machine.&lt;/li&gt;
&lt;li&gt;&lt;a href="#data-mining"&gt;Data mining&lt;/a&gt;&lt;br&gt;
In this section the raw data is accessed which will be further analyzed in later steps.
No advanced techniques are used for data mining as the focus in this post lies on the next two steps.&lt;/li&gt;
&lt;li&gt;&lt;a href="#data-preprocessing"&gt;Data preprocessing&lt;/a&gt;&lt;br&gt;
Using the raw data it's possible to pre-process them in order to simplify the data analysis in the next step.
In this step the data will also be imported into the database.&lt;/li&gt;
&lt;li&gt;&lt;a href="#data-analysis-and-visualization"&gt;Data analysis and visualization&lt;/a&gt;&lt;br&gt;
Custom analysis and charts are implemented in this section.&lt;/li&gt;
&lt;li&gt;&lt;a href="#summary-and-outlook"&gt;Sumary and outlook&lt;/a&gt;&lt;br&gt;
Having all the data available in Elasticsearch it's possible to do a lot more.
This outlook will give a few more ideas what to do next.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Let's start!&lt;/p&gt;
&lt;h2 id="preconditions"&gt;&lt;a class="markdownIt-Anchor" href="#preconditions"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Preconditions&lt;/h2&gt;
&lt;p&gt;To analyze the data the so called &lt;a href="https://www.elastic.co/de/webinars/introduction-elk-stack"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;ELK-Stack or Elastic Stack&lt;/a&gt; (Elasticsearch, Logstash, Kibana) is used.&lt;/p&gt;
&lt;p&gt;An instance of Elasticsearch and Kibana is required to run on the machine in order to analyze the data later on.
To simplify the setup process it's best to not install them directly on the development machine but use &lt;a href="https://www.docker.com/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;docker&lt;/a&gt; to manage the tools in a virtual environment.
By doing so environments are separated, which means that it doesn't matter what operating system your working machine runs - Windows, macOS or Linux - and what versions of tools you have installed.
The containers are isolated and describe their own system and dependencies.
In case you haven't used docker yet, make sure to &lt;a href="https://www.docker.com/community-edition"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;install docker first&lt;/a&gt;.
To get started quickly I have created a &lt;a href="https://github.com/dotcs/komoot-elk-jupyter"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;dockerized environment&lt;/a&gt; that can be used directly.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# clone the Docker Elastic Stack description into the folder named komoot-analysis&lt;/span&gt;
git &lt;span class="hljs-built_in"&gt;clone&lt;/span&gt; https://github.com/dotcs/komoot-elk-jupyter.git

&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; komoot-elk-jupyter    &lt;span class="hljs-comment"&gt;# change to this directory&lt;/span&gt;
docker-compose up        &lt;span class="hljs-comment"&gt;# spin up the ELK stack and jupyter&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;code&gt;docker-compose&lt;/code&gt; will spin up a network in which one instance of each, Elasticsearch, Logstash, Kibana and Jupyter, is running.
They share the same network configuration so that they can talk to each other.&lt;/p&gt;
&lt;p&gt;If everything went fine the following links should work on your developer machine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Kibana: &lt;a href="http://localhost:5601"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;http://localhost:5601&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Jupyter: &lt;a href="http://localhost:8888"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;http://localhost:8888&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;small&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: On a Mac Docker runs inside VirtualBox.
Typically the IP of this machine is not bound to localhost, so the links might not work.
Make sure to use the correct one by running &lt;code&gt;docker-machine ip default&lt;/code&gt;, where &lt;code&gt;default&lt;/code&gt; is the name of the machine in my case.&lt;/p&gt;
&lt;/small&gt;
&lt;p&gt;Alright, we've finished the first step in which we spun up a complex development environment that allows for intensive data analysis.
Now everything is set up to continue gathering the data.
Let's move on!&lt;/p&gt;
&lt;h2 id="data-mining"&gt;&lt;a class="markdownIt-Anchor" href="#data-mining"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Data mining&lt;/h2&gt;
&lt;p&gt;While Komoot provides an &lt;a href="https://static.komoot.de/doc/api/stable/v007/latest/index.html"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;experimental API&lt;/a&gt;, it seems impossible to create an application token without participating in some kind of beta program.
&lt;a href="https://github.com/komoot/komoot-oauth2-connect-example/issues/2"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;My issue&lt;/a&gt; in the corresponding github project has not been answered yet, so a workaround is necessary to get access to the data.&lt;/p&gt;
&lt;p&gt;Fortunately it's easy to call the official API endpoints from the browser when logged in into their web application, because it uses the very same API.
For the necessary API call you must be aware of your personal &lt;code&gt;USER_ID&lt;/code&gt;.
It can be extracted from the link to the profile in the web application, which has the following schema: &lt;code&gt;https://www.komoot.de/user/{USER_ID}&lt;/code&gt;.
In my case it is &lt;code&gt;320477127324&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;In order to fetch your latest data in JSON format, use the following URL: &lt;code&gt;https://www.komoot.de/api/v007/users/{USER_ID}/tours/&lt;/code&gt;.
Save the content of this file to &lt;code&gt;jupyter-notebooks/data/komoot.json&lt;/code&gt;, it will be needed in the next step.&lt;/p&gt;
&lt;h3 id="data-sample"&gt;&lt;a class="markdownIt-Anchor" href="#data-sample"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Data sample&lt;/h3&gt;
&lt;p&gt;A single entry does look like this.
&lt;em&gt;Note that some information was omitted to save space and it's not used in this blog post anyway.&lt;/em&gt;&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;{
  _embedded: {
    tours: [
      {
        status: &lt;span class="hljs-string"&gt;&amp;quot;public&amp;quot;&lt;/span&gt;,
        type: &lt;span class="hljs-string"&gt;&amp;quot;tour_recorded&amp;quot;&lt;/span&gt;,
        date: &lt;span class="hljs-string"&gt;&amp;quot;2017-06-03T14:14:37.000+02:00&amp;quot;&lt;/span&gt;,
        name: &lt;span class="hljs-string"&gt;&amp;quot;Runde an der Würm entlang&amp;quot;&lt;/span&gt;,
        distance: &lt;span class="hljs-number"&gt;42980.88913041058&lt;/span&gt;,
        duration: &lt;span class="hljs-number"&gt;7660&lt;/span&gt;,
        sport: &lt;span class="hljs-string"&gt;&amp;quot;touringbicycle&amp;quot;&lt;/span&gt;,
        _links: {
          creator: { href: &lt;span class="hljs-string"&gt;&amp;quot;http://api.komoot.de/v007/users/320477127324/profile_embedded&amp;quot;&lt;/span&gt; },
          self: { href: &lt;span class="hljs-string"&gt;&amp;quot;http://api.komoot.de/v007/tours/17503590?_embedded=&amp;quot;&lt;/span&gt; },
          coordinates: { href: &lt;span class="hljs-string"&gt;&amp;quot;http://api.komoot.de/v007/tours/17503590/coordinates&amp;quot;&lt;/span&gt; }
        },
        id: &lt;span class="hljs-number"&gt;17503590&lt;/span&gt;,
        changed_at: &lt;span class="hljs-string"&gt;&amp;quot;2017-06-03T15:06:51.000Z&amp;quot;&lt;/span&gt;,
        kcal_active: &lt;span class="hljs-number"&gt;880&lt;/span&gt;,
        kcal_resting: &lt;span class="hljs-number"&gt;154&lt;/span&gt;,
        start_point: {
          lat: &lt;span class="hljs-number"&gt;48.795956&lt;/span&gt;,
          lng: &lt;span class="hljs-number"&gt;8.85&lt;/span&gt;,
          alt: &lt;span class="hljs-number"&gt;426&lt;/span&gt;
        },
        elevation_up: &lt;span class="hljs-number"&gt;548.1543636580948&lt;/span&gt;,
        elevation_down: &lt;span class="hljs-number"&gt;544.0455472560726&lt;/span&gt;,
        time_in_motion: &lt;span class="hljs-number"&gt;7236&lt;/span&gt;,
        map_image: { &lt;span class="hljs-comment"&gt;/* ... */&lt;/span&gt; },
        map_image_preview: { &lt;span class="hljs-comment"&gt;/* ... */&lt;/span&gt; },
        _embedded: {
          creator: { &lt;span class="hljs-comment"&gt;/* ... */&lt;/span&gt; },
          _links: { &lt;span class="hljs-comment"&gt;/* ... */&lt;/span&gt; } },
          display_name: &lt;span class="hljs-string"&gt;&amp;quot;Fabian Müller&amp;quot;&lt;/span&gt;
          }
        }
      },
      &lt;span class="hljs-comment"&gt;/* ... other tour entries ... */&lt;/span&gt;
    ]
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="data-preprocessing"&gt;&lt;a class="markdownIt-Anchor" href="#data-preprocessing"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Data preprocessing&lt;/h2&gt;
&lt;p&gt;To analyze the data it needs to be imported into Elasticsearch first.
To do so I'll preprocess the data using Python with Pandas.&lt;/p&gt;
&lt;small&gt;
&lt;p&gt;&lt;strong&gt;Hint&lt;/strong&gt;: All steps in this section can be found in the Jupyter notebook located in &lt;code&gt;jupyter-notebooks/komoot-elk-preprocessing.ipynb&lt;/code&gt;.
I'll explain step by step what each step does.
The repo comes with some sample data which is located in &lt;code&gt;jupyter-notebooks/data/komoot-sample-data.json&lt;/code&gt;.
In case you haven't dowwnloaded your own data you can also use this data to get started - although it's boring because it only comes with three entries. ;-)&lt;/p&gt;
&lt;/small&gt;
&lt;p&gt;Create a new notebook by opening &lt;a href="http://localhost:8888"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Jupyter&lt;/a&gt; and click on &lt;code&gt;New &amp;gt; Notebook &amp;gt; Python 3&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First all necessary modules need to be imported.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; json
&lt;span class="hljs-keyword"&gt;import&lt;/span&gt; pandas &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; pd
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; pandas &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; DataFrame, Series
&lt;span class="hljs-keyword"&gt;from&lt;/span&gt; elasticsearch &lt;span class="hljs-keyword"&gt;import&lt;/span&gt; Elasticsearch
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next a connection to Elasticsearch is established.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;es = Elasticsearch(hosts=[&lt;span class="hljs-string"&gt;&amp;#x27;localhost&amp;#x27;&lt;/span&gt;], http_auth=&lt;span class="hljs-string"&gt;&amp;quot;elastic:changeme&amp;quot;&lt;/span&gt;)
es.info()
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Output:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;{
    &lt;span class="hljs-attr"&gt;&amp;quot;cluster_name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;docker-cluster&amp;quot;&lt;/span&gt;,
    &lt;span class="hljs-attr"&gt;&amp;quot;cluster_uuid&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;Dvit14MhRgiEoTxfIkuzYA&amp;quot;&lt;/span&gt;,
    &lt;span class="hljs-attr"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;osb6xs4&amp;quot;&lt;/span&gt;,
    &lt;span class="hljs-attr"&gt;&amp;quot;tagline&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;You Know, for Search&amp;quot;&lt;/span&gt;,
    &lt;span class="hljs-attr"&gt;&amp;quot;version&amp;quot;&lt;/span&gt;: {
        &lt;span class="hljs-attr"&gt;&amp;quot;build_date&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;2017-04-28T17:43:27.229Z&amp;quot;&lt;/span&gt;,
        &lt;span class="hljs-attr"&gt;&amp;quot;build_hash&amp;quot;&lt;/span&gt;: &lt;span class="hljs-string"&gt;&amp;quot;780f8c4&amp;quot;&lt;/span&gt;,
        &lt;span class="hljs-attr"&gt;&amp;quot;build_snapshot&amp;quot;&lt;/span&gt;: False,
        &lt;span class="hljs-attr"&gt;&amp;quot;lucene_version&amp;quot;&lt;/span&gt;: &amp;#x27;&lt;span class="hljs-number"&gt;6.5&lt;/span&gt;&lt;span class="hljs-number"&gt;.0&lt;/span&gt;&amp;#x27;,
        &lt;span class="hljs-attr"&gt;&amp;quot;number&amp;quot;&lt;/span&gt;: &amp;#x27;&lt;span class="hljs-number"&gt;5.4&lt;/span&gt;&lt;span class="hljs-number"&gt;.0&lt;/span&gt;&amp;#x27;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that the notebook and Elasticsearch are connected it's time to read the data in Python.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-comment"&gt;# Read data into python dictionary.&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# Use `data/komoot-sample-data.json` in this call to read the sample data.&lt;/span&gt;
&lt;span class="hljs-keyword"&gt;with&lt;/span&gt; &lt;span class="hljs-built_in"&gt;open&lt;/span&gt;(&lt;span class="hljs-string"&gt;&amp;#x27;data/komoot-sample-data.json&amp;#x27;&lt;/span&gt;) &lt;span class="hljs-keyword"&gt;as&lt;/span&gt; data_file:
    data = json.load(data_file)
df = DataFrame(data[&lt;span class="hljs-string"&gt;&amp;#x27;_embedded&amp;#x27;&lt;/span&gt;][&lt;span class="hljs-string"&gt;&amp;#x27;tours&amp;#x27;&lt;/span&gt;])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;First data is loaded into the &lt;code&gt;data&lt;/code&gt; variable, then only the list of tours is loaded into a pandas &lt;code&gt;DataFrame&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Before filling the data into the database it's worth to have a closer look into the data structure.
Especially the start point of each tour seems to not match what Elasticsearch can work with.
Elasticsearch defines so called &lt;a href="https://www.elastic.co/guide/en/elasticsearch/reference/current/geo-point.html"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;&lt;code&gt;geo_point&lt;/code&gt;s&lt;/a&gt;, which are required to have a special format.
To make use of &lt;code&gt;geo_point&lt;/code&gt;s the format needs to be transformed a bit.
Let's first tell Elasticsearch that it should expect a &lt;code&gt;geo_point&lt;/code&gt; here:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;mapping = &lt;span class="hljs-string"&gt;&amp;#x27;&amp;#x27;&amp;#x27;
{
  &amp;quot;mappings&amp;quot;: {
    &amp;quot;tour&amp;quot;: {
      &amp;quot;properties&amp;quot;: {
        &amp;quot;start_point&amp;quot;: {
          &amp;quot;type&amp;quot;: &amp;quot;geo_point&amp;quot;
        }
      }
    }
  }
}&amp;#x27;&amp;#x27;&amp;#x27;&lt;/span&gt;
es.indices.create(index=&lt;span class="hljs-string"&gt;&amp;#x27;komoot&amp;#x27;&lt;/span&gt;, ignore=&lt;span class="hljs-number"&gt;400&lt;/span&gt;, body=mapping)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This piece of code creates an index called &lt;code&gt;komoot&lt;/code&gt; in which it tells Elasticsearch to expect &lt;code&gt;start_point&lt;/code&gt; to be of type &lt;code&gt;geo_point&lt;/code&gt;.
The &lt;a href="https://www.elastic.co/blog/index-vs-type"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;document type&lt;/a&gt; is called &lt;code&gt;tour&lt;/code&gt; in the index.
It's necessary to transform the data before sending it to Elasticsearch:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;df[&lt;span class="hljs-string"&gt;&amp;#x27;start_point&amp;#x27;&lt;/span&gt;] = df[&lt;span class="hljs-string"&gt;&amp;#x27;start_point&amp;#x27;&lt;/span&gt;]\
    .&lt;span class="hljs-built_in"&gt;map&lt;/span&gt;(&lt;span class="hljs-keyword"&gt;lambda&lt;/span&gt; item: [item[&lt;span class="hljs-string"&gt;&amp;#x27;lng&amp;#x27;&lt;/span&gt;], item[&lt;span class="hljs-string"&gt;&amp;#x27;lat&amp;#x27;&lt;/span&gt;]])
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This transforms the &lt;code&gt;start_point&lt;/code&gt; column from a dictionary into a list with two entries, longitude and latitude of the starting point of the tour.&lt;/p&gt;
&lt;p&gt;Next the data can be send to Elasticsearch which will create the rest of the search index automatically based on the types of the input values.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-keyword"&gt;for&lt;/span&gt; i, row &lt;span class="hljs-keyword"&gt;in&lt;/span&gt; df.iterrows():
    res = es.index(index=&lt;span class="hljs-string"&gt;&amp;#x27;komoot&amp;#x27;&lt;/span&gt;, doc_type=&lt;span class="hljs-string"&gt;&amp;#x27;tour&amp;#x27;&lt;/span&gt;, &lt;span class="hljs-built_in"&gt;id&lt;/span&gt;=item[&lt;span class="hljs-string"&gt;&amp;#x27;id&amp;#x27;&lt;/span&gt;], body=row.to_json())
    print(row[&lt;span class="hljs-string"&gt;&amp;#x27;id&amp;#x27;&lt;/span&gt;], res[&lt;span class="hljs-string"&gt;&amp;#x27;result&amp;#x27;&lt;/span&gt;])

&lt;span class="hljs-comment"&gt;# output&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# 17342318 created&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# 17140311 created&lt;/span&gt;
&lt;span class="hljs-comment"&gt;# 17069942 created&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the next steps Kibana is used to query data and generate some nice plots from it.
The remaining thing to do is to tell Kibana that komoot is the preferred index.
To do so go to &lt;a href="http://localhost:5601"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Kibana&lt;/a&gt;, log in by using the default username (&lt;code&gt;elastic&lt;/code&gt;) and password (&lt;code&gt;changeme&lt;/code&gt;) which should bring you right to the Management tab in which a default index must be selected.
In this screen use &lt;code&gt;komoot&lt;/code&gt; as the index name, check that it contains time-based events and set the time-field name to be &lt;code&gt;date&lt;/code&gt;.
Click on &lt;code&gt;create&lt;/code&gt; to configure the index pattern.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/kibana-index-pattern.png" alt="Kibana: Interface to configure an index pattern" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Kibana: Interface to configure an index pattern&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;h2 id="data-analysis-and-visualization"&gt;&lt;a class="markdownIt-Anchor" href="#data-analysis-and-visualization"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Data analysis and visualization&lt;/h2&gt;
&lt;p&gt;Now comes the most interesting part - the data analysis.
In this section charts will be built using Kibana which accesses Elasticsearch to provide real-time charts.
In our case the data is quite sparse compared to server logs, for which Kibana is developed and where you have up to hundreds of logs per second, but nevertheless we will create charts that will change over time, so it's nice to have them encapulated in Kibana.
After creating such charts it's only necessary to push new data to Elasticsearch to get them updated.
It's as easy as that. Okay, let's see how this works.&lt;/p&gt;
&lt;p&gt;We'll create three types of visualizations in this tutorial:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#tile-map-of-all-recorded-tours"&gt;A tile map of all recorded tours&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#heatmap-of-distance-over-time"&gt;A heatmap of distance over time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#heatmap-of-distance-over-time"&gt;A bar chart of time in motion and elevation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also a &lt;a href="#dashboard"&gt;Dashboard containing all the previous charts&lt;/a&gt; will be created.&lt;/p&gt;
&lt;h3 id="tile-map-of-all-recorded-tours"&gt;&lt;a class="markdownIt-Anchor" href="#tile-map-of-all-recorded-tours"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Tile map of all recorded tours&lt;/h3&gt;
&lt;p&gt;Create a new visualization via &lt;code&gt;Visualize &amp;gt; Plus Icon (Create new visualization)&lt;/code&gt; and choose &lt;code&gt;Tile Map&lt;/code&gt;.
On the next page select &lt;code&gt;komoot&lt;/code&gt; as the index.
This will create a map with no content yet.&lt;/p&gt;
&lt;p&gt;To show a heatmap based on all recorded tours, choose &lt;code&gt;Geo Coordinates&lt;/code&gt; as the bucket.
This will automatically set up &lt;code&gt;Geohash&lt;/code&gt; as the aggregation type and the field &lt;code&gt;start_point&lt;/code&gt; since it is the only entry of type &lt;code&gt;geo_point&lt;/code&gt; in the schema of the index.
Click tab &lt;code&gt;Options&lt;/code&gt; and choose &lt;code&gt;Heatmap&lt;/code&gt; as the map type.
Depending on the data it might be useful to fine-tune the parameters in this tab later on.
To only select the recorded tours and ignore the planned ones, type &lt;code&gt;type:tour_recorded&lt;/code&gt; into the search bar at the top of the screen.
Make sure to choose a proper time span in the upper right corner (e.g. use the last two years or so).
I cannot say how often I have forgotten to do so and wondered why there is no data to display. ;-)&lt;/p&gt;
&lt;p&gt;Press the primary button &lt;code&gt;► (Apply Changes)&lt;/code&gt; to see your result.
You might need to zoom in into the region where your data points are located.
Et voilà there it is, enjoy your first heatmap!&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/kibana-heatmap.png" alt="Geo-Heatmap of recorded tours" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Geo-Heatmap of recorded tours&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Once you're satisfied with the result save the map by clicking on &lt;code&gt;Save&lt;/code&gt; at the top of the screen and choose a proper name, e.g. &amp;quot;Komoot: Geo-Heatmap of recorded tours&amp;quot;.
This allows to add the chart to a dashboard later on.&lt;/p&gt;
&lt;h3 id="heatmap-of-distance-over-time"&gt;&lt;a class="markdownIt-Anchor" href="#heatmap-of-distance-over-time"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Heatmap of distance over time&lt;/h3&gt;
&lt;p&gt;Next let's see how well we are performing.
I'd like to know how often I went for a ride and how long my rides are.&lt;/p&gt;
&lt;p&gt;Go to &lt;code&gt;Visualize &amp;gt; Plus Icon (Create new visualization) &amp;gt; Heat Map&lt;/code&gt;.
Again select &lt;code&gt;komoot&lt;/code&gt; as the index, and define a &lt;code&gt;Y-Axis&lt;/code&gt; first.
Choose &lt;code&gt;Histogram&lt;/code&gt; as the aggregation type and choose &lt;code&gt;distance&lt;/code&gt; as the field.
Define a proper interval, for me &lt;code&gt;10000&lt;/code&gt; or 10km per bucket worked fine.
Click on &lt;code&gt;Advanced&lt;/code&gt; and set &lt;code&gt;{&amp;quot;order&amp;quot; : { &amp;quot;_key&amp;quot; : &amp;quot;desc&amp;quot; }}&lt;/code&gt; as the JSON input.
This tells Kibana that the Histogram should be sorted by the key of each bucket.
Click on &lt;code&gt;Add sub-buckets&lt;/code&gt; to add a &lt;code&gt;X-Axis&lt;/code&gt; and choose &lt;code&gt;Date Histogram&lt;/code&gt; as the sub-aggregation type.
Set &lt;code&gt;date&lt;/code&gt; as the field and &lt;code&gt;Monthly&lt;/code&gt; as the interval.&lt;/p&gt;
&lt;p&gt;Again set &lt;code&gt;type:tour_recorded&lt;/code&gt; into the search bar at the top of the screen to limit the results to the recorded tours.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/kibana-heatmap-2.png" alt="Heat map: Distance (buckets) over the time (buckets)" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Heat map: Distance (buckets) over the time (buckets)&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Once you're satisfied with the result save the map by clicking on Save at the top of the screen and choose a proper name, e.g. &amp;quot;Komoot: Heatmap distance over time&amp;quot;.
This allows to add the chart to a dashboard later on.&lt;/p&gt;
&lt;h3 id="bar-chart-of-time-in-motion-and-elevation"&gt;&lt;a class="markdownIt-Anchor" href="#bar-chart-of-time-in-motion-and-elevation"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Bar chart of time in motion and elevation&lt;/h3&gt;
&lt;p&gt;Now let's see how the average time in motion varies over time and combine that with the average elevation in a given time interval.&lt;/p&gt;
&lt;p&gt;Go to &lt;code&gt;Visualize &amp;gt; Plus Icon (Create new visualization) &amp;gt; Vertical Bar&lt;/code&gt;.
Select &lt;code&gt;komoot&lt;/code&gt; as the index and set the &lt;code&gt;X-Axis&lt;/code&gt; to be of type &lt;code&gt;Date Histogram&lt;/code&gt;.
Choose a &lt;code&gt;Weekly&lt;/code&gt; interval.
Then define two &lt;code&gt;Y-Axis&lt;/code&gt;, where one has aggregation type &lt;code&gt;Average&lt;/code&gt; and field &lt;code&gt;time_in_motion&lt;/code&gt;.
The other one is also of aggregation type &lt;code&gt;Average&lt;/code&gt; but has set &lt;code&gt;elevation_up&lt;/code&gt; as its field.&lt;/p&gt;
&lt;p&gt;Then go to tab &lt;code&gt;Metrics &amp;amp; Axes&lt;/code&gt; and add a second &lt;code&gt;Y-Axis&lt;/code&gt;.
Afterwards in section &lt;code&gt;Metrics&lt;/code&gt; set both axis to type &lt;code&gt;normal&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;And once again do not forget to set &lt;code&gt;type:tour_recorded&lt;/code&gt; in the search bar at the top of the screen to limit the results to the recorded tours.&lt;/p&gt;
&lt;p&gt;This gives a nice bar chart that shows the average time in motion as well as the average upwards elevation.
Seems like I have climbed a lot more in average in the last year than nowadays.
Do I get old? ;-)&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/kibana-bar-chart.png" alt="Bar chart showing average time in motion and average upwards elevation" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Bar chart showing average time in motion and average upwards elevation&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;h3 id="dashboard"&gt;&lt;a class="markdownIt-Anchor" href="#dashboard"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Dashboard&lt;/h3&gt;
&lt;p&gt;To sum up the visualization part let's create a dashboard.
It will be used to display all charts side by side to each other.
The main purpose is to interactively explore data.
Since all visualizations are connected in the dashboard changing the search query or time interval will update all visualizations at once.
Let's get started!&lt;/p&gt;
&lt;p&gt;Click on &lt;code&gt;Dashboard &amp;gt; Plus Icon (Create new dashboard)&lt;/code&gt; to create a new dashboard.
Then click &lt;code&gt;Add&lt;/code&gt; at the top of the screen and select the visualizations we have created before.
You can re-arrange them as you like, also the size and position can be changed.
To save the dashboard click &lt;code&gt;Save&lt;/code&gt; on the top of the screen and give it a proper name, e.g. &lt;code&gt;Komoot&lt;/code&gt;.
I'd recommend to store the time by checking &lt;code&gt;Store time with dashboard&lt;/code&gt;.
This will update the currently selected time span each time the dashboard is opened.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/kibana-dashboard.png" alt="Custom dashboard with custom visualizations in Kibana" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Custom dashboard with custom visualizations in Kibana&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;Note that you can use the search query to further reduce the number of results.
This will automatically update all charts which is super useful if you have a bunch of charts that you want to keep in sync.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/kibana-dashboard-2.png" alt="Changing search queries will automatically update the visuals" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Changing search queries will automatically update the visuals&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;h2 id="summary-and-outlook"&gt;&lt;a class="markdownIt-Anchor" href="#summary-and-outlook"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Summary and Outlook&lt;/h2&gt;
&lt;p&gt;Let's recap what we have done on our journey to create a dashboard with customized charts.&lt;/p&gt;
&lt;p&gt;We used a dockerized environment to spin up containers which isolate Elasticsearch, Kibana and Jupyter.
This technique in itself is very helpful to isolate environments and get rid of any dependency hell issues.
I highly recommend to work in dockerized environments as these enviroments can be shared easily, such as I have done it by providing you a git repository that defines all the needed tools.&lt;/p&gt;
&lt;p&gt;Data mining has been done by calling an API endpoint by hand and copying the data manually.
Although it was simple to do, this is no good solution as it requires to manually log into a website to generate the necessary authentication credentials.
I'd like to do better here, which means using the OAuth2 layer of the API, but currently this seems not possible.
In case you can use an OAuth layer I'd recommend to do so, because by doing so the data can be requested automatically and therefore the database can be updated regularly with the latest and freshest data available.&lt;/p&gt;
&lt;p&gt;For data preprocessing we have used Python and Pandas which I like to work with.
Of course other tools are available and there is no need to use the tools I've used.
Feel free to use whatever you like to work with.
This section also gives a lot of freedom what to do next.
Besides using Python only to preprocess data also analytics could be done here.
As we have connected Python with the Elasticsearch database, it's easy to do queries here against the database.
The &lt;code&gt;elasticsearch&lt;/code&gt; module comes with a large API and you should definitively have a look what else it can do.
We could also either visualize data directly in Python using matplotlib or other libraries or maybe do some kind of Machine learning.
Sky is the limit at this point.&lt;/p&gt;
&lt;p&gt;After we've put the data into the database we have analyzed it using Kibana, a powerful frontend for Elasticsearch.
We have generated our own visualizations which are easy to create and highly dynamical.
We have created a dashboard to group multiple charts and make them dependent on the same query and time interval.
This can be a large time saver when digging into the data to gain more and more insights.
If you want to show your work to others, note that &lt;a href="https://www.elastic.co/guide/en/kibana/current/sharing-dashboards.html"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;dashboards can be shared&lt;/a&gt;.
But note that sharing requires to have the stack running on a server, not only on your local dev machine.&lt;/p&gt;
&lt;p&gt;I hope you enjoyed this article. Let me know if you have any questions or remarks.&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="data-pipeline"/>
    <category term="docker"/>
    <category term="elasticsearch"/>
    <category term="kibana"/>
    <category term="tech"/>
  </entry>
  <entry>
    <title>Howto train a CNN with TensorFlow on FloydHub</title>
    <published>2017-06-11T12:00:00Z</published>
    <updated>2017-06-11T12:00:00Z</updated>
    <id>https://blog.dotcs.me/posts/cnn-floydhub</id>
    <link href="https://blog.dotcs.me/posts/cnn-floydhub"/>
    <summary>This article demonstrates how to solve the MNIST Digit Recognizer task by using a Convolutional Neural Net (CNN). It will be trained with TensorFlow, Googles open-source software library for Machine Intelligence. For the heavy-lifting the cloud provider FloydHub will be used.</summary>
    <content type="html" xml:base="https://blog.dotcs.me/posts/cnn-floydhub">&lt;p&gt;Nowadays artificial intelligence (AI) is a big thing.
Computational power increased a lot within the last years which makes it possible to build and run large Neural Networks (NN) for Deep Learning (DL) that model various problems extremely well.
Unfortunately huge networks consume large amounts of memory and computational power.
Often developers do not have the necessary hardware available to solve larger problems in a suitable time.
One solution to this problem is to shift the heavy computational task to the some cloud provider.&lt;/p&gt;
&lt;p&gt;This article demonstrates how to solve the &lt;a href="https://www.kaggle.com/c/digit-recognizer/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;MNIST Digit Recognizer task&lt;/a&gt; by using a Convolutional Neural Net (CNN).
It will be trained with &lt;a href="https://www.tensorflow.org/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;TensorFlow&lt;/a&gt;, Google's open-source software library for Machine Intelligence.
For the heavy-lifting the cloud provider FloydHub will be used.&lt;/p&gt;
&lt;p&gt;This article uses the Kaggle Dataset for &lt;a href="https://www.kaggle.com/c/digit-recognizer/data"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;MNIST Digit Recognizer&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="table-of-content"&gt;&lt;a class="markdownIt-Anchor" href="#table-of-content"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Table of content&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href="#why-floydhub"&gt;Why FloydHub&lt;/a&gt;&lt;br&gt;
Quick overview of what FloydHub is and why I've chosen this provider.&lt;/li&gt;
&lt;li&gt;&lt;a href="#cnn-architecture"&gt;CNN Architecture&lt;/a&gt;&lt;br&gt;
Anwers how the CNN architecture does look like in more detail.&lt;/li&gt;
&lt;li&gt;&lt;a href="#get-the-code"&gt;Get the code&lt;/a&gt;&lt;br&gt;
Explains how to get the code to run get your own instance up and running.&lt;/li&gt;
&lt;li&gt;&lt;a href="#upload-data-to-floydhub"&gt;Upload data to FloydHub&lt;/a&gt;&lt;br&gt;
Explains how data can be transferred to FloydHub by using their CLI tool.&lt;/li&gt;
&lt;li&gt;&lt;a href="#run-a-jupyter-notebook"&gt;Run a Jupyter notebook&lt;/a&gt;&lt;br&gt;
A Jupyter notebook will be started on FloydHub.&lt;/li&gt;
&lt;li&gt;&lt;a href="#train-the-cnn"&gt;Train the CNN&lt;/a&gt;&lt;br&gt;
Explains how the CNN can be trained in the cloud.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="why-floydhub"&gt;&lt;a class="markdownIt-Anchor" href="#why-floydhub"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Why FloydHub?&lt;/h2&gt;
&lt;p&gt;The aim of Floydhub is to be the &amp;quot;Heroku for Deep Learning&amp;quot;.
Given how easy it is to use Heroku for deploying websites, it seems a perfect candidate to focus on the things we really want to do - not to much on how to handle the infrastructure.&lt;/p&gt;
&lt;p&gt;Obviously another reason is that if we need huge amounts of GPU power, FloydHub has us covered.
They offer access to one Nvidia Tesla K80 graphic card with 12GB RAM per instance.
And this GPU is a beast! It gives us the option to run even very deep and memory intensive networks.
Also this provider offers a per second billing and auto-termination of jobs after they run.
I think this setup is quite good and something I wanted definitively to explore.&lt;/p&gt;
&lt;p&gt;This article assumes that you have created an account at FloydHub and installed their CLI tool.
If you haven't please do so first and get 100 hours free GPU, which is more than enough to follow this article.&lt;/p&gt;
&lt;h2 id="cnn-architecture"&gt;&lt;a class="markdownIt-Anchor" href="#cnn-architecture"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; CNN Architecture&lt;/h2&gt;
&lt;p&gt;Let's first talk about how our CNN will look like.
We'll use a CNN with four convolution (CONV), two max-pooling (POOL) and two dense (or fully connected, FC) layers.
The basic structure looks like this:&lt;/p&gt;
&lt;div class="overflow-x-auto"&gt;&lt;table&gt;&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;LAYER TYPE&lt;/th&gt;
&lt;th style="text-align:center"&gt;F&lt;/th&gt;
&lt;th style="text-align:center"&gt;S&lt;/th&gt;
&lt;th style="text-align:center"&gt;PAD&lt;/th&gt;
&lt;th style="text-align:center"&gt;FILTERS&lt;/th&gt;
&lt;th style="text-align:center"&gt;X&lt;/th&gt;
&lt;th style="text-align:center"&gt;Y&lt;/th&gt;
&lt;th style="text-align:center"&gt;Z&lt;/th&gt;
&lt;th style="text-align:center"&gt;MEM (COUNT)&lt;/th&gt;
&lt;th style="text-align:center"&gt;MEM (SIZE IN KB)&lt;/th&gt;
&lt;th style="text-align:center"&gt;WEIGHTS (COUNT)&lt;/th&gt;
&lt;th style="text-align:center"&gt;&lt;/th&gt;
&lt;th style="text-align:center"&gt;USAGE OF WEIGHTS IN THIS LAYER&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;INPUT&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;28&lt;/td&gt;
&lt;td style="text-align:center"&gt;28&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;784&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.7656&lt;/td&gt;
&lt;td style="text-align:center"&gt;0&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONV&lt;/td&gt;
&lt;td style="text-align:center"&gt;3&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;64&lt;/td&gt;
&lt;td style="text-align:center"&gt;28&lt;/td&gt;
&lt;td style="text-align:center"&gt;28&lt;/td&gt;
&lt;td style="text-align:center"&gt;64&lt;/td&gt;
&lt;td style="text-align:center"&gt;50176&lt;/td&gt;
&lt;td style="text-align:center"&gt;49.0000&lt;/td&gt;
&lt;td style="text-align:center"&gt;576&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0001&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONV&lt;/td&gt;
&lt;td style="text-align:center"&gt;3&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;64&lt;/td&gt;
&lt;td style="text-align:center"&gt;28&lt;/td&gt;
&lt;td style="text-align:center"&gt;28&lt;/td&gt;
&lt;td style="text-align:center"&gt;64&lt;/td&gt;
&lt;td style="text-align:center"&gt;50176&lt;/td&gt;
&lt;td style="text-align:center"&gt;49.0000&lt;/td&gt;
&lt;td style="text-align:center"&gt;36864&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0055&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POOL&lt;/td&gt;
&lt;td style="text-align:center"&gt;2&lt;/td&gt;
&lt;td style="text-align:center"&gt;2&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;14&lt;/td&gt;
&lt;td style="text-align:center"&gt;14&lt;/td&gt;
&lt;td style="text-align:center"&gt;64&lt;/td&gt;
&lt;td style="text-align:center"&gt;12544&lt;/td&gt;
&lt;td style="text-align:center"&gt;12.2500&lt;/td&gt;
&lt;td style="text-align:center"&gt;0&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONV&lt;/td&gt;
&lt;td style="text-align:center"&gt;3&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;128&lt;/td&gt;
&lt;td style="text-align:center"&gt;14&lt;/td&gt;
&lt;td style="text-align:center"&gt;14&lt;/td&gt;
&lt;td style="text-align:center"&gt;128&lt;/td&gt;
&lt;td style="text-align:center"&gt;25088&lt;/td&gt;
&lt;td style="text-align:center"&gt;24.5000&lt;/td&gt;
&lt;td style="text-align:center"&gt;73728&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0110&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CONV&lt;/td&gt;
&lt;td style="text-align:center"&gt;3&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;128&lt;/td&gt;
&lt;td style="text-align:center"&gt;14&lt;/td&gt;
&lt;td style="text-align:center"&gt;14&lt;/td&gt;
&lt;td style="text-align:center"&gt;128&lt;/td&gt;
&lt;td style="text-align:center"&gt;25088&lt;/td&gt;
&lt;td style="text-align:center"&gt;24.5000&lt;/td&gt;
&lt;td style="text-align:center"&gt;147456&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0220&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;POOL&lt;/td&gt;
&lt;td style="text-align:center"&gt;2&lt;/td&gt;
&lt;td style="text-align:center"&gt;2&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;7&lt;/td&gt;
&lt;td style="text-align:center"&gt;7&lt;/td&gt;
&lt;td style="text-align:center"&gt;128&lt;/td&gt;
&lt;td style="text-align:center"&gt;6272&lt;/td&gt;
&lt;td style="text-align:center"&gt;6.1250&lt;/td&gt;
&lt;td style="text-align:center"&gt;0&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FC&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1024&lt;/td&gt;
&lt;td style="text-align:center"&gt;1024&lt;/td&gt;
&lt;td style="text-align:center"&gt;1.0000&lt;/td&gt;
&lt;td style="text-align:center"&gt;6422528&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.9598&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DROPOUT&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FC&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;1&lt;/td&gt;
&lt;td style="text-align:center"&gt;10&lt;/td&gt;
&lt;td style="text-align:center"&gt;10&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0098&lt;/td&gt;
&lt;td style="text-align:center"&gt;10240&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;0.0015&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;SOFTMAX&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;td style="text-align:center"&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;&lt;p&gt;In total this CNN has around 6.7 million weights.
As it's quite common in CNNs nearby all parameters (ca. 96%) are located towards the end of the network in the first FC layer.
Note that after the first FC layer a dropout layer is used for regularization of weights.
After the last FC layer a softmax function is used to squash values between 0 and 1 which allows to treat them as predictions.&lt;/p&gt;
&lt;p&gt;The following meta parameters are used to train the CNN:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;learning_rate = &lt;span class="hljs-number"&gt;0.001&lt;/span&gt;       &lt;span class="hljs-comment"&gt;# learning rate of the network&lt;/span&gt;
training_iters = &lt;span class="hljs-number"&gt;100000&lt;/span&gt;     &lt;span class="hljs-comment"&gt;# number of total iterations used for training&lt;/span&gt;
batch_size = &lt;span class="hljs-number"&gt;56&lt;/span&gt;             &lt;span class="hljs-comment"&gt;# number of samples in each training step&lt;/span&gt;
keep_prob = &lt;span class="hljs-number"&gt;0.75&lt;/span&gt;            &lt;span class="hljs-comment"&gt;# probability to keep nodes in DROPOUT layer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Alright, let's have some fun with this network and FloydHub.&lt;/p&gt;
&lt;h2 id="get-the-code"&gt;&lt;a class="markdownIt-Anchor" href="#get-the-code"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Get the code&lt;/h2&gt;
&lt;p&gt;To get the code please checkout this &lt;a href="https://github.com/dotcs/mnist-cnn-floydhub"&gt;&lt;i class="lab la-github" title="This link refers to an external site"&gt;&lt;/i&gt;git repository&lt;/a&gt;.
It comes with a dockerized environment which installs all necessary requirements, such as NumPy, SciPy, Jupyter and tensorflow.
To get started make sure to have &lt;a href="https://docs.docker.com/engine/installation/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;docker installed&lt;/a&gt;.&lt;/p&gt;
&lt;small&gt;
&lt;p&gt;If you don't want to use the dockerized setup, feel free to use the notebooks directly from the &lt;code&gt;notebooks&lt;/code&gt; directory.
But be aware that in this case you have to adjust the &lt;code&gt;INPUT_ROOT&lt;/code&gt; and &lt;code&gt;OUTPUT_ROOT&lt;/code&gt; paths in the &lt;code&gt;floydhub-simple-cnn.ipynb&lt;/code&gt; file.
You then also have to install all requirements on your own.&lt;/p&gt;
&lt;/small&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;git &lt;span class="hljs-built_in"&gt;clone&lt;/span&gt; https://github.com/dotcs/mnist-cnn-floydhub  &lt;span class="hljs-comment"&gt;# clone git repository to local machine&lt;/span&gt;
&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; mnist-cnn-floydhub      &lt;span class="hljs-comment"&gt;# change into the cloned directory&lt;/span&gt;
sh extract-zip-locally.sh  &lt;span class="hljs-comment"&gt;# extract zipped training and test data to ./input folder&lt;/span&gt;
docker-compose up          &lt;span class="hljs-comment"&gt;# start the dockerized environment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now a local instance of Jupyter should run on &lt;a href="http://localhost:8888/"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;localhost:8888&lt;/a&gt;.
You can use that to discover how the CNN is written using TensorFlow. The corresponding notebook is called &lt;code&gt;floydhub-simple-cnn.ipynb&lt;/code&gt; and will be listed in Jupyter.
But beware, training could take forever depending on your machine.
This is exactly why we need to do computation in the cloud.
Let's go ahead!&lt;/p&gt;
&lt;h2 id="upload-data-to-floydhub"&gt;&lt;a class="markdownIt-Anchor" href="#upload-data-to-floydhub"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Upload data to FloydHub&lt;/h2&gt;
&lt;p&gt;To access data in a VM in FloydHub it must be transferred to the cloud first.
FloydHub provides commands via their CLI to upload large datasets from our machine to their cloud.
A zipped version of the train and test datasets is placed in the &lt;code&gt;floydhub-zip&lt;/code&gt; folder.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; floydhub-zip                &lt;span class="hljs-comment"&gt;# change into folder that contains the zipped data&lt;/span&gt;
floyd data init mnist.zipped   &lt;span class="hljs-comment"&gt;# create a new data resource on FloydHub&lt;/span&gt;
floyd data upload              &lt;span class="hljs-comment"&gt;# upload the data to that resource&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now the following output in your terminal, which tells you that 14.7 MB of data are transferred to FloydHub.
It also gives you access to the &lt;code&gt;DATA_ID&lt;/code&gt; after the upload has been finished.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;Compressing data ...
Creating data source. Total upload size: 14.7MiB
Uploading compressed data ...
Done=============================] 15378159/15378159 - 00:00:00
Upload finished
DATA ID                 NAME                    VERSION
----------------------  --------------------  ---------
P3WVS2Vswo2o66pMsbPMTC  dotcs/mnist.zipped:1          1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In the summary table the &lt;code&gt;DATA_ID&lt;/code&gt; is listed.
In my case is &lt;code&gt;P3WVS2Vswo2o66pMsbPMTC&lt;/code&gt; but is different in your case.
Make sure to copy this ID as we'll need it in a second.&lt;/p&gt;
&lt;p&gt;What we have transferred to FloydHub is a zipped copy of our data.
This is not exactly what we want to work with, so let's unzip it first on the remote machine.
We do so by switching to the &lt;code&gt;floydhub-unzip&lt;/code&gt; folder.
Since unzipping is a one-time-step I suggest to create a new project for this task only.
This keeps the logs and version control of other projects that you have on FloydHub clean.&lt;/p&gt;
&lt;p&gt;By running a shell command with the &lt;code&gt;--data&lt;/code&gt; flag, we specify the data-container that should be mounted to the /input path of the container in which the shell command (or later the tensorflow environment) runs.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; floydhub-unzip
floyd init mnist.unzipped
floyd run --data P3WVS2Vswo2o66pMsbPMTC &lt;span class="hljs-string"&gt;&amp;quot;unzip /input/train-test.zip -d /output&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;You should now see again some output that looks like this:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;Creating project run. Total upload size: 282.0B
Syncing code ...
Done=============================] 1027/1027 - 00:00:00
RUN ID                  NAME                      VERSION
----------------------  ----------------------  ---------
b8yRAXUGMnhoTbrVvPMfCX  dotcs/mnist.unzipped:1          1


To view logs enter:
    floyd logs b8yRAXUGMnhoTbrVvPMfCX
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This time our task runs asynchronously - again on a fresh VM.
At some point in the future the command will finish its work and the container will automatically be shut down by FloydHub.
To see what happens behind the scene it's possible to either check the &lt;a href="https://www.floydhub.com/experiments"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;dashboard&lt;/a&gt; or run the &lt;code&gt;floyd logs &amp;lt;RUN_ID&amp;gt;&lt;/code&gt; command that was referenced in the output.&lt;/p&gt;
&lt;p&gt;In my case the output is:&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;2017-06-11 06:25:12,073 INFO - Preparing to run TaskInstance &amp;lt;TaskInstance: dotcs/mnist.unzipped:1 (id: NQpRp4cU3srGWtMK33voLJ) (checksum: 9d9d607a8649c3ab0d9d69489f2d8fc4) (last update: 2017-06-11 06:25:12.065267) [queued]&amp;gt;
2017-06-11 06:25:12,085 INFO - Starting attempt 1 at 2017-06-11 06:25:12.077828
2017-06-11 06:25:12,322 INFO - Executing command in container: stdbuf -o0 sh command.sh
2017-06-11 06:25:12,322 INFO - Pulling Docker image: floydhub/tensorflow:1.1.0-py3
2017-06-11 06:25:13,506 INFO - Starting container...
2017-06-11 06:25:13,740 INFO -
################################################################################

2017-06-11 06:25:13,741 INFO - Run Output:
2017-06-11 06:25:13,792 INFO - Archive:  /input/train-test.zip
2017-06-11 06:25:15,027 INFO - inflating: /output/train.csv
2017-06-11 06:25:15,896 INFO - inflating: /output/test.csv
2017-06-11 06:25:15,942 INFO -
################################################################################

2017-06-11 06:25:15,943 INFO - Waiting for container to complete...
2017-06-11 06:25:16,287 INFO - [success] Finishing execution in 4 seconds for TaskInstance &amp;lt;TaskInstance: dotcs/mnist.unzipped:1 (id: NQpRp4cU3srGWtMK33voLJ) (checksum: 9d9d607a8649c3ab0d9d69489f2d8fc4) (last update: 2017-06-11 06:25:16.278860) [success]&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we see that two files have been written to the &lt;code&gt;/output&lt;/code&gt; folder: &lt;code&gt;train.csv&lt;/code&gt; and &lt;code&gt;test.csv&lt;/code&gt;.
And again this data-container gets an ID assigned which can be used as the input container for another runtime container.&lt;/p&gt;
&lt;p&gt;To get the ID of all containers there run the command floyd data status.
This gives us a list of all data-container, where the unzipped one has the DATA_ID &lt;code&gt;NQpRp4cU3srGWtMK33voLJ&lt;/code&gt; in my case.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;DATA ID                 CREATED        DISK USAGE    NAME                                                VERSION
----------------------  -------------  ------------  ------------------------------------------------  ---------
NQpRp4cU3srGWtMK33voLJ  2 minutes ago  121.97 MB     dotcs/mnist.unzipped:1/output                             1
P3WVS2Vswo2o66pMsbPMTC  5 minutes ago  14.66 MB      dotcs/mnist.zipped:1                                      1
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Finally we do have transferred and extracted our data completely.
Since we don't need the zipped file anymore we can get rid of it: &lt;code&gt;floyd data delete P3WVS2Vswo2o66pMsbPMTC&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Now it's time to move to the more interesting tasks.
Let's get the CNN up and running.&lt;/p&gt;
&lt;h2 id="run-a-jupyter-notebook"&gt;&lt;a class="markdownIt-Anchor" href="#run-a-jupyter-notebook"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Run a Jupyter notebook&lt;/h2&gt;
&lt;p&gt;Multiple options exist how to run code at FloydHub using the CLI.
Here I have written code as a Jupyter notebook which makes it necessary to run floyd with the flag --mode jupyter.
To run a Jupyter notebook we first have to create another project which will be solely used for Jupyter.
This spins up a fresh new VM for us.
By using the --gpu flag we get access to an Nvidia Tesla K80 GPU with 12 GB of RAM.
Can you already feel the power? ☺&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;&lt;span class="hljs-built_in"&gt;cd&lt;/span&gt; notebooks
floyd init mnist-cnn-floydhub
floyd run --gpu --mode jupyter --env tensorflow --data NQpRp4cU3srGWtMK33voLJ
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It might take a while to spin up the VM and the output should look like this.
Note that in the first step around 21kB of data is uploaded to the VM.
This is our Jupyter notebook, so we can access it directly in the upcoming VM.
Because files that are uploaded in this step cannot exceed a given file size limit is is necessary to upload larger files with the floyd data CLI command as we have seen before.&lt;/p&gt;
&lt;pre class="hljs"&gt;&lt;code&gt;Creating project run. Total upload size: 21.4KiB
Syncing code ...
Done=============================] 22993/22993 - 00:00:00
RUN ID                  NAME                          VERSION
----------------------  --------------------------  ---------
bViegecgc6cWncp9vmJRjG  dotcs/mnist-cnn-floydhub:1          1

Setting up your instance and waiting for Jupyter notebook to become available ......................

Path to jupyter notebook: https://www.floydhub.com:8000/XP3WqMBjV3meJejcmMhW66

To view logs enter:
    floyd logs bViegecgc6cWncp9vmJRjG
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="train-the-cnn"&gt;&lt;a class="markdownIt-Anchor" href="#train-the-cnn"&gt;&lt;span class="hidden sm:inline-block x-headline-anchor"&gt;#&lt;/span&gt;&lt;/a&gt; Train the CNN&lt;/h2&gt;
&lt;p&gt;We have waited long enough to train our CNN, now we can do so.
Go to the URL provided in the command above, in my case &lt;code&gt;https://www.floydhub.com:8000/XP3WqMBjV3meJejcmMhW66&lt;/code&gt;, and select the notebook floydhub-simple-cnn.&lt;/p&gt;
&lt;p&gt;Select &lt;code&gt;Cell &amp;gt; Run All&lt;/code&gt; to run the notebook and wait for the CNN to be trained.
After is has finished &lt;strong&gt;make sure to close the machine via the &lt;a href="https://www.floydhub.com/experiments"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;dashboard&lt;/a&gt;&lt;/strong&gt;.
Jupyter notebooks &lt;strong&gt;run infinitely and are not shut down automatically&lt;/strong&gt; so make sure to shut it down yourself when you're finished.
It should like this after shutdown:&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/fh-notebook-shutdown.png" alt="Correctly shutdown Jupyter notebook on FloydHub" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Correctly shutdown Jupyter notebook on FloydHub&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;While the notebook is executed several files are written to the &lt;code&gt;/output&lt;/code&gt; directory of this VM.
After training the trained model is written to &lt;code&gt;/output/model.ckpt&lt;/code&gt;.
Predictions on the Kaggle test dataset are written to &lt;code&gt;/output/prediction.csv&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To download the data go to &lt;a href="https://www.floydhub.com/data"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;floydhub.com/data&lt;/a&gt; and click on the arrow in the &lt;code&gt;View&lt;/code&gt; column.
This opens a detail view on the right which allows you to either browse the data (and download only parts of it) by clicking on the &lt;code&gt;open&lt;/code&gt; button or download all the data as a tar-archive by clicking on the &lt;code&gt;download&lt;/code&gt; button.&lt;/p&gt;
&lt;p&gt;
        &lt;div class="text-center bg-gray-100 py-2"&gt;
            &lt;div class="overflow-x-auto px-1"&gt;
                &lt;img src="/posts/%3Cpost_slug%3E/fh-download-data.png" alt="Data export on FloydHub" class="inline-block mb-2"&gt;
            &lt;/div&gt;
            &lt;div class="italic text-gray-700"&gt;Data export on FloydHub&lt;/div&gt;
        &lt;/div&gt;
    &lt;/p&gt;
&lt;p&gt;And by the way, predictions of this CNN yield an accuracy of 0.95071 on the &lt;a href="https://www.kaggle.com/c/digit-recognizer/leaderboard"&gt;&lt;i class="las la-external-link-alt" title="This link refers to an external site"&gt;&lt;/i&gt;Kaggle Leaderboard&lt;/a&gt;, which means that about 5 out of 100 images are classified wrongly.
This is a not too good result actually, but also not too bad.
Now it's up to you to use the power of this setup to yield a better result! Have fun!&lt;/p&gt;
</content>
    <author>
      <name>dotcs</name>
    </author>
    <category term="machine-learning"/>
    <category term="tech"/>
  </entry>
</feed>