diff --git a/.gitignore b/.gitignore
index 0722697922e716a2ee93c68edcd0426b2a7c7480..be3c97de8916151178138fe29a87fdac9086d3ab 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
-stream-tv-server*
-binaries/
-.vscode
-version
\ No newline at end of file
+/binaries/
+/.vscode
+/version
+/stream-server*
+*-packr.go
\ No newline at end of file
diff --git a/.gitlab-ci-server.yml b/.gitlab-ci-server.yml
index c2c3cde0c715ca9eb1ff05746d267b5454f22a53..91e689712e9c3ac81f0a7861cfecbfb904430d9a 100644
--- a/.gitlab-ci-server.yml
+++ b/.gitlab-ci-server.yml
@@ -1,4 +1,4 @@
-# This is the CI file for the stream-tv-server
+# This is the CI file for the stream-server
 
 server:
   stage: build
@@ -7,19 +7,20 @@ server:
   before_script:
     - go mod download
     # TWITCH_CLIENT_ID is supplied via the GitLab CI/CD environment variables. This way only project owners can see the private key
-    - export LDFLAGS="-s -X main.twitchClientID=$TWITCH_CLIENT_ID -X main.version=$(cat version)"
+    - export LDFLAGS="-s -X stream-server/internal/server.defaultTwitchClientID=$TWITCH_CLIENT_ID -X stream-server/internal/server.Version=$(cat version)"
+    - cd internal/server/ && packr2 && cd ../../
 
   script:
-    - env GOOS=darwin   GOARCH=386    go build -ldflags "$LDFLAGS" -o binaries/darwin-x86/stream-tv-server          ./cmd/stream-tv-server
-    - env GOOS=darwin   GOARCH=amd64  go build -ldflags "$LDFLAGS" -o binaries/darwin-x86_64/stream-tv-server       ./cmd/stream-tv-server
+    - env GOOS=darwin   GOARCH=386    go build -ldflags "$LDFLAGS" -o binaries/darwin-x86/stream-server          ./cmd/stream-server
+    - env GOOS=darwin   GOARCH=amd64  go build -ldflags "$LDFLAGS" -o binaries/darwin-x86_64/stream-server       ./cmd/stream-server
 
-    - env GOOS=linux    GOARCH=386    go build -ldflags "$LDFLAGS" -o binaries/linux-x86/stream-tv-server           ./cmd/stream-tv-server
-    - env GOOS=linux    GOARCH=amd64  go build -ldflags "$LDFLAGS" -o binaries/linux-x86_64/stream-tv-server        ./cmd/stream-tv-server
-    - env GOOS=linux    GOARCH=arm    go build -ldflags "$LDFLAGS" -o binaries/linux-arm/stream-tv-server           ./cmd/stream-tv-server
-    - env GOOS=linux    GOARCH=arm64  go build -ldflags "$LDFLAGS" -o binaries/linux-arm64/stream-tv-server         ./cmd/stream-tv-server
+    - env GOOS=linux    GOARCH=386    go build -ldflags "$LDFLAGS" -o binaries/linux-x86/stream-server           ./cmd/stream-server
+    - env GOOS=linux    GOARCH=amd64  go build -ldflags "$LDFLAGS" -o binaries/linux-x86_64/stream-server        ./cmd/stream-server
+    - env GOOS=linux    GOARCH=arm    go build -ldflags "$LDFLAGS" -o binaries/linux-arm/stream-server           ./cmd/stream-server
+    - env GOOS=linux    GOARCH=arm64  go build -ldflags "$LDFLAGS" -o binaries/linux-arm64/stream-server         ./cmd/stream-server
 
-    - env GOOS=windows  GOARCH=386    go build -ldflags "$LDFLAGS" -o binaries/windows-x86/stream-tv-server.exe     ./cmd/stream-tv-server
-    - env GOOS=windows  GOARCH=amd64  go build -ldflags "$LDFLAGS" -o binaries/windows-x86_64/stream-tv-server.exe  ./cmd/stream-tv-server
+    - env GOOS=windows  GOARCH=386    go build -ldflags "$LDFLAGS" -o binaries/windows-x86/stream-server.exe     ./cmd/stream-server
+    - env GOOS=windows  GOARCH=amd64  go build -ldflags "$LDFLAGS" -o binaries/windows-x86_64/stream-server.exe  ./cmd/stream-server
 
   artifacts:
     paths: # pass binaries directory to deploy stage
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index b960d3dc26cd6ffad002bceb0695c6cd9668adba..c9b900c56b81b7f7da38a7523d61ad4535940a6a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,3 +1,3 @@
 # Contributing
 
-https://simons-nzse-2.h-da.io/stream-tv/about/contributing/
\ No newline at end of file
+#### https://stream-server.h-da.io/about/contributing/
diff --git a/README.md b/README.md
index 0ae894fdc722187820418e67d0be5ee9cc64a3f7..c839d083983e221fac750627cb1228fc9fc6cad7 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,3 @@
-# Stream TV Project
+# Stream Server
 
-https://simons-nzse-2.h-da.io/stream-tv/
\ No newline at end of file
+#### https://stream-server.h-da.io/
\ No newline at end of file
diff --git a/cmd/stream-tv-server/main.go b/cmd/stream-server/main.go
similarity index 64%
rename from cmd/stream-tv-server/main.go
rename to cmd/stream-server/main.go
index b5ab2d19dd2519e5c952fc1dfc506c3e12218338..29c59130191688fa7a07140ddb8736faa2043aab 100644
--- a/cmd/stream-tv-server/main.go
+++ b/cmd/stream-server/main.go
@@ -3,38 +3,15 @@ package main
 import (
 	"fmt"
 	"log"
-	"net/http"
 	"os"
 
 	"github.com/urfave/cli"
 
-	"stream-tv/internal/api"
-	"stream-tv/internal/util"
-	"stream-tv/internal/website"
-	"stream-tv/pkg/twitch"
+	"stream-server/internal/server"
+	"stream-server/internal/util"
 )
 
-const (
-	docURL = "https://simons-nzse-2.h-da.io/stream-tv/server/"
-)
-
-var (
-	// version will automatically be set by the CI pipeline.
-	version = "dev"
-
-	// twitchClientID will automatically be set by the CI pipeline.
-	twitchClientID = ""
-)
-
-func init() {
-	// If you want to use your own twitch client id set the TWITCH_CLIENT_ID environment variable.
-
-	if clientID := os.Getenv("TWITCH_CLIENT_ID"); clientID != "" {
-		twitch.ClientID = clientID
-	} else {
-		twitch.ClientID = twitchClientID
-	}
-}
+const docURL = "https://stream-server.h-da.io"
 
 func main() {
 	app := cli.NewApp()
@@ -44,10 +21,10 @@ func main() {
 	var flagLocal bool
 	var flagHelp bool
 
-	app.Usage = "Stream TV Server " + docURL
+	app.Usage = docURL
 	app.HideHelp = true
 
-	app.Version = version
+	app.Version = server.Version
 
 	app.Flags = []cli.Flag{
 		cli.IntFlag{
@@ -109,16 +86,10 @@ func main() {
 			}
 		}
 
-		fmt.Printf("Read the documentation at %squickstart/ to get started.\n", docURL)
+		fmt.Printf("Read the quickstart at %s/quickstart to get started.\n", docURL)
 		fmt.Printf("Stop with Ctrl-C or close this terminal.\n")
 
-		mux := http.NewServeMux() // new router
-
-		mux.Handle("/", website.Handler())
-		mux.Handle("/twitch/", api.TwitchHandler())
-		mux.Handle("/tv/", api.TVHandler())
-
-		return http.ListenAndServe(listenAddr, mux)
+		return server.ListenAndServe(listenAddr)
 	}
 
 	err := app.Run(os.Args)
diff --git a/docs/about/changelog.md b/docs/about/changelog.md
new file mode 100644
index 0000000000000000000000000000000000000000..8972800aae135919d1677d98bb19cdaba8612f18
--- /dev/null
+++ b/docs/about/changelog.md
@@ -0,0 +1,5 @@
+!!! note
+    To upgrade the Stream Server to the latest version just replace the executable with the newest from [Downloads](../download-and-run.md#download). There are no dependencies.
+
+<!-- Insert CHANGELOG.md from the root -->
+--8<-- "CHANGELOG.md"
diff --git a/docs/about/contributing.md b/docs/about/contributing.md
index 22268aff40ceec2bf56c20f048d368284b2ec875..924bda09a20971c4e5df91d38e7da5b9158f989c 100644
--- a/docs/about/contributing.md
+++ b/docs/about/contributing.md
@@ -19,21 +19,21 @@ CI pipeline: `.gitlab-ci-docs.yml`
 ## Server
 > `go.mod`, `go.sum`, `cmd/`, `internal/`, `pkg/`
 
-The Stream TV Server is written in [Go](https://golang.org). It uses [Go Modules](https://github.com/golang/go/wiki/Modules).
+The Stream Server is written in [Go](https://golang.org). It uses [Go Modules](https://github.com/golang/go/wiki/Modules).
 `go.mod` and `go.sum` are for the module system to ensure [Reproducible builds](https://reproducible-builds.org/).
 
-The `cmd/` directory contains only the `stream-tv-server` command. It sets up the HTTP server and forwards requests to the respective `website`, `twitch` and `tv` handlers in `internal/`.  
+The `cmd/` directory contains only the `stream-server` command. The code of the server is in `internal/server` and the static files for the playback website in `internal/static`.  
 `pkg/` contains project independent code to access the Twitch API. This separation is done in accordance to the [Standard Go Project Layout](https://github.com/golang-standards/project-layout).
 
-Local development (requires go 1.12 and a twitch client id):
+Local development (requires go 1.12 and a Twitch client id):
 
 ``` bash tab="bash" linenums="1"
-env TWITCH_CLIENT_ID="your client id here" go run ./cmd/stream-tv-server/main.go
+env TWITCH_CLIENT_ID="your client id here" go run ./cmd/stream-server
 ```
 
 ``` PowerShell tab="PowerShell" linenums="1"
 $env:TWITCH_CLIENT_ID="your client id here"
-go run .\cmd\stream-tv-server\main.go
+go run .\cmd\stream-server
 ```
 
 CI pipeline: `.gitlab-ci-server.yml`
diff --git a/docs/android-integration.md b/docs/android-integration.md
new file mode 100644
index 0000000000000000000000000000000000000000..f408a7cb5d9d48c7e715a0c357c4ab7d923cbb63
--- /dev/null
+++ b/docs/android-integration.md
@@ -0,0 +1,17 @@
+# Android Integration
+
+This is a very simple helper to call the Stream Server. See the [reference](reference.md) for examples (select the `StreamServerClient.java` example tab).
+
+
+!!! note
+    The abstraction level is about on the same as the `HttpRequest.java` of the original TV-Server. The students still have to implement AsyncTask and all the other stuff.
+    Also the package definition in the first line needs to be updated.
+
+## `StreamServerClient.java`
+
+[Download](../assets/StreamServerClient.java) · [View in GitLab](https://code.fbi.h-da.de/stream-server/stream-server/blob/master/docs/assets/StreamServerClient.java)
+
+<!-- Insert StreamServerClient.java -->
+``` java linenums="1"
+--8<-- "docs/assets/StreamServerClient.java"
+```
\ No newline at end of file
diff --git a/docs/assets/StreamTV.java b/docs/assets/StreamServerClient.java
similarity index 75%
rename from docs/assets/StreamTV.java
rename to docs/assets/StreamServerClient.java
index aa1ae9d636b05c6bc8f6440457501eeb0b266212..8b228469eff67c61ec591509c744058711809f1b 100644
--- a/docs/assets/StreamTV.java
+++ b/docs/assets/StreamServerClient.java
@@ -1,4 +1,4 @@
-package de.simonkirsten.streamtvexampleapp;
+package de.simonkirsten.streamserverclient;
 
 import android.net.Uri;
 import android.support.annotation.NonNull;
@@ -19,27 +19,27 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 
-public class StreamTV {
+public class StreamServerClient {
     private Uri baseUri;
 
     /**
-     * Creates a new instance of StreamTV.
+     * Creates a new instance of StreamServerClient.
      * The instance is very lightweight so it is okay to create a new instance for each request.
      *
      * @param ipAddressWithPort The IP address with port e.g. "10.0.2.2:8080"
      */
-    public StreamTV(String ipAddressWithPort) {
+    public StreamServerClient(String ipAddressWithPort) {
         baseUri = Uri.parse("http://" + ipAddressWithPort);
     }
 
     /**
-     * Creates a new instance of StreamTV.
+     * Creates a new instance of StreamServerClient.
      * The instance is very lightweight so it is okay to create a new instance for each request.
      *
      * @param ipAddress The IP address e.g. "10.0.2.2"
      * @param port The port e.g. 8080
      */
-    public StreamTV(String ipAddress, int port) {
+    public StreamServerClient(String ipAddress, int port) {
         baseUri = Uri.parse("http://" + ipAddress + ":" + port);
     }
 
@@ -92,8 +92,8 @@ public class StreamTV {
     }
 
     /**
-     * Updates and returns the new state of the TV.
-     * Reference at <a href="https://simons-nzse-2.h-da.io/stream-tv/server/reference/#tvstate">https://simons-nzse-2.h-da.io/stream-tv/server/reference/#tvstate</a>
+     * Updates and returns the new state of the display.
+     * Reference at <a href="https://stream-server.h-da.io/reference#display">https://stream-server.h-da.io/reference#display</a>
      *
      * @param large_channel Sets the channel name of the large screen.
      *                      This screen is the always visible main screen.
@@ -118,7 +118,7 @@ public class StreamTV {
      * @param show_chat Sets whether the chat should be shown or not.
      *                  Set to null to keep the old value.
      */
-    public JSONObject tvState(@Nullable String large_channel, @Nullable String small_channel, @Nullable Float volume, @Nullable Float small_scale, @Nullable Boolean show_chat) throws IOException, JSONException {
+    public JSONObject display(@Nullable String large_channel, @Nullable String small_channel, @Nullable Float volume, @Nullable Float small_scale, @Nullable Boolean show_chat) throws IOException, JSONException {
         Map<String, String> params = new HashMap<>();
         if (large_channel != null) {
             params.put("large_channel", large_channel);
@@ -140,38 +140,38 @@ public class StreamTV {
             params.put("show_chat", show_chat.toString());
         }
 
-        String content = doRequest("/tv/state", params);
+        String content = doRequest("/display", params);
         return new JSONObject(content);
     }
 
     /**
      * Gets the most popular games on Twitch right now.
-     * Reference at <a href="https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchgamestop">https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchgamestop</a>
+     * Reference at <a href="https://stream-server.h-da.io/reference#twitchgettopgames">https://stream-server.h-da.io/reference#twitchgamestop</a>
      */
-    public List<JSONObject> twitchGamesTop() throws IOException, JSONException {
-        String content = doRequest("/twitch/games/top", null);
+    public List<JSONObject> twitchGetTopGames() throws IOException, JSONException {
+        String content = doRequest("/twitch/getTopGames", null);
 
         return jsonArrayToJsonObjectList(new JSONArray(content));
     }
 
     /**
      * Searches games by query.
-     * Reference at <a href="https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchgamessearch">https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchgamessearch</a>
+     * Reference at <a href="https://stream-server.h-da.io/reference#twitchsearchgames">https://stream-server.h-da.io/reference#twitchsearchgames</a>
      *
      * @param query What should be searched.
      */
-    public List<JSONObject> twitchGamesSearch(@NonNull String query) throws IOException, JSONException {
+    public List<JSONObject> twitchSearchGames(@NonNull String query) throws IOException, JSONException {
         Map<String, String> params = new HashMap<>();
         params.put("query", query);
 
-        String content = doRequest("/twitch/games/search", params);
+        String content = doRequest("/twitch/searchGames", params);
 
         return jsonArrayToJsonObjectList(new JSONArray(content));
     }
 
     /**
      * Gets the most popular streams on Twitch right now. Optionally filter by channels, game and language.
-     * Reference at <a href="https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchstreamstop">https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchstreamstop</a>
+     * Reference at <a href="https://stream-server.h-da.io/reference#twitchgettopstreams">https://stream-server.h-da.io/reference#twitchgettopstreams</a>
      *
      * @param channels Specify up to 100 channels separated by `,` that the search should be limited to.
      *                 See the examples in the reference why that would be useful.
@@ -180,7 +180,7 @@ public class StreamTV {
      *
      * @param language Specify a language that the search should be limited to.
      */
-    public List<JSONObject> twitchStreamsTop(@Nullable String[] channels, @Nullable String game, @Nullable String language) throws IOException, JSONException {
+    public List<JSONObject> twitchGetTopStreams(@Nullable String[] channels, @Nullable String game, @Nullable String language) throws IOException, JSONException {
         Map<String, String> params = new HashMap<>();
         if (channels != null) {
             params.put("channels", TextUtils.join(",", channels));
@@ -194,17 +194,17 @@ public class StreamTV {
             params.put("language", language);
         }
 
-        String content = doRequest("/twitch/streams/top", params);
+        String content = doRequest("/twitch/getTopStreams", params);
 
         return jsonArrayToJsonObjectList(new JSONArray(content));
     }
 
     /**
      * Gets the featured games on Twitch's homepage.
-     * Reference at <a href="https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchstreamsfeatured">https://simons-nzse-2.h-da.io/stream-tv/server/reference/#twitchstreamsfeatured</a>
+     * Reference at <a href="https://stream-server.h-da.io/reference#twitchgetfeaturedstreams">https://stream-server.h-da.io/reference#twitchgetfeaturedstreams</a>
      */
-    public List<JSONObject> twitchStreamsFeatured() throws IOException, JSONException {
-        String content = doRequest("/twitch/streams/featured", null);
+    public List<JSONObject> twitchGetFeaturedStreams() throws IOException, JSONException {
+        String content = doRequest("/twitch/getFeaturedStreams", null);
 
         return jsonArrayToJsonObjectList(new JSONArray(content));
     }
diff --git a/docs/changelog.md b/docs/changelog.md
deleted file mode 100644
index 7559bcc89075db1b068775cde60a187e8b9a8422..0000000000000000000000000000000000000000
--- a/docs/changelog.md
+++ /dev/null
@@ -1,5 +0,0 @@
-!!! note
-    To upgrade the Stream TV Server to the latest version just replace the executable with the newest from [Downloads](/server/#download). There are no dependencies.
-
-<!-- Insert CHANGELOG.md from the root -->
---8<-- "CHANGELOG.md"
diff --git a/docs/download-and-run.md b/docs/download-and-run.md
new file mode 100644
index 0000000000000000000000000000000000000000..e78b6462686bb89ad4632b31f65870e93bb8a3c2
--- /dev/null
+++ b/docs/download-and-run.md
@@ -0,0 +1,66 @@
+# Download and Run
+
+## Download
+
+Version `
+--8<-- "version"
+`
+
+OS              | CPU Platform  | Download
+--------------- | ------------- | ----------------------------------------------------------------------
+Windows			| 64 bit		| [stream-server.exe](../binaries/windows-x86_64/stream-server.exe)
+&nbsp;			| 32 bit		| [stream-server.exe](../binaries/windows-x86/stream-server.exe)
+Linux 			| 64 bit		| [stream-server](../binaries/linux-x86_64/stream-server)
+&nbsp;			| 32 bit		| [stream-server](../binaries/linux-x86/stream-server)
+&nbsp;			| ARM64			| [stream-server](../binaries/linux-arm64/stream-server)
+&nbsp;			| ARM32			| [stream-server](../binaries/linux-arm/stream-server)
+Darwin (macOS)	| 64 bit		| [stream-server](../binaries/darwin-x86_64/stream-server)
+&nbsp;			| 32 bit		| [stream-server](../binaries/darwin-x86/stream-server)
+
+??? expert info
+	If you do not wish to use a precompiled executable you can [compile it yourself](../about/contributing/#server).
+
+## Run
+
+### Windows
+1. Just double-click the exe or run it in a cmd or PowerShell `.\stream-server.exe`.
+2. *Optional:* Provide [command-line options](options.md).
+
+!!! warning
+	If you get [this](../assets/windows-protected-your-pc.png) *"Windows protected your PC"* warning press <kbd>More info</kbd> and <kbd>Run anyway</kbd>.
+
+### Linux
+1. Assign the file execute permission in a terminal: `chmod +x stream-server`.
+2. Run it from the terminal `./stream-server`.
+3. *Optional:* Provide [command-line options](options.md).
+
+### Darwin (macOS) <small>in the Terminal</small>
+1. Assign the file execute permission in a terminal: `chmod +x stream-server`.
+2. Run it from the terminal `./stream-server`.
+3. *Optional:* Provide [command-line options](options.md).
+
+### Darwin (macOS) <small>from the GUI</small>
+1. Assign the file execute permission in a terminal: `chmod +x stream-server`.
+
+	!!! important
+		This is important as otherwise macOS will try to edit the file in TextEdit when you open it.
+
+2. In Finder right click the file and <kbd>Open</kbd>.
+3. Confirm the dialog again with <kbd>Open</kbd>.
+
+!!! info
+	After the first launch the file can just be double-clicked.  
+	Read more: [Open an app from an unidentified developer](https://help.apple.com/machelp/mac/10.12/index.html?localePath=en.lproj#/mh40616).
+
+## Common Errors
+
+*	listen tcp 0.0.0.0:80: bind: permission denied
+
+	> The port range below 1024 is restricted on \*nix systems to superusers ([more info](https://unix.stackexchange.com/a/16568)).  
+	> Either run as superuser `sudo ./stream-server -p 80` or change the port.
+
+* 	listen tcp 0.0.0.0:8080: bind: address already in use
+
+	
+	> Something is already using the port. This could be another Stream Server instance or some other application (like a webserver).  
+	> Close / disable the other service or change the port via the [command-line options](options.md).
diff --git a/docs/index.md b/docs/index.md
index 1b394381e7bf04e395ba23386f99dca1a8768d8b..b4f29648e045b093b13c278e648f878015b1eb9c 100644
--- a/docs/index.md
+++ b/docs/index.md
@@ -1,6 +1,6 @@
-# Stream TV Project
+# Stream Server
 
-Stream TV is a project developed at Darmstadt University of Applied Sciences [h-da.de](https://h-da.de) for the User-Centric Software Development hands-on training. In the course the students develop an Android app that remote controls the Stream TV Server.
+Stream Server is a project developed at Darmstadt University of Applied Sciences [h-da.de](https://h-da.de) for the User-Centric Software Development hands-on training. In the course the students develop an Android app that remote controls the Stream Server.
 
 This server displays streams of the popular [Twitch.tv](https://twitch.tv) livestream platform on the student's target device (laptop, desktop, tv, smart device, etc.).
 The apps can request the server via a simple HTTP API to query the Twitch API for content and to change the state of the display (e.g. control playback).
@@ -17,14 +17,11 @@ The goal was to
 
 ## Documentation layout
 
-- **Project**  
-  _What you are reading right now_ and [Changelog](changelog.md).
-
-- **Server**  
-  [Download and Run](server/index.md), [Options](server/options.md), [Quickstart](server/quickstart.md), [Reference](server/reference.md) and [Java integration](server/streamtv-java.md).
+- **Stream Server**  
+  _What you are reading right now_, [Download and Run](download-and-run.md), [Options](options.md), [Quickstart](quickstart.md), [Reference](reference.md) and [Android Integration](android-integration.md).
 
 - **About**  
-  [Contributing info (code layout, build and deploy instructions etc.)](about/contributing.md), [Author's notes](about/authors-notes.md), [Previous considerations](about/previous-considerations.md) and [License](about/license.md).
+  [Changelog](about/changelog.md), [Contributing info (code layout, build and deploy instructions etc.)](about/contributing.md), [Author's Notes](about/authors-notes.md), [Previous Considerations](about/previous-considerations.md) and [License](about/license.md).
 
 
 !!! info
diff --git a/docs/server/options.md b/docs/options.md
similarity index 76%
rename from docs/server/options.md
rename to docs/options.md
index f5ae0d9e6fe71fadfd790f9da67cfaabb74c0604..b63fbdbc2b3b50596a48b517fdf90c09c7f247f2 100644
--- a/docs/server/options.md
+++ b/docs/options.md
@@ -7,17 +7,17 @@
 	This is totally fine for most users. You can skip this page.
 
 ```bash
-$ ./stream-tv-server --help
+$ ./stream-server --help
 ```
 ```
 NAME:
-   stream-tv-server.exe - Stream TV Server https://simons-nzse-2.h-da.io/stream-tv/server/
+   stream-server - https://stream-server.h-da.io
 
 USAGE:
-   stream-tv-server.exe [global options] [arguments...]
+   stream-server [global options] [arguments...]
 
 VERSION:
-   v0.9.1
+   v0.9.9
 
 GLOBAL OPTIONS:
    --port value, -p value  http port to listen on (default: 8080)
@@ -27,7 +27,7 @@ GLOBAL OPTIONS:
    --version, -v           print the version
 ```
 
-By default stream-tv-server listens on all ip addresses on port 8080. This means that it is exposed to the local network (LAN / WLAN) which is needed if the app is on a phone. Note that the phone must be in the same network as the computer / laptop.
+By default stream-server listens on all ip addresses on port 8080. This means that it is exposed to the local network (LAN / WLAN) which is needed if the app is on a phone. Note that the phone must be in the same network as the computer / laptop.
 
 When using the Android Emulator on the same device as the server this exposure is not necessary. Use the `--local` flag to only listen on `127.0.0.1` aka `localhost`. The emulator must then connect to `10.0.2.2` as described here:
 
diff --git a/docs/server/quickstart.md b/docs/quickstart.md
similarity index 61%
rename from docs/server/quickstart.md
rename to docs/quickstart.md
index d6b74f1d8d07b2b862d32a263d76f7bdfacb6f6d..ee35a11db723dc8de2b318cc136a1b3f30f9ceb4 100644
--- a/docs/server/quickstart.md
+++ b/docs/quickstart.md
@@ -1,33 +1,30 @@
 # Quickstart <small>(learning by doing)</small>
 
 ```bash
-$ ./stream-tv-server
+$ ./stream-server
 ```
 ```
 Serving on
   http://127.0.0.1:8080
-  http://192.168.0.136:8080
-Read the documentation at https://simons-nzse-2.h-da.io/stream-tv/server/usage/ on how to use this server.
+  http://192.168.0.66:8080
+Read the quickstart at https://stream-server.h-da.io/quickstart to get started.
 Stop with Ctrl-C or close this terminal.
 ```
 
 !!! note
-    In the examples of this documentation only the local address is used. If you want to connect from your phone or the emulator you need to use the network address (`192.168.0.136` in this case). Also we assume you use the default port `8080`.
+    In the examples of this documentation only the local address is used. If you want to connect from your phone or the emulator you need to use the network address (`192.168.0.66` in this case). Also we assume you use the default port `8080`.
 
 
 1. Open <http://127.0.0.1:8080> in a browser. Leave this tab / window open while you do the other steps.
-2. Get the current state of the tv:
+2. Get the current state of the display:
 
-    ``` md tab="Browser"
-    > http://127.0.0.1:8080/tv/state
-    ```
-
-    ``` java tab="StreamTV.java" linenums="1"
-    StreamTV streamTV = new StreamTV("10.0.2.2:8080");
+    <pre><code><http://127.0.0.1:8080/display></code></pre>
+    ``` java
+    StreamServerClient client = new StreamServerClient("10.0.2.2:8080");
 
-    JSONObject state = streamTV.tvState(null, null, null, null, null);
+    JSONObject state = client.display(null, null, null, null, null);
     ```
-
+    
     !!! example
         ``` json
         {
@@ -41,14 +38,9 @@ Stop with Ctrl-C or close this terminal.
 
 3. Find some stream on twitch:
 
-    ``` md tab="Browser"
-    > http://127.0.0.1:8080/twitch/streams/top
-    ```
-
-    ``` java tab="StreamTV.java" linenums="1"
-    StreamTV streamTV = new StreamTV("10.0.2.2:8080");
-
-    List<JSONObject> topStreams = streamTV.twitchStreamsTop(null, null, null); // no filters
+    <pre><code><http://127.0.0.1:8080/twitch/getTopStreams></code></pre>
+    ``` java
+    List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, null); // no filters
     ```
 
     !!! example
@@ -72,16 +64,11 @@ Stop with Ctrl-C or close this terminal.
         ]
         ```
 
-4. Start playback of some stream with chat and muted volume:
-
-    ``` md tab="Browser"
-    > http://127.0.0.1:8080/tv/state?large_channel={channel name here}&volume=0&show_chat=true
-    ```
-
-    ``` java tab="StreamTV.java" linenums="1"
-    StreamTV streamTV = new StreamTV("10.0.2.2:8080");
+4. Start playback of your selected stream with chat and muted volume:
 
-    JSONObject state = streamTV.tvState("channel name here", null, 0.0f, null, true);
+    <pre><code><http://127.0.0.1:8080/display?large_channel={channel name here}&volume=0&show_chat=true></code></pre>
+    ``` java
+    client.display("channel name here", null, 0.0f, null, true);
     ```
 
     !!! example
diff --git a/docs/reference.md b/docs/reference.md
new file mode 100644
index 0000000000000000000000000000000000000000..f3d3fc2f9cb2dc715b69c165ab5dbf9211a2a617
--- /dev/null
+++ b/docs/reference.md
@@ -0,0 +1,191 @@
+# Reference
+
+## `/display`
+
+> Updates and returns the new state of the display.
+
+Optional Parameters:
+
+| Name            | Type   | Description |
+| --------------- | ------ | ----------- |
+| `large_channel` | string | Sets the channel name of the large screen.<br>This screen is the always visible main screen.<br>Set to *empty* to disable (see examples). |
+| `small_channel` | string | Sets the channel name of the small screen.<br>This screen is a small Picture-in-Picture like screen in the top right corner.<br>Set to *empty* to disable (see examples). |
+| `volume`        | float  | Sets the volume of the large screen.<br>Set to `#!java 0` or `#!java 0.0` to mute.<br>Values are in the range of `#!java 0.0 - 1.0`.<br> Note: The small screen is always muted.|
+| `small_scale`   | float  | Sets the scale (size) of the small screen.<br>The value is relative to the width of the large screen.<br>Reasonable values are in the range of `#!java 0.2 - 0.5`.<br>It is best to just experiment with this setting. |
+| `show_chat`     | bool   | Sets whether the chat should be shown or not.<br>Accepts `#!java true` and `#!java false`. |
+
+!!! note
+    All parameters are optional and can be combined. If none are supplied no updates are done and the current state gets returned.
+
+Returns [`DisplayState`](#displaystate).
+
+??? example "Examples"
+    *   Only set the large_channel to riotgames
+        <pre><code><http://127.0.0.1:8080/display?large_channel=riotgames></code></pre>
+        ``` java
+        /* JSONObject newState = */ client.display("riotgames", null, null, null, null);
+        ```
+
+    *   Set the large_channel to monstercat, disable the small_channel, set the volume to 0.8 and hide the chat.  
+        Notice the small_scale is not changed (because of its absence)
+        <pre><code><http://127.0.0.1:8080/display?large_channel=monstercat&small_channel=&volume=0.8&show_chat=false></code></pre>
+        ``` java
+        /* JSONObject newState = */ client.display("monstercat", "", 0.8f, null, false);
+        ```
+
+---
+
+## `/twitch`
+
+### `/twitch/getTopGames`
+
+> Gets the most popular games on Twitch right now.
+
+*No Parameters*
+
+Returns [`Game`](#game) array.
+
+??? example "Examples"
+    *   Get top games
+        <pre><code><http://127.0.0.1:8080/twitch/getTopGames></code></pre>
+        ``` java
+        List<JSONObject> topGames = client.twitchGetTopGames();
+        ```
+
+---
+
+### `/twitch/searchGames`
+> Searches games by query.
+
+Parameters:
+
+| Name      | Type   | Description |
+| --------- | ------ | ----------- |
+| `query`   | string | What should be searched.<br>**Required** |
+
+Returns [`Game`](#game) array.
+
+??? example "Examples"
+    *   Search for the "game" Talk Shows & Podcasts
+        <pre><code><http://127.0.0.1:8080/twitch/searchGames?query=talk%20show></code></pre>
+        ``` java
+        List<JSONObject> foundGames = client.twitchSearchGames("talk show");
+        ```
+
+    *   Search for ove (will show games starting with "ove" like Overwatch)
+        <pre><code><http://127.0.0.1:8080/twitch/searchGames?query=ove></code></pre>
+        ``` java
+        List<JSONObject> foundGames = client.twitchSearchGames("ove");
+        ```
+
+---
+
+### `/twitch/getTopStreams`
+
+> Gets the most popular streams on Twitch right now. Optionally filter by channels, game and language.
+
+Optional Parameters:
+
+| Name          | Type   | Description |
+| ------------- | ------ | ----------- |
+| `channels`    | string | Specify up to 100 channels separated by `,` that the search should be limited to.<br>See the examples why that would be useful. |
+| `game`        | string | Specify a game that the search should be limited to. |
+| `language`    | string | Specify a language that the search should be limited to. |
+
+!!! note
+    All parameters are optional and can be combined. If none are supplied the top streams for all channels, games and languages are returned.
+
+Returns [`Stream`](#stream) array.
+
+??? example "Examples"
+    *   Get the unfiltered top streams
+        <pre><code><http://127.0.0.1:8080/twitch/getTopStreams></code></pre>
+        ``` java
+        List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, null);
+        ```
+
+    *   German top streams
+        <pre><code><http://127.0.0.1:8080/twitch/getTopStreams?language=de></code></pre>
+        ``` java
+        List<JSONObject> topStreams = client.twitchGetTopStreams(null, null, "de");
+        ```
+
+    *   English top streams in Talk Shows and Podcasts
+        <pre><code><http://127.0.0.1:8080/twitch/getTopStreams?language=en&game=Talk%20Shows%20%26%20Podcasts></code></pre>
+        ``` java
+        List<JSONObject> topStreams = client.twitchGetTopStreams(null, "Talk Shows and Podcasts", "en");
+        ```
+
+    *   Tip: If your app implements a favorite list you can very easily query these channels and see who is streaming and other information
+        <pre><code><http://127.0.0.1:8080/twitch/getTopStreams?channels=xqcow,dafran,kitboga></code></pre>
+        ``` java
+        String[] favoriteChannels = {"xqcow", "dafran", "kitboga"};
+
+        List<JSONObject> topStreams = client.twitchGetTopStreams(favoriteChannels, null, null);
+        ```
+
+---
+
+### `/twitch/getFeaturedStreams`
+
+> Gets the featured streams on Twitch's homepage.
+
+*No Parameters*
+
+Returns [`Stream`](#stream) array.
+
+??? example "Examples"
+    *   Get featured streams
+        <pre><code><http://127.0.0.1:8080/twitch/getFeaturedStreams></code></pre>
+        ``` java
+        List<JSONObject> featuredStreams = client.twitchGetFeaturedStreams();
+        ```
+
+---
+
+## JSON Structures <small>(by Example)</small>
+
+### DisplayState
+``` json
+{
+	"large_channel": "xqcow",
+	"small_channel": "",
+	"volume": 0.75,
+	"small_scale": 0.25,
+	"show_chat": true
+}
+```
+
+### Game
+``` json
+{
+    "name": "Grand Theft Auto V",
+    "viewers": 118716,
+    "box_img_url": "https://static-cdn.jtvnw.net/ttv-boxart/Grand%20Theft%20Auto%20V-272x380.jpg",
+    "logo_img_url": "https://static-cdn.jtvnw.net/ttv-logoart/Grand%20Theft%20Auto%20V-240x144.jpg"
+}
+```
+
+### Stream
+``` json
+{
+    "average_fps": 60,
+    "started_at": "2019-08-20T19:54:47Z",
+    "game": "Fortnite",
+    "preview_img_url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_tfue-640x360.jpg",
+    "video_height": 1080,
+    "viewers": 42870,
+    "status": "High Kill Solos",
+    "language": "en",
+    "mature": false,
+    "channel_name": "tfue",
+    "channel_display_name": "Tfue",
+    "channel_logo_img_url": "https://static-cdn.jtvnw.net/jtv_user_pictures/2470b5c6-a737-4ba6-8987-c28e0ca839e1-profile_image-300x300.jpg"
+}
+```
+
+!!! note
+    * The image at the `preview_img_url` url updates itself every couple of seconds. If the app uses preview images it could also.
+    * `started_at` is an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp.
+    * `video_height`: `#!json 1080` = `HD`, `#!json < 1080` = `SD`, `#!json > 1080` = `UHD`.
+    * `mature`: If the stream is meant for mature audiences (set by the streamer themselves as a guideline).
\ No newline at end of file
diff --git a/docs/server/index.md b/docs/server/index.md
deleted file mode 100644
index e4e1e4d089abdc7383acbe3102876f76a2cc0715..0000000000000000000000000000000000000000
--- a/docs/server/index.md
+++ /dev/null
@@ -1,68 +0,0 @@
-# Stream TV Server
-
-## Download
-
-Version `
---8<-- "version"
-`
-
-OS              | CPU Platform  | Download
---------------- | ------------- | ----------------------------------------------------------------------
-Windows			| 64 bit		| [stream-tv-server.exe](../binaries/windows-x86_64/stream-tv-server.exe)
-&nbsp;			| 32 bit		| [stream-tv-server.exe](../binaries/windows-x86/stream-tv-server.exe)
-Linux 			| 64 bit		| [stream-tv-server](../binaries/linux-x86_64/stream-tv-server)
-&nbsp;			| 32 bit		| [stream-tv-server](../binaries/linux-x86/stream-tv-server)
-&nbsp;			| ARM64			| [stream-tv-server](../binaries/linux-arm64/stream-tv-server)
-&nbsp;			| ARM32			| [stream-tv-server](../binaries/linux-arm/stream-tv-server)
-Darwin (macOS)	| 64 bit		| [stream-tv-server](../binaries/darwin-x86_64/stream-tv-server)
-&nbsp;			| 32 bit		| [stream-tv-server](../binaries/darwin-x86/stream-tv-server)
-
-??? expert info
-	If you do not wish to use a precompiled executable you can [compile it yourself](../about/contributing/#server).
-
-## Run
-
-### Windows
-1. Just double-click the exe or run it in a cmd or PowerShell `.\stream-tv-server.exe`.
-2. *Optional:* Provide [command-line options](options.md).
-
-!!! warning
-	If you get [this](../assets/windows-protected-your-pc.png) *"Windows protected your PC"* warning press <kbd>More info</kbd> and <kbd>Run anyway</kbd>.
-
-### Linux
-1. Assign the file execute permission in a terminal: `chmod +x stream-tv-server`.
-2. Run it from the terminal `./stream-tv-server`.
-3. *Optional:* Provide [command-line options](options.md).
-
-### Darwin (macOS) <small>in the Terminal</small>
-1. Assign the file execute permission in a terminal: `chmod +x stream-tv-server`.
-2. Run it from the terminal `./stream-tv-server`.
-3. *Optional:* Provide [command-line options](options.md).
-
-### Darwin (macOS) <small>from the GUI</small>
-1. Assign the file execute permission in a terminal: `chmod +x stream-tv-server`.
-
-	!!! important
-		This is important as otherwise macOS will try to edit the file in TextEdit when you open it.
-
-2. In Finder right click the file and <kbd>Open</kbd>.
-3. Confirm the dialog again with <kbd>Open</kbd>.
-
-!!! info
-	After the first launch the file can just be double-clicked.  
-	Read more: [Open an app from an unidentified developer](https://help.apple.com/machelp/mac/10.12/index.html?localePath=en.lproj#/mh40616).
-
-## Common Errors
-
-- 	<small>`listen tcp 0.0.0.0:80: bind: permission denied`</small>
-
-	The port range below 1024 is restricted on \*nix systems to superusers ([more info](https://unix.stackexchange.com/a/16568)).  
-	Either run as superuser `sudo ./stream-tv-server -p 80` or change the port.
-
-- 	<small>`listen tcp 0.0.0.0:8080: bind: address already in use`</small>
-
-	<small>`listen tcp 0.0.0.0:8080: bind: Only one usage of each socket address (protocol/network address/port) is normally permitted.`</small>
-
-	Something is already using the port. This could be another Stream TV Server instance or some other application (like a webserver).
-
-	Close / disable the other service or change the port via the [command-line options](options.md).
diff --git a/docs/server/reference.md b/docs/server/reference.md
deleted file mode 100644
index 49b081de6fa0c28a318f3a44f10c8fb9168110af..0000000000000000000000000000000000000000
--- a/docs/server/reference.md
+++ /dev/null
@@ -1,232 +0,0 @@
-# Reference
-
-## `/tv`
-
-### `/tv/state`
-
-> Updates and returns the new state of the TV.
-
-Optional Parameters:
-
-| Name            | Type   | Description |
-| --------------- | ------ | ----------- |
-| `large_channel` | string | Sets the channel name of the large screen.<br>This screen is the always visible main screen.<br>Set to *empty* to disable (see examples). |
-| `small_channel` | string | Sets the channel name of the small screen.<br>This screen is a small Picture-in-Picture like screen in the top right corner.<br>Set to *empty* to disable (see examples). |
-| `volume`        | float  | Sets the volume of the large screen.<br>Set to `#!java 0` or `#!java 0.0` to mute.<br>Values are in the range of `#!java 0.0 - 1.0`.<br> Note: The small screen is always muted.|
-| `small_scale`   | float  | Sets the scale (size) of the small screen.<br>The value is relative to the width of the large screen.<br>Reasonable values are in the range of `#!java 0.2 - 0.5`.<br>It is best to just experiment with this setting. |
-| `show_chat`     | bool   | Sets whether the chat should be shown or not.<br>Accepts `#!java true` and `#!java false`. |
-
-!!! note
-    All parameters are optional and can be combined. If none are supplied no updates are done and the current state gets returned.
-
-Returns: [`TVState`](#tvstate_1).
-
-Examples:
-
-``` md tab="Browser"
-Only set the large_channel to riotgames
-> http://127.0.0.1:8080/tv/state?large_channel=riotgames
-
-Set the large_channel to monstercat
-Disable the small_channel
-Set the volume to 0.8
-The small_scale is not changed (because of its absence)
-Hide the chat
-> http://127.0.0.1:8080/tv/state?large_channel=monstercat&small_channel=&volume=0.8&show_chat=false
-```
-
-``` java tab="StreamTV.java" linenums="1"
-StreamTV streamTV = new StreamTV("10.0.2.2:8080");
-
-// Only set the large_channel to riotgames
-JSONObject newState = streamTV.tvState("riotgames", null, null, null, null);
-
-// Set the large_channel to monstercat
-// Disable the small_channel
-// Set the volume to 0.8
-// The small_scale is not changed (because of null)
-// Hide the chat
-JSONObject newState = streamTV.tvState("monstercat", "", 0.8f, null, false);
-```
-
----
-
-## `/twitch`
-
-### `/twitch/games/top`
-
-> Gets the most popular games on Twitch right now.
-
-*No Parameters*
-
-Returns: [`Game`](#game) array.
-
-Examples:
-
-``` md tab="Browser"
-> http://127.0.0.1:8080/twitch/games/top
-```
-
-``` java tab="StreamTV.java" linenums="1"
-StreamTV streamTV = new StreamTV("10.0.2.2:8080");
-
-List<JSONObject> topGames = streamTV.twitchGamesTop();
-```
-
----
-
-### `/twitch/games/search`
-> Searches games by query.
-
-Parameters:
-
-| Name      | Type   | Description |
-| --------- | ------ | ----------- |
-| `query`   | string | What should be searched.<br>**Required** |
-
-Returns: [`Game`](#game) array.
-
-Examples:
-
-``` md tab="Browser"
-Search for the game Talk Shows
-> http://127.0.0.1:8080/twitch/games/search?query=Talk%20Shows
-Search for ove (will show games starting with "ove" like Overwatch)
-> http://127.0.0.1:8080/twitch/games/search?query=ove
-```
-
-``` java tab="StreamTV.java" linenums="1"
-StreamTV streamTV = new StreamTV("10.0.2.2:8080");
-
-// Search for the game Talk Shows & Podcasts
-List<JSONObject> topGames = streamTV.twitchGamesSearch("talk show");
-
-// Search for ove (will show games starting with "ove" like Overwatch)
-List<JSONObject> topGames = streamTV.twitchGamesSearch("ove");
-```
-
----
-
-### `/twitch/streams/top`
-
-> Gets the most popular streams on Twitch right now. Optionally filter by channels, game and language.
-
-Optional Parameters:
-
-| Name          | Type   | Description |
-| ------------- | ------ | ----------- |
-| `channels`    | string | Specify up to 100 channels separated by `,` that the search should be limited to.<br>See the examples why that would be useful. |
-| `game`        | string | Specify a game that the search should be limited to. |
-| `language`    | string | Specify a language that the search should be limited to. |
-
-!!! note
-    All parameters are optional and can be combined. If none are supplied the top streams for all channels, games and languages are returned.
-
-Returns: [`Stream`](#stream) array.
-
-Examples:
-
-``` md tab="Browser"
-Get the unfiltered top streams
-> http://127.0.0.1:8080/twitch/streams/top
-
-German top streams
-> http://127.0.0.1:8080/twitch/streams/top?language=de
-
-English top streams in Talk Shows and Podcasts
-> http://127.0.0.1:8080/twitch/streams/top?language=en&game=Talk%20Shows%20%26%20Podcasts
-
-Tip: If your app implements a favorite list you can very easily query these channels and see who is streaming and other information:
-> http://127.0.0.1:8080/twitch/streams/top?channels={comma separated favorite channels}
-> http://127.0.0.1:8080/twitch/streams/top?channels=xqcow,dafran,kitboga
-
-```
-
-``` java tab="StreamTV.java" linenums="1"
-StreamTV streamTV = new StreamTV("10.0.2.2:8080");
-
-// Get the unfiltered top streams
-List<JSONObject> topStreams = streamTV.twitchStreamsTop(null, null, null);
-
-// German top streams
-List<JSONObject> topStreams = streamTV.twitchStreamsTop(null, null, "de");
-
-// English top streams in Talk Shows and Podcasts
-List<JSONObject> topStreams = streamTV.twitchStreamsTop(null, "Talk Shows and Podcasts", "en");
-
-// Tip: If your app implements a favorite list you can very easily query these channels and see who is streaming and other information:
-String[] favoriteChannels = {"xqcow", "dafran", "kitboga"};
-
-List<JSONObject> topStreams = streamTV.twitchStreamsTop(favoriteChannels, null, null);
-```
-
----
-
-### `/twitch/streams/featured`
-
-> Gets the featured streams on Twitch's homepage.
-
-*No Parameters*
-
-Returns: [`Stream`](#stream) array.
-
-Examples:
-
-``` tab="Browser"
-http://127.0.0.1:8080/twitch/streams/featured
-```
-
-``` java tab="StreamTV.java" linenums="1"
-StreamTV streamTV = new StreamTV("10.0.2.2:8080");
-
-List<JSONObject> featuredStreams = streamTV.twitchStreamsFeatured();
-```
-
----
-
-## JSON Structures <small>(by Example)</small>
-
-### TVState
-``` json
-{
-	"large_channel": "xqcow",
-	"small_channel": "",
-	"volume": 0.75,
-	"small_scale": 0.25,
-	"show_chat": true
-}
-```
-
-### Game
-``` json
-{
-    "name": "Grand Theft Auto V",
-    "viewers": 118716,
-    "box_img_url": "https://static-cdn.jtvnw.net/ttv-boxart/Grand%20Theft%20Auto%20V-272x380.jpg",
-    "logo_img_url": "https://static-cdn.jtvnw.net/ttv-logoart/Grand%20Theft%20Auto%20V-240x144.jpg"
-}
-```
-
-### Stream
-``` json
-{
-    "average_fps": 60,
-    "started_at": "2019-08-20T19:54:47Z",
-    "game": "Fortnite",
-    "preview_img_url": "https://static-cdn.jtvnw.net/previews-ttv/live_user_tfue-640x360.jpg",
-    "video_height": 1080,
-    "viewers": 42870,
-    "status": "High Kill Solos",
-    "language": "en",
-    "mature": false,
-    "channel_name": "tfue",
-    "channel_display_name": "Tfue",
-    "channel_logo_img_url": "https://static-cdn.jtvnw.net/jtv_user_pictures/2470b5c6-a737-4ba6-8987-c28e0ca839e1-profile_image-300x300.jpg"
-}
-```
-
-!!! note
-    * The image at the `preview_img_url` url updates itself every couple of seconds. If the app uses preview images it could also.
-    * `started_at` is an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp.
-    * `video_height`: `#!json 1080` = `HD`, `#!json < 1080` = `SD`, `#!json > 1080` = `UHD`.
-    * `mature`: If the stream is meant for mature audiences (set by the streamer himself as a guideline).
\ No newline at end of file
diff --git a/docs/server/streamtv-java.md b/docs/server/streamtv-java.md
deleted file mode 100644
index 422668eb18602ba67c95dcd7e232b9e3d2179b4e..0000000000000000000000000000000000000000
--- a/docs/server/streamtv-java.md
+++ /dev/null
@@ -1,15 +0,0 @@
-# `StreamTV.java`
-
-This is a very simple helper to call the Stream TV server. See the [reference](reference.md) for examples (select the `StreamTV.java` example tab).
-
-
-!!! note
-    The abstraction level is about on the same as the `HttpRequest.java` of the original TV-Server. The students still have to implement AsyncTask and all the other stuff.
-    Also the package definition in the first line needs to be updated.
-
-[Download](../assets/StreamTV.java) · [View in GitLab](#todo)
-
-<!-- Insert StreamTV.java -->
-``` java linenums="1"
---8<-- "docs/assets/StreamTV.java"
-```
\ No newline at end of file
diff --git a/go.mod b/go.mod
index 405b3de01ef0c3ea95354032b365cf9dc41af7a2..8892a02ce5ab74f8670d76373a23d7c84544eb68 100644
--- a/go.mod
+++ b/go.mod
@@ -1,7 +1,14 @@
-module stream-tv
+module stream-server
 
 require (
 	github.com/JamesStewy/sse v0.3.0
+	github.com/go-chi/chi v4.0.2+incompatible
+	github.com/gobuffalo/packr/v2 v2.7.1
 	github.com/hashicorp/go-multierror v1.0.0
+	github.com/rogpeppe/go-internal v1.5.0 // indirect
 	github.com/urfave/cli v1.21.0
+	golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf // indirect
+	golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c // indirect
+	gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect
+	gopkg.in/yaml.v2 v2.2.4 // indirect
 )
diff --git a/go.sum b/go.sum
index c7d7f838f41194888ff0d9d716fe82f258bf5cc2..15b6d1e102d7f85d4b40626b407114eaf1d02da4 100644
--- a/go.sum
+++ b/go.sum
@@ -1,11 +1,98 @@
 github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
 github.com/JamesStewy/sse v0.3.0 h1:1CABzcQydehMc54Vypki6b5+/WMmGZVWi7owRvDKo7g=
 github.com/JamesStewy/sse v0.3.0/go.mod h1:i60+CezIhaOZYviCbCRqXCDB7cVCWvUqlD2AYnxPVRE=
+github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
+github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
+github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
+github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
+github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
+github.com/go-chi/chi v4.0.2+incompatible h1:maB6vn6FqCxrpz4FqWdh4+lwpyZIQS7YEAUcHlgXVRs=
+github.com/go-chi/chi v4.0.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ=
+github.com/gobuffalo/envy v1.7.0/go.mod h1:n7DRkBerg/aorDM8kbduw5dN3oXGswK5liaSCx4T5NI=
+github.com/gobuffalo/envy v1.7.1 h1:OQl5ys5MBea7OGCdvPbBJWRgnhC/fGona6QKfvFeau8=
+github.com/gobuffalo/envy v1.7.1/go.mod h1:FurDp9+EDPE4aIUS3ZLyD+7/9fpx7YRt/ukY6jIHf0w=
+github.com/gobuffalo/logger v1.0.1 h1:ZEgyRGgAm4ZAhAO45YXMs5Fp+bzGLESFewzAVBMKuTg=
+github.com/gobuffalo/logger v1.0.1/go.mod h1:2zbswyIUa45I+c+FLXuWl9zSWEiVuthsk8ze5s8JvPs=
+github.com/gobuffalo/packd v0.3.0 h1:eMwymTkA1uXsqxS0Tpoop3Lc0u3kTfiMBE6nKtQU4g4=
+github.com/gobuffalo/packd v0.3.0/go.mod h1:zC7QkmNkYVGKPw4tHpBQ+ml7W/3tIebgeo1b36chA3Q=
+github.com/gobuffalo/packr/v2 v2.7.1 h1:n3CIW5T17T8v4GGK5sWXLVWJhCz7b5aNLSxW6gYim4o=
+github.com/gobuffalo/packr/v2 v2.7.1/go.mod h1:qYEvAazPaVxy7Y7KR0W8qYEE+RymX74kETFqjFoFlOc=
 github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
 github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
 github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
 github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
+github.com/joho/godotenv v1.3.0 h1:Zjp+RcGpHhGlrMbJzXTrZZPrWj+1vfm90La1wgB6Bhc=
+github.com/joho/godotenv v1.3.0/go.mod h1:7hK45KPybAkOC6peb+G5yklZfMxEjkZhHbwpqxOKXbg=
+github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
+github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
+github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
+github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
+github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
+github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
+github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
+github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
+github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
+github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
+github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
+github.com/rogpeppe/go-internal v1.3.2/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.4.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/rogpeppe/go-internal v1.5.0 h1:Usqs0/lDK/NqTkvrmKSwA/3XkZAs7ZAW/eLeQ2MVBTw=
+github.com/rogpeppe/go-internal v1.5.0/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
+github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
+github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4=
+github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
+github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
+github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
+github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
+github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
+github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
+github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
+github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
+github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
 github.com/urfave/cli v1.21.0 h1:wYSSj06510qPIzGSua9ZqsncMmWE3Zr55KBERygyrxE=
 github.com/urfave/cli v1.21.0/go.mod h1:lxDj6qX9Q6lWQxIrbrT0nwecwUtRnhVZAJjJZrVUZZQ=
+github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
+golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
+golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
+golang.org/x/crypto v0.0.0-20190621222207-cc06ce4a13d4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
+golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf h1:fnPsqIDRbCSgumaMCRpoIoF2s4qxv0xSSS0BVZUE/ss=
+golang.org/x/crypto v0.0.0-20191029031824-8986dd9e96cf/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
+golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
+golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
+golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
+golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
+golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20190515120540-06a5c4944438/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c h1:S/FtSvpNLtFBgjTqcKsRpsa6aVsI6iztaz1bQd9BJwE=
+golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
+golang.org/x/tools v0.0.0-20191004055002-72853e10c5a3/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
+golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v2 v2.2.4 h1:/eiJrUcujPVeJ3xlSWaiNi3uSVmDGBK1pDHUHAnao1I=
+gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff --git a/internal/api/twitch.go b/internal/api/twitch.go
deleted file mode 100644
index a03701eb50494198cdf06cf361f3481c6f61680d..0000000000000000000000000000000000000000
--- a/internal/api/twitch.go
+++ /dev/null
@@ -1,98 +0,0 @@
-package api
-
-import (
-	"net/http"
-	"strings"
-
-	"stream-tv/internal/util"
-	"stream-tv/pkg/twitch"
-)
-
-// TODO: this file lacks documentation. It is pretty self explanatory though.
-
-func gamesTopHandleFunc(w http.ResponseWriter, r *http.Request) {
-
-	response, err := twitch.GetTopGames()
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-
-	_, err = util.ServeJSON(w, &response)
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-}
-
-func gamesSearchHandleFunc(w http.ResponseWriter, r *http.Request) {
-	query := r.URL.Query().Get("query")
-
-	if query == "" {
-		http.Error(w, "Parameter 'query' is required and can not be empty.", http.StatusBadRequest)
-		return
-	}
-
-	response, err := twitch.SearchGames(query)
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-
-	_, err = util.ServeJSON(w, &response)
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-}
-
-func streamsTopHandleFunc(w http.ResponseWriter, r *http.Request) {
-	urlQuery := r.URL.Query()
-
-	// the following is needed as otherwise strings.Split will return [""] which we don't want
-	var channels []string
-	if channelsParam := urlQuery.Get("channels"); channelsParam != "" {
-		channels = strings.Split(channelsParam, ",")
-	}
-
-	game := urlQuery.Get("game")
-	language := urlQuery.Get("language")
-
-	response, err := twitch.GetTopStreams(channels, game, language)
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-
-	_, err = util.ServeJSON(w, &response)
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-}
-
-func streamsFeaturedHandleFunc(w http.ResponseWriter, r *http.Request) {
-	response, err := twitch.GetFeaturedStreams()
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-
-	_, err = util.ServeJSON(w, &response)
-	if err != nil {
-		util.ServeError(w, err)
-		return
-	}
-}
-
-// TwitchHandler returns a http.Handler that serves requests for the /twitch backend.
-func TwitchHandler() http.Handler {
-	mux := http.NewServeMux()
-
-	mux.HandleFunc("/twitch/games/top", gamesTopHandleFunc)
-	mux.HandleFunc("/twitch/games/search", gamesSearchHandleFunc)
-	mux.HandleFunc("/twitch/streams/top", streamsTopHandleFunc)
-	mux.HandleFunc("/twitch/streams/featured", streamsFeaturedHandleFunc)
-
-	return mux
-}
diff --git a/internal/api/tv.go b/internal/server/display.go
similarity index 54%
rename from internal/api/tv.go
rename to internal/server/display.go
index 3efb53c97ce3e23c7f4c7b7ea9ffe0b86698bb50..fd1300ecd6d6863764e782bf39444e9b1d7bd6e6 100644
--- a/internal/api/tv.go
+++ b/internal/server/display.go
@@ -1,21 +1,19 @@
-package api
+package server
 
 import (
 	"encoding/json"
-	"fmt"
 	"log"
 	"net/http"
-	"net/url"
 	"strconv"
 
 	"github.com/JamesStewy/sse"
 	"github.com/hashicorp/go-multierror"
 
-	"stream-tv/internal/util"
+	"stream-server/internal/util"
 )
 
-// tvState struct defines the state of the TV.
-type tvState struct {
+// displayState struct defines the state of the display.
+type displayState struct {
 	LargeChannel string  `json:"large_channel"`
 	SmallChannel string  `json:"small_channel"`
 	Volume       float32 `json:"volume"`
@@ -23,9 +21,9 @@ type tvState struct {
 	ShowChat     bool    `json:"show_chat"`
 }
 
-// state is the actual current state of the TV.
+// state is the current state of the display.
 // It is initialized with default values.
-var state = tvState{
+var state = displayState{
 	LargeChannel: "",
 	SmallChannel: "",
 	Volume:       0.5,
@@ -33,70 +31,50 @@ var state = tvState{
 	ShowChat:     false,
 }
 
-// getQueryParam is a helper function to get an param from url.Values while making sure the param is present and has only a single value.
-func getQueryParam(v url.Values, param string) (value string, ok bool, err error) {
-	values, ok := v[param]
-	if !ok {
-		return
-	}
-
-	valuesCount := len(values)
-
-	if valuesCount == 1 {
-		value = values[0]
-		return
-	}
-
-	err = fmt.Errorf("Got %d values for parameter '%s' but expected only one: %s", valuesCount, param, values)
-	return
-}
-
 // clients holds the clients that are connected to the event handler. It is used to broadcast state changes to all SSE (Server-Sent Events) clients.
 // Note: the only reason we use a sse.Client => bool map is that we can call *delete* with the client as key. The actual bool value that is stored holds no significance whatsoever.
 // This is basically a *set* in go.
 var clients map[*sse.Client]bool = make(map[*sse.Client]bool)
 
-// stateHandleFunc updated the state based on the query string.
+// displayStateHandler updated the state based on the query string.
 // For example
-// 	/tv/state?large_channel=asdf&small_channel=null&volume=&small_scale=0.25&show_chat=true
+// 	/display?large_channel=asdf&small_channel=null&volume=&small_scale=0.25&show_chat=true
 // will
 //  - set large_channel to asdf
-//  - reset small_channel to nil/null
+//  - reset small_channel to null
 //  - do nothing with volume (the parameter can also be omitted)
 //  - set small_scale to 0.25
 //  - set show_chat to true
-
-// If the floats (volume and small_scale) or the boolean (show_chat) could not be parsed no changes are made at all and an error gets returned.
-func stateHandleFunc(w http.ResponseWriter, r *http.Request) {
-	query := r.URL.Query()
-
+//
+// If the floats (volume and small_scale) or the boolean (show_chat) could not be parsed, no changes are made at all and an error gets returned even if other fields could be parsed.
+func displayStateHandler(w http.ResponseWriter, r *http.Request) {
 	// the new state we will replace the current state with IF everything parses correctly.
 	newState := state
 
-	// we save the error messages of the 3 parsing steps here.
+	// we save the error messages of the 5 parsing steps here.
 	var errs error
 
-	if value, ok, err := getQueryParam(query, "large_channel"); ok {
+	if value, err := util.GetSingleQueryParam(r, "large_channel"); value != nil {
 		if err != nil {
 			errs = multierror.Append(err)
 		} else {
-			newState.LargeChannel = value
+			newState.LargeChannel = *value
 		}
 	}
 
-	if value, ok, err := getQueryParam(query, "small_channel"); ok {
+	if value, err := util.GetSingleQueryParam(r, "small_channel"); value != nil {
 		if err != nil {
 			errs = multierror.Append(err)
 		} else {
-			newState.SmallChannel = value
+			newState.SmallChannel = *value
 		}
 	}
 
-	if value, ok, err := getQueryParam(query, "volume"); ok {
+	if value, err := util.GetSingleQueryParam(r, "volume"); value != nil {
 		if err != nil {
 			errs = multierror.Append(err)
 		} else {
-			newVolume, err := strconv.ParseFloat(value, 32)
+			newVolume, err := strconv.ParseFloat(*value, 32)
 			if err != nil {
 				errs = multierror.Append(errs, err)
 			} else {
@@ -105,11 +83,11 @@ func stateHandleFunc(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	if value, ok, err := getQueryParam(query, "small_scale"); ok {
+	if value, err := util.GetSingleQueryParam(r, "small_scale"); value != nil {
 		if err != nil {
 			errs = multierror.Append(err)
 		} else {
-			newSmallScale, err := strconv.ParseFloat(value, 32)
+			newSmallScale, err := strconv.ParseFloat(*value, 32)
 			if err != nil {
 				errs = multierror.Append(errs, err)
 			} else {
@@ -118,11 +96,11 @@ func stateHandleFunc(w http.ResponseWriter, r *http.Request) {
 		}
 	}
 
-	if value, ok, err := getQueryParam(query, "show_chat"); ok {
+	if value, err := util.GetSingleQueryParam(r, "show_chat"); value != nil {
 		if err != nil {
 			errs = multierror.Append(err)
 		} else {
-			newShowChat, err := strconv.ParseBool(value)
+			newShowChat, err := strconv.ParseBool(*value)
 			if err != nil {
 				errs = multierror.Append(errs, err)
 			} else {
@@ -134,38 +112,38 @@ func stateHandleFunc(w http.ResponseWriter, r *http.Request) {
 	if errs != nil { // we had errors
 		http.Error(w, errs.Error(), http.StatusBadRequest)
 
-		log.Printf("Error(s) while parsing /tv/state query: %v\n", errs)
-		return
-	}
-
-	body, err := util.ServeJSON(w, newState)
-	if err != nil {
-		http.Error(w, err.Error(), http.StatusInternalServerError)
-		log.Println(err)
+		log.Printf("Error(s) while parsing /display query: %v\n", errs)
 		return
 	}
 
 	if state != newState {
 		state = newState
-		log.Printf("Updated state (%s):\n%s\n", r.URL.RawQuery, string(body))
+
+		body, err := json.Marshal(state)
+		if err != nil {
+			return
+		}
+		log.Printf("Updated display (%s):\n%s\n", r.URL.RawQuery, string(body))
 
 		// craft the SSE message
 		msg := sse.Msg{
 			Data: string(body),
 		}
 
-		// send it to all clients
+		// broadcast it to all clients
 		for client := range clients {
 			client.Send(msg)
 		}
 	}
+
+	util.ServeIndentedJSON(w, r, &state)
 }
 
-// eventsHandleFunc serves a SSE (Server-sent events) endpoint that the website(s) can connect to. It publishes the state when it gets changed.
-func eventsHandleFunc(w http.ResponseWriter, r *http.Request) {
+// displayEventsHandler serves a SSE (Server-sent events) endpoint that the website(s) can connect to. It publishes the state when it gets changed.
+func displayEventsHandler(w http.ResponseWriter, r *http.Request) {
 	client, err := sse.ClientInit(w)
 
-	// return error if unable to initialise Server-Sent Events
+	// return error if unable to initialize sse connection
 	if err != nil {
 		http.Error(w, err.Error(), http.StatusInternalServerError)
 		return
@@ -193,13 +171,3 @@ func eventsHandleFunc(w http.ResponseWriter, r *http.Request) {
 	// run the in the context of the request
 	client.Run(r.Context())
 }
-
-// TVHandler returns a http.Handler that serves requests for the /tv backend
-func TVHandler() http.Handler {
-	mux := http.NewServeMux()
-
-	mux.HandleFunc("/tv/state", stateHandleFunc)
-	mux.HandleFunc("/tv/events", eventsHandleFunc)
-
-	return mux
-}
diff --git a/internal/server/server.go b/internal/server/server.go
new file mode 100644
index 0000000000000000000000000000000000000000..4e551b96c928789562c163c24e9ef47282444042
--- /dev/null
+++ b/internal/server/server.go
@@ -0,0 +1,39 @@
+package server
+
+import (
+	"net/http"
+
+	"github.com/go-chi/chi"
+	"github.com/gobuffalo/packr/v2"
+)
+
+// Version will automatically be set by the CI pipeline.
+var Version = "dev"
+
+// ListenAndServe will listen and serve on the provided listenAddr
+func ListenAndServe(listenAddr string) error {
+	r := chi.NewRouter()
+
+	// this 'box' by packr will automatically serve the static folder when developing
+	// but when built by the CI pipeline will pack the assets into the executable
+	staticBox := packr.New("static", "../static")
+
+	r.Route("/twitch", func(r chi.Router) {
+		r.Get("/getTopGames", twitchGetTopGamesHandler)
+		r.Get("/searchGames", twitchSearchGamesHandler)
+		r.Get("/getTopStreams", twitchGetTopStreamsHandler)
+		r.Get("/getFeaturedStreams", twitchGetFeaturedStreamsHandler)
+	})
+
+	r.Get("/display", func(w http.ResponseWriter, r *http.Request) {
+		if r.Header.Get("Accept") == "text/event-stream" {
+			displayEventsHandler(w, r)
+		} else {
+			displayStateHandler(w, r)
+		}
+	})
+
+	r.Mount("/", http.FileServer(staticBox))
+
+	return http.ListenAndServe(listenAddr, r)
+}
diff --git a/internal/server/twitch.go b/internal/server/twitch.go
new file mode 100644
index 0000000000000000000000000000000000000000..a2eb5699151e0ba5a7ab0e7719f40358ee3c1286
--- /dev/null
+++ b/internal/server/twitch.go
@@ -0,0 +1,94 @@
+package server
+
+import (
+	"net/http"
+	"os"
+
+	"stream-server/internal/util"
+	"stream-server/pkg/twitch"
+)
+
+// client is the Twitch Client instance
+var client *twitch.Client
+
+// defaultTwitchClientID will automatically be set by the CI pipeline.
+// for local builds the TWITCH_CLIENT_ID environment variable must be set
+var defaultTwitchClientID string
+
+func init() {
+	// If you want to use your own twitch client id set the TWITCH_CLIENT_ID environment variable.
+
+	if twitchClientID := os.Getenv("TWITCH_CLIENT_ID"); twitchClientID != "" {
+		client = twitch.NewClient(twitchClientID)
+	} else {
+		client = twitch.NewClient(defaultTwitchClientID)
+	}
+}
+
+// twitchGetTopGamesHandler handles the GetTopGames endpoint
+func twitchGetTopGamesHandler(w http.ResponseWriter, r *http.Request) {
+	response, err := client.GetTopGames()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadGateway)
+		return
+	}
+
+	util.ServeIndentedJSON(w, r, &response)
+}
+
+// twitchSearchGamesHandler handles the SearchGames endpoint
+func twitchSearchGamesHandler(w http.ResponseWriter, r *http.Request) {
+	paramQuery, err := util.GetSingleQueryParam(r, "query")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	if paramQuery == nil || *paramQuery == "" {
+		http.Error(w, "Parameter 'query' is required and can not be empty.", http.StatusBadRequest)
+		return
+	}
+
+	response, err := client.SearchGames(*paramQuery)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadGateway)
+		return
+	}
+
+	util.ServeIndentedJSON(w, r, &response)
+}
+
+// twitchGetTopStreamsHandler handles the GetTopStreams endpoint
+func twitchGetTopStreamsHandler(w http.ResponseWriter, r *http.Request) {
+	paramChannels := util.GetMultipleQueryParams(r, "channels")
+	game, err := util.GetSingleQueryParam(r, "game")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	language, err := util.GetSingleQueryParam(r, "language")
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadRequest)
+		return
+	}
+
+	response, err := client.GetTopStreams(paramChannels, game, language)
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadGateway)
+		return
+	}
+
+	util.ServeIndentedJSON(w, r, &response)
+}
+
+// twitchGetFeaturedStreamsHandler handles the GetFeaturedStreams endpoint
+func twitchGetFeaturedStreamsHandler(w http.ResponseWriter, r *http.Request) {
+	response, err := client.GetFeaturedStreams()
+	if err != nil {
+		http.Error(w, err.Error(), http.StatusBadGateway)
+		return
+	}
+
+	util.ServeIndentedJSON(w, r, &response)
+}
diff --git a/internal/static/favicon.ico b/internal/static/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..e4fb0e72d55aca9d83f791e1b8419e339fca21fe
Binary files /dev/null and b/internal/static/favicon.ico differ
diff --git a/internal/static/index.html b/internal/static/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..4f9d22233b88e24bfdde24bfc352809f39641057
--- /dev/null
+++ b/internal/static/index.html
@@ -0,0 +1,33 @@
+<!DOCTYPE html>
+<html>
+<head>
+	<meta charset="utf-8">
+	<meta name="viewport" content="width=device-width, initial-scale=1">
+	<title>Stream Server</title>
+
+	<script src="https://player.twitch.tv/js/embed/v1.js"></script> <!-- twitch embed -->
+
+	<link rel="stylesheet" href="style.css">
+</head>
+<body>
+	<div id="large-player" class="hidden">
+		<div id="small-player-container">
+			<div id="small-player" class="hidden"></div>
+		</div>
+	</div>
+	<iframe id="chat" frameborder="0" scrolling="no" src="about:blank"></iframe>
+
+	<div id="overlay">
+		<h2>Loading...</h2>
+		<p>
+			<a href="https://stream-server.h-da.io">Documentation</a> · 
+			<a href="https://stream-server.h-da.io/reference">Reference</a>
+		</p>
+		<small>
+			<a href="javascript:toggleFullscreen()">toggle fullscreen</a>
+		</small>
+	</div>
+
+	<script src="main.js"></script>
+</body>
+</html>
\ No newline at end of file
diff --git a/internal/static/main.js b/internal/static/main.js
new file mode 100644
index 0000000000000000000000000000000000000000..85442b8d8a36d9752e444cdbc0235a21bdafc605
--- /dev/null
+++ b/internal/static/main.js
@@ -0,0 +1,164 @@
+"use strict";
+
+var large_player;
+var small_player;
+
+function newLargePlayer(channel) {
+    return new Twitch.Player('large-player', {
+        channel: channel,
+        width: '',
+        height: '',
+        controls: false,
+        muted: false
+    });
+}
+
+function newSmallPlayer(channel) {
+    return new Twitch.Player('small-player', {
+        channel: channel,
+        width: '',
+        height: '',
+        controls: false,
+        muted: true
+    });
+}
+
+const large_player_elem = document.getElementById('large-player');
+const small_player_elem = document.getElementById('small-player');
+const overlay_elem = document.getElementById('overlay');
+const chat_elem = document.getElementById('chat');
+
+const defaultState = {
+    large_channel: '',
+    small_channel: '',
+    volume: 0.5,
+    small_scale: 0.3,
+    show_chat: false
+};
+
+let state = defaultState;
+
+function updateState(newState) {
+    state = newState;
+    
+    console.log('State:', state);
+
+    // large player:
+
+    // we need to destroy the player
+    if (state.large_channel == '' && large_player) {
+        large_player.destroy();
+        large_player = undefined;
+        chat_elem.src = 'about:blank';
+    }
+
+    // we should be playing something
+    if (state.large_channel != '') {
+
+        function update() {
+            // the volume needs to be changed
+            if (large_player.getVolume() != state.volume) {
+                large_player.setVolume(state.volume);
+            }
+
+            // the channel needs to be changed
+            if (large_player.getChannel() != state.large_channel) {
+                large_player.setChannel(state.large_channel);
+            }
+        }
+
+        // we need to create the large player
+        if (!large_player) {
+            large_player = newLargePlayer(state.large_channel);
+            large_player.addEventListener(Twitch.Player.READY, update);
+            large_player.addEventListener(Twitch.Player.PLAY, update);
+        } else {
+            update();
+        }
+
+        var chat_elem_src = 'https://www.twitch.tv/embed/' + state.large_channel + '/chat?darkpopout';
+
+        if (chat_elem_src != chat_elem.src) {
+            chat_elem.src = chat_elem_src;
+        }
+    }
+
+    // small player:
+    
+    // we need to destroy the player
+    if (state.small_channel == '' && small_player) {
+        small_player.destroy();
+        small_player = undefined;
+    }
+
+    // we should be playing something
+    if (state.small_channel != '') {
+
+        function update() {
+            // the channel needs to be changed
+            if (small_player.getChannel() != state.small_channel) {
+                small_player.setChannel(state.small_channel);
+            }
+        }
+
+        // we need to create the small player
+        if (!small_player) {
+            small_player = newSmallPlayer(state.small_channel);
+
+            small_player.addEventListener(Twitch.Player.READY, update);
+            small_player.addEventListener(Twitch.Player.PLAY, update);
+        } else {
+            update();
+        }
+    }
+
+    const small_player_size = state.small_scale * 100 + '%';
+    small_player_elem.style.width = small_player_size;
+    small_player_elem.style.height = small_player_size;
+
+
+    small_player_elem.className = (state.small_channel == '') ? 'hidden' : '';
+    large_player_elem.className = (state.large_channel == '') ? 'hidden' : '';
+    overlay_elem.className = (state.large_channel != '' || state.small_channel != '') ? 'hidden' : '';
+
+    document.body.className = state.show_chat ? 'with-chat' : '';
+}
+
+if (!window.EventSource) {
+    overlay_elem.firstElementChild.innerText = 'Your browser (probably IE / Edge) does not support EventSource. Please try any other modern browser.';
+} else {
+    const events = new EventSource('/display');
+
+    events.onmessage = m => {
+        updateState(JSON.parse(m.data));
+    };
+
+    events.onopen = () => {
+        overlay_elem.firstElementChild.innerText = 'Nothing playing';
+    };
+
+    events.onerror = err => {
+        console.error(err);
+
+        updateState(defaultState);
+        overlay_elem.firstElementChild.innerText = 'Connection to stream-server failed. Make sure it\'s running.';
+    };
+}
+
+function toggleFullscreen() {
+    if (!document.fullscreenElement) { // we are not in fullscreen
+        document.documentElement.requestFullscreen()
+    } else {
+        if (document.exitFullscreen) {
+            document.exitFullscreen();
+        }
+    }
+}
+
+large_player_elem.addEventListener('mousedown', (event) => {
+    if (event.detail > 1) { // double click
+        toggleFullscreen();
+
+        event.preventDefault();
+    }
+});
\ No newline at end of file
diff --git a/internal/static/style.css b/internal/static/style.css
new file mode 100644
index 0000000000000000000000000000000000000000..421fc818fdfa8d228e59712655fa313d9d8bb113
--- /dev/null
+++ b/internal/static/style.css
@@ -0,0 +1,81 @@
+body {
+    display: flex;
+
+    margin: 0;
+    padding: 0;
+    height: 100vh;
+    width: calc(100vw + 21.25rem); /* without chat the body is 21.25rem (the size of the chat) wider */
+
+    background-color: black;
+    overflow: hidden;
+    transition: width 0.3s; /* width changes are animated */
+}
+
+body.with-chat {
+    width: 100vw; /* with chat the width is the normal 100vw */
+}
+
+#large-player {
+    position: relative; /* while this 'relative' has no impact for #large-player it is the reference point for the #small-player-container which is positioned absolute to this element */
+    display: flex;
+    flex-grow: 1; /* #large-player fills remaining space while #chat does not */
+}
+
+#small-player-container { /* this container is necessary for the #small-player to keep an 16:9 aspect ratio */
+    position: absolute;
+    width: 100%;
+    height: 0;
+    padding-top: 56.25%; /* this is the trick that keeps the child in a 16:9 ratio - read more here https://www.w3schools.com/howto/howto_css_aspect_ratio.asp */
+}
+
+#small-player {
+    position: absolute;
+    top: 0; /* pin small player to the top right */
+    right: 0;
+    
+    /* Note: the width and height property get set via script */
+
+    transition: width 0.3s, height 0.3s; /* width and height changes are animated */
+    display: flex;
+}
+
+#small-player > iframe,
+#large-player > iframe { /* the iframes always grow to the parents size */
+    flex-grow: 1;
+
+    pointer-events: none;
+}
+
+.hidden > iframe {
+    display: none;
+}
+
+#overlay {
+    font-family: Roboto, Helvetica, Arial, sans-serif;
+    color: white;
+
+    /* center the content */
+    
+    position: fixed;
+    top: 0;
+    right: 0;
+    left: 0;
+    bottom: 0;
+
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+}
+
+#overlay.hidden {
+    display: none;
+}
+
+#chat {
+    width: 21.25rem;
+}
+
+a:link, a:active, a:visited, a:hover {
+    color: inherit;
+}
\ No newline at end of file
diff --git a/internal/util/util.go b/internal/util/util.go
index e42a7bd340e1c833d28dceda57d3c4f00e955187..3c6b7bef9681aae1c3780b68ae7e4353b2cbee29 100644
--- a/internal/util/util.go
+++ b/internal/util/util.go
@@ -1,43 +1,76 @@
 package util
 
 import (
+	"bytes"
 	"encoding/json"
 	"fmt"
 	"net"
 	"net/http"
 	"os/exec"
 	"runtime"
+	"strings"
 )
 
-// ServeJSON writes obj to the http.ResponseWriter w marshaled as JSON.
-// The following headers are set:
-//  - Content-Type: application/json
-//  - Cache-Control: no-store
-func ServeJSON(w http.ResponseWriter, obj interface{}) ([]byte, error) {
-	return ServeJSONWithStatus(w, obj, http.StatusOK)
+// ServeIndentedJSON marshals 'v' to JSON, automatically escaping HTML and setting the
+// Content-Type as application/json.
+// adapted from responder.JSON
+func ServeIndentedJSON(w http.ResponseWriter, r *http.Request, v interface{}) {
+	buf := &bytes.Buffer{}
+	enc := json.NewEncoder(buf)
+	enc.SetEscapeHTML(true)
+	enc.SetIndent("", "\t")
+
+	if err := enc.Encode(v); err != nil {
+		http.Error(w, err.Error(), http.StatusInternalServerError)
+		return
+	}
+
+	w.Header().Set("Content-Type", "application/json; charset=utf-8")
+	w.Header().Set("Cache-Control", "no-store")
+	w.WriteHeader(http.StatusOK)
+	w.Write(buf.Bytes())
 }
 
-// ServeJSONWithStatus writes obj to the http.ResponseWriter w marshaled as JSON and sets the given status code.
-// The following headers are set:
-//  - Content-Type: application/json
-//  - Cache-Control: no-store
-func ServeJSONWithStatus(w http.ResponseWriter, obj interface{}, status int) ([]byte, error) {
-	body, err := json.MarshalIndent(obj, "", "\t")
-	if err != nil {
-		return nil, err
+// GetSingleQueryParam is a helper function to get a single param from a request while making sure the param is present and has only a single value.
+// If multiple values are present an error is returned
+// If no values are found value is nil
+func GetSingleQueryParam(r *http.Request, param string) (value *string, err error) {
+	foundValues, found := r.URL.Query()[param]
+	if !found {
+		return
 	}
 
-	w.Header().Set("Content-Type", "application/json")
-	w.Header().Set("Cache-Control", "no-store")
-	w.WriteHeader(status)
-	w.Write(body)
+	valuesCount := len(foundValues)
+
+	if valuesCount == 1 {
+		value = &foundValues[0]
+		return
+	}
 
-	return body, nil
+	err = fmt.Errorf("Got %d values for parameter '%s' but expected only one", valuesCount, param)
+	return
 }
 
-// ServeError is a shorthand for http.Error(w, err.Error(), http.StatusInternalServerError)
-func ServeError(w http.ResponseWriter, err error) {
-	http.Error(w, err.Error(), http.StatusInternalServerError)
+// GetMultipleQueryParams is a helper function to get multiple params from a request while making sure the param is present
+// Both ?key=value1,value2,value3 and ?key=value1&key=value2&key=value3 are supported
+// If no values are found values is nil
+func GetMultipleQueryParams(r *http.Request, param string) (values []string) {
+	foundValues, found := r.URL.Query()[param]
+	if !found {
+		return
+	}
+
+	values = make([]string, 0)
+
+	for _, value := range foundValues {
+		for _, splitValue := range strings.Split(value, ",") {
+			if splitValue != "" {
+				values = append(values, splitValue)
+			}
+		}
+	}
+
+	return
 }
 
 // GetOutboundIP returns the outbound IP of this device.
diff --git a/internal/website/website.go b/internal/website/website.go
deleted file mode 100644
index eaabde3debbebc3b8438e451fcbc2129a95a884a..0000000000000000000000000000000000000000
--- a/internal/website/website.go
+++ /dev/null
@@ -1,295 +0,0 @@
-package website
-
-import (
-	"net/http"
-)
-
-// website is the html source of the website.
-// It is possible to have this in a serperate .html file and include it at compile time but for simplicity and portability we just keep it here inline.
-// No React, Vue, etc. or jQuery is used.
-// Also there are some Visual Studio Code plugins in development that should enable html syntax highlighting here.
-var website = []byte(`
-<!DOCTYPE html>
-<html>
-<head>
-	<meta charset="utf-8">
-	<meta name="viewport" content="width=device-width, initial-scale=1">
-	<title>Stream TV</title>
-
-	<script src="https://player.twitch.tv/js/embed/v1.js"></script> <!-- twitch embed -->
-
-	<style>
-		body {
-			display: flex;
-
-			margin: 0;
-			padding: 0;
-			height: 100vh;
-			width: calc(100vw + 21.25rem); /* without chat the body is 21.25rem (the size of the chat) wider */
-
-			background-color: black;
-			overflow: hidden;
-			transition: width 0.3s; /* width changes are animated */
-		}
-
-		body.with-chat {
-			width: 100vw; /* with chat the width is the normal 100vw */
-		}
-
-		#large-player {
-			position: relative; /* while this 'relative' has no impact for #large-player it is the reference point for the #small-player-container which is positioned absolute to this element */
-			display: flex;
-			flex-grow: 1; /* #large-player fills remaining space while #chat does not */
-		}
-
-		#small-player-container { /* this container is necessary for the #small-player to keep an 16:9 aspect ratio */
-			position: absolute;
-			width: 100%;
-			height: 0;
-			padding-top: 56.25%; /* this is the trick that keeps the child in a 16:9 ratio - read more here https://www.w3schools.com/howto/howto_css_aspect_ratio.asp */
-		}
-
-		#small-player {
-			position: absolute;
-			top: 0; /* pin small player to the top right */
-			right: 0;
-			
-			/* Note: the width and height property get set via script */
-
-			transition: width 0.3s, height 0.3s; /* width and height changes are animated */
-			display: flex;
-		}
-
-		#small-player > iframe,
-		#large-player > iframe { /* the iframes always grow to the parents size */
-			flex-grow: 1;
-
-			pointer-events: none;
-		}
-
-		.hidden > iframe {
-			display: none;
-		}
-
-		#overlay {
-			font-family: Roboto, Helvetica, Arial, sans-serif;
-			color: white;
-
-			/* center the content */
-			
-			position: fixed;
-			top: 0;
-			right: 0;
-			left: 0;
-			bottom: 0;
-
-			display: flex;
-			flex-direction: column;
-			align-items: center;
-			justify-content: center;
-		}
-
-		#overlay.hidden {
-			display: none;
-		}
-
-		#chat {
-			width: 21.25rem;
-		}
-
-		a:link, a:active, a:visited, a:hover {
-			color: inherit;
-		}
-	</style>
-</head>
-<body>
-	<div id="large-player" class="hidden">
-		<div id="small-player-container">
-			<div id="small-player" class="hidden"></div>
-		</div>
-	</div>
-	<iframe id="chat" frameborder="0" scrolling="no" src="about:blank"></iframe>
-
-	<div id="overlay">
-		<h2>Loading...</h2>
-		<p>
-			<a href="https://simons-nzse-2.h-da.io/stream-tv/server">Documentation</a> · 
-			<a href="https://simons-nzse-2.h-da.io/stream-tv/server/reference/">Reference</a>
-		</p>
-		<small>
-			<a href="javascript:toggleFullscreen()">toggle fullscreen</a>
-		</small>
-	</div>
-
-	<script>
-		var large_player;
-		var small_player;
-
-		function newLargePlayer(channel) {
-			return new Twitch.Player('large-player', {
-				channel: channel,
-				width: '',
-				height: '',
-				controls: false,
-				muted: false
-			});
-		}
-
-		function newSmallPlayer(channel) {
-			return new Twitch.Player('small-player', {
-				channel: channel,
-				width: '',
-				height: '',
-				controls: false,
-				muted: true
-			});
-		}
-
-		const large_player_elem = document.getElementById('large-player');
-		const small_player_elem = document.getElementById('small-player');
-		const overlay_elem = document.getElementById('overlay');
-		const chat_elem = document.getElementById('chat');
-
-		const defaultState = {
-			large_channel: '',
-			small_channel: '',
-			volume: 0.5,
-			small_scale: 0.3,
-			show_chat: false
-		};
-
-		let state = defaultState;
-
-		function updateState(newState) {
-			state = newState;
-			
-			console.log("State:", state);
-
-			// large player:
-
-			// we need to destroy the player
-			if (state.large_channel == '' && large_player) {
-				large_player.destroy();
-				large_player = undefined;
-				chat_elem.src = 'about:blank';
-			}
-
-			// we should be playing something
-			if (state.large_channel != '') {
-				// we need to create the large player
-				if (!large_player) {
-					large_player = newLargePlayer(state.large_channel);
-				}
-
-				// the channel needs to be changed
-				if (large_player.getChannel() != state.large_channel) {
-					large_player.setChannel(state.large_channel);
-				}
-
-				// the volume needs to be changed
-				if (large_player.getVolume() != state.volume) {
-					large_player.setVolume(state.volume);
-				}
-
-				var chat_elem_src = 'https://www.twitch.tv/embed/' + state.large_channel + '/chat?darkpopout';
-
-				if (chat_elem_src != chat_elem.src) {
-					chat_elem.src = chat_elem_src;
-				}
-			}
-
-			// small player:
-			
-			// we need to destroy the player
-			if (state.small_channel == '' && small_player) {
-				small_player.destroy();
-				small_player = undefined;
-			}
-
-			// we should be playing something
-			if (state.small_channel != '') {
-				// we need to create the small player
-				if (!small_player) {
-					small_player = newSmallPlayer(state.small_channel);
-				}
-
-				// the channel needs to be changed
-				if (small_player.getChannel() != state.small_channel) {
-					small_player.setChannel(state.small_channel);
-				}
-			}
-
-			const small_player_size = state.small_scale * 100 + '%';
-			small_player_elem.style.width = small_player_size;
-			small_player_elem.style.height = small_player_size;
-
-
-			small_player_elem.className = (state.small_channel == '') ? 'hidden' : '';
-			large_player_elem.className = (state.large_channel == '') ? 'hidden' : '';
-			overlay_elem.className = (state.large_channel != '' || state.small_channel != '') ? 'hidden' : '';
-
-			document.body.className = state.show_chat ? 'with-chat' : '';
-		}
-
-		if (!window.EventSource) {
-			overlay_elem.firstElementChild.innerText = "Your browser (probably IE / Edge) does not support EventSource. Please try any other modern browser.";
-		} else {
-			const events = new EventSource('/tv/events');
-
-			events.onmessage = m => {
-				updateState(JSON.parse(m.data));
-			};
-
-			events.onopen = () => {
-				overlay_elem.firstElementChild.innerText = "Nothing playing";
-			};
-
-			events.onerror = err => {
-				console.error(err);
-
-				updateState(defaultState);
-				overlay_elem.firstElementChild.innerText = "Connection to stream-tv-server failed. Make sure it's running and reload the page.";
-			};
-		}
-
-		function toggleFullscreen() {
-			if (!document.fullscreenElement) { // we are not in fullscreen
-				document.documentElement.requestFullscreen()
-			} else {
-				if (document.exitFullscreen) {
-					document.exitFullscreen();
-				}
-			}
-		}
-
-		large_player_elem.addEventListener('mousedown', (event) => {
-			if (event.detail > 1) { // double click
-				toggleFullscreen();
-
-				event.preventDefault();
-			}
-		});
-
-	</script>
-
-</body>
-</html>
-`)
-
-// websiteHandleFunc just serves the website
-func websiteHandleFunc(w http.ResponseWriter, r *http.Request) {
-	// The "/" pattern (in main.go) matches everything that isn't handled by somebody else, so  we need to check
-	// that we're at the root here.
-
-	if r.URL.Path != "/" {
-		http.NotFound(w, r)
-	} else {
-		w.Header().Set("Content-Type", "text/html")
-		w.Write(website)
-	}
-}
-
-// Handler returns a http.Handler that serves requests for the / backend
-func Handler() http.Handler {
-	return http.HandlerFunc(websiteHandleFunc)
-}
diff --git a/mkdocs.yml b/mkdocs.yml
index 5d8b6a4b390306d2bfba06e52082b6548b5894ce..570b6dd94f9dc34509b2c894d291484cdd0a1151 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -1,11 +1,11 @@
-site_name: Stream TV Project Documentation
-site_description: TODO
+site_name: Stream Server Documentation
+# site_description: TODO
 site_author: Simon Kirsten
-site_url: https://simons-nzse-2.h-da.io/stream-tv/
+site_url: https://stream-server.h-da.io/
 
 # Repository
-repo_name: simons-nzse-2/stream-tv
-repo_url: https://code.fbi.h-da.de/simons-nzse-2/stream-tv
+repo_name: stream-server
+repo_url: https://code.fbi.h-da.de/stream-server/stream-server
 
 # Copyright
 # copyright: 'Copyright &copy; 2019 ...'
@@ -60,19 +60,17 @@ markdown_extensions:
 
 # Page tree
 nav:
-  - Project:
-    - Stream TV Project: index.md
-    - Changelog: changelog.md
-
-  - Server:
-    - Stream TV Server: server/index.md
-    - Options: server/options.md
-    - Quickstart: server/quickstart.md
-    - Reference: server/reference.md
-    - StreamTV.java: server/streamtv-java.md
+  - Stream Server:
+    - Project: index.md
+    - Download and Run: download-and-run.md
+    - Options: options.md
+    - Quickstart: quickstart.md
+    - Reference: reference.md
+    - Android Integration: android-integration.md
 
   - About:
+    - Changelog: about/changelog.md
     - Contributing: about/contributing.md
-    - Author's notes: about/authors-notes.md
-    - Previous considerations: about/previous-considerations.md
+    - Author's Notes: about/authors-notes.md
+    - Previous Considerations: about/previous-considerations.md
     - License: about/license.md
diff --git a/pkg/twitch/games.go b/pkg/twitch/games.go
index d9de98f2d158b1c381727b2750d4cc2f22ad8a4a..651a45de1080d0546efb960e4dbf73d2f806a547 100644
--- a/pkg/twitch/games.go
+++ b/pkg/twitch/games.go
@@ -35,14 +35,14 @@ func (g *twitchGame) toSimplified() Game {
 }
 
 // GetTopGames gets the top games from the twitch API.
-func GetTopGames() (response []Game, err error) {
+func (c *Client) GetTopGames() (response []Game, err error) {
 	var twitchResponse struct {
 		Top []struct {
 			Game twitchGame
 		} `json:"top"`
 	}
 
-	err = Request("/games/top", url.Values{}, &twitchResponse)
+	err = c.Request("/games/top", url.Values{}, &twitchResponse)
 	if err != nil {
 		return
 	}
@@ -57,7 +57,7 @@ func GetTopGames() (response []Game, err error) {
 }
 
 // SearchGames searches games via the twitch API.
-func SearchGames(query string) (response []Game, err error) {
+func (c *Client) SearchGames(query string) (response []Game, err error) {
 	var twitchResponse struct {
 		Games []twitchGame `json:"games"`
 	}
@@ -65,7 +65,7 @@ func SearchGames(query string) (response []Game, err error) {
 	params := url.Values{}
 	params.Set("query", query)
 
-	err = Request("/search/games", params, &twitchResponse)
+	err = c.Request("/search/games", params, &twitchResponse)
 	if err != nil {
 		return
 	}
diff --git a/pkg/twitch/streams.go b/pkg/twitch/streams.go
index ff6dddecc000dfd4d4ba2f78bbf038bcd3cbf3e3..7193f1d0316cee2c136065944af284f8d8afef84 100644
--- a/pkg/twitch/streams.go
+++ b/pkg/twitch/streams.go
@@ -64,14 +64,14 @@ func (s *twitchStream) toSimplified() Stream {
 }
 
 // GetFeaturedStreams gets the featured streams from the twitch API.
-func GetFeaturedStreams() (response []Stream, err error) {
+func (c *Client) GetFeaturedStreams() (response []Stream, err error) {
 	var twitchResponse struct {
 		Featured []struct {
 			Stream twitchStream `json:"stream"`
 		} `json:"featured"`
 	}
 
-	err = Request("/streams/featured", url.Values{}, &twitchResponse)
+	err = c.Request("/streams/featured", url.Values{}, &twitchResponse)
 	if err != nil {
 		return
 	}
@@ -88,17 +88,17 @@ func GetFeaturedStreams() (response []Stream, err error) {
 }
 
 // GetTopStreams gets the top streams from the twitch API.
-func GetTopStreams(channels []string, game string, language string) (response []Stream, err error) {
+func (c *Client) GetTopStreams(channels []string, game *string, language *string) (response []Stream, err error) {
 	var twitchResponse struct {
 		Streams []twitchStream `json:"streams"`
 	}
 
 	params := url.Values{}
 
-	if len(channels) != 0 {
+	if channels != nil {
 		// e.g. esl_csgo => 31239503 via https://dev.twitch.tv/docs/v5/reference/users/#get-users
 
-		channels, err = ChannelsToID(channels)
+		channels, err = c.ChannelsToID(channels)
 		if err != nil {
 			return
 		}
@@ -106,15 +106,15 @@ func GetTopStreams(channels []string, game string, language string) (response []
 		params.Set("channel", strings.Join(channels, ","))
 	}
 
-	if game != "" {
-		params.Set("game", game)
+	if game != nil {
+		params.Set("game", *game)
 	}
 
-	if language != "" {
-		params.Set("language", language)
+	if language != nil {
+		params.Set("language", *language)
 	}
 
-	err = Request("/streams/", params, &twitchResponse)
+	err = c.Request("/streams/", params, &twitchResponse)
 	if err != nil {
 		return
 	}
diff --git a/pkg/twitch/twitch.go b/pkg/twitch/twitch.go
index feee6b9fb42ea82297ea4135b0f05524fbaa8859..224406ae3bec22196c6ac683ea85b1c008d76eb1 100644
--- a/pkg/twitch/twitch.go
+++ b/pkg/twitch/twitch.go
@@ -10,12 +10,25 @@ import (
 	"strings"
 )
 
-// ClientID is the twitch client id used for requests. Must be set before any other calls.
-var ClientID string
+// Client is used to request the Twitch API
+type Client struct {
+	// ClientID is the twitch client id used for requests. Must be set before any other calls.
+	ClientID string
+
+	// channelsToIDCache is used by ChannelsToId to cache the channels to id mapping
+	channelsToIDCache map[string]string
+}
+
+// NewClient returns a new Client with the provided clientID
+func NewClient(clientID string) *Client {
+	return &Client{
+		ClientID: clientID,
+	}
+}
 
 // Request requests the give twitch api kraken endpoint.
-func Request(endpoint string, query url.Values, output interface{}) error {
-	if ClientID == "" {
+func (c *Client) Request(endpoint string, query url.Values, output interface{}) error {
+	if c.ClientID == "" {
 		return errors.New("Twitch ClientID is not set")
 	}
 
@@ -28,7 +41,7 @@ func Request(endpoint string, query url.Values, output interface{}) error {
 		return err
 	}
 
-	req.Header.Add("Client-ID", ClientID)
+	req.Header.Add("Client-ID", c.ClientID)
 	req.Header.Add("Accept", "application/vnd.twitchtv.v5+json")
 
 	resp, err := http.DefaultClient.Do(req)
@@ -55,14 +68,12 @@ func Request(endpoint string, query url.Values, output interface{}) error {
 	return nil
 }
 
-var channelsToIDCache = make(map[string]string)
-
 // ChannelsToID translates the given channels to ids.
-func ChannelsToID(channels []string) (ids []string, err error) {
+func (c *Client) ChannelsToID(channels []string) (ids []string, err error) {
 	var unknownChannels []string
 
 	for _, channel := range channels {
-		if _, ok := channelsToIDCache[channel]; !ok {
+		if _, ok := c.channelsToIDCache[channel]; !ok {
 			unknownChannels = append(unknownChannels, channel)
 		}
 	}
@@ -76,22 +87,21 @@ func ChannelsToID(channels []string) (ids []string, err error) {
 		}
 
 		params := url.Values{}
-
 		params.Set("login", strings.Join(unknownChannels, ","))
 
-		err = Request("/users", params, &twitchResponse)
+		err = c.Request("/users", params, &twitchResponse)
 
 		if err != nil {
 			return
 		}
 
 		for _, user := range twitchResponse.Users {
-			channelsToIDCache[user.Name] = user.ID
+			c.channelsToIDCache[user.Name] = user.ID
 		}
 	}
 
 	for _, channel := range channels {
-		ids = append(ids, channelsToIDCache[channel])
+		ids = append(ids, c.channelsToIDCache[channel])
 	}
 
 	return