From 4e6c917672bf316c4422f47e9dc5acfaec9c8c7e Mon Sep 17 00:00:00 2001
From: Simon Kirsten <stsnkirs@stud.h-da.de>
Date: Fri, 1 Nov 2019 23:44:04 +0100
Subject: [PATCH] Major rework and restructure

---
 .gitignore                                    |   9 +-
 .gitlab-ci-server.yml                         |  21 +-
 CONTRIBUTING.md                               |   2 +-
 README.md                                     |   4 +-
 .../main.go                                   |  43 +--
 docs/about/changelog.md                       |   5 +
 docs/about/contributing.md                    |  10 +-
 docs/android-integration.md                   |  17 +
 ...{StreamTV.java => StreamServerClient.java} |  44 +--
 docs/changelog.md                             |   5 -
 docs/download-and-run.md                      |  66 ++++
 docs/index.md                                 |  13 +-
 docs/{server => }/options.md                  |  10 +-
 docs/{server => }/quickstart.md               |  47 +--
 docs/reference.md                             | 191 ++++++++++++
 docs/server/index.md                          |  68 ----
 docs/server/reference.md                      | 232 --------------
 docs/server/streamtv-java.md                  |  15 -
 go.mod                                        |   9 +-
 go.sum                                        |  87 ++++++
 internal/api/twitch.go                        |  98 ------
 internal/{api/tv.go => server/display.go}     | 104 +++---
 internal/server/server.go                     |  39 +++
 internal/server/twitch.go                     |  94 ++++++
 internal/static/favicon.ico                   | Bin 0 -> 15406 bytes
 internal/static/index.html                    |  33 ++
 internal/static/main.js                       | 164 ++++++++++
 internal/static/style.css                     |  81 +++++
 internal/util/util.go                         |  77 +++--
 internal/website/website.go                   | 295 ------------------
 mkdocs.yml                                    |  32 +-
 pkg/twitch/games.go                           |   8 +-
 pkg/twitch/streams.go                         |  20 +-
 pkg/twitch/twitch.go                          |  36 ++-
 34 files changed, 1008 insertions(+), 971 deletions(-)
 rename cmd/{stream-tv-server => stream-server}/main.go (64%)
 create mode 100644 docs/about/changelog.md
 create mode 100644 docs/android-integration.md
 rename docs/assets/{StreamTV.java => StreamServerClient.java} (75%)
 delete mode 100644 docs/changelog.md
 create mode 100644 docs/download-and-run.md
 rename docs/{server => }/options.md (76%)
 rename docs/{server => }/quickstart.md (61%)
 create mode 100644 docs/reference.md
 delete mode 100644 docs/server/index.md
 delete mode 100644 docs/server/reference.md
 delete mode 100644 docs/server/streamtv-java.md
 delete mode 100644 internal/api/twitch.go
 rename internal/{api/tv.go => server/display.go} (54%)
 create mode 100644 internal/server/server.go
 create mode 100644 internal/server/twitch.go
 create mode 100644 internal/static/favicon.ico
 create mode 100644 internal/static/index.html
 create mode 100644 internal/static/main.js
 create mode 100644 internal/static/style.css
 delete mode 100644 internal/website/website.go

diff --git a/.gitignore b/.gitignore
index 0722697..be3c97d 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 c2c3cde..91e6897 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 b960d3d..c9b900c 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 0ae894f..c839d08 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 b5ab2d1..29c5913 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 0000000..8972800
--- /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 22268af..924bda0 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 0000000..f408a7c
--- /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 aa1ae9d..8b22846 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 7559bcc..0000000
--- 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 0000000..e78b646
--- /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 1b39438..b4f2964 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 f5ae0d9..b63fbdb 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 d6b74f1..ee35a11 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 0000000..f3d3fc2
--- /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 e4e1e4d..0000000
--- 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 49b081d..0000000
--- 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 422668e..0000000
--- 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 405b3de..8892a02 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 c7d7f83..15b6d1e 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 a03701e..0000000
--- 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 3efb53c..fd1300e 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 0000000..4e551b9
--- /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 0000000..a2eb569
--- /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
GIT binary patch
literal 15406
zcmZQzU}Rus5D);-3Je)63=C!r3=9ei5Wa>W1H(KP1_lEI2tPxOf#H}a1A_(w1A_oa
z9Roz10S9<HN1^WB9EG3n<|zCp5&xt>FU&5S_I_At_y5zDu>T*HTfo#3N6%IM|MT9q
z{|NB!|F5S<|9@EJK$QN^dosaV|NZ#z|Hst@|9`)_0OtSw^8Ek%MS29y_<Eultp59@
zSrE1JRR90|_698W<!}W=3{O~p+~D{B|Nnpgf81CBQ9DlsuK(+a7Kj)g{h&DfcDn2T
z&pR8yYQJBZ2e$kFzkmNfuJ;0qfz%L#e_UG((fj}Z|8J+eNz(gyPX<Kq-{1d#zrOtc
z(~bm^^uz4_daU99r_CY%|Ns5{|HBGvqRserssn8Qr)|;yKW&Wyvp;SMBwGLbg*yL#
zJv#x`2O@sIJp2FMJQbq#!@~FDTKE4S*LnSaH&+RlUYvOVW(FRbk~|33i=$W|S{PK$
zC@?TEI1ngj{x>i%y_uz0^KOp9Gd%vm6-MNWKLgnbwtEiPZgS0`NZ%WX-SqS~x_bxG
zZgjT{3I2!G&i}t0D*ONKOz(eCdHQ8v&i@a~%xLcR56jH||9Wx=RODdde}Dh~dU6<C
z9?~KYy<epJ|L?~KnD(NJ{QLL+`{g<K$~3aV>D##p=r-Zyf4{nbEPHT;9jKl{wh$E7
zpz<43?|j~!{{PSW+sM*j_NT3pRC5EYod7oY|Ns9V*Epk?{eG#*|9`(e!=%CV&j&kE
z)Zp?PG2(wdJ_MTyBi=30#4!8UqkS-GF#Y%IOAIx{*o95a&s%H2X2OUM%P`x1==T5n
z`H5=we_ZeV|Np<gFzf$=+JeaLe81QLBmDn-xJxzrL3VvRHxX|C_X|^z%vSvW^WIjt
z9E|bf`Vu5HB)8$Q>iv483S0(#*_#ECpR4#E)*ge|3#LJBvNbLcH3ZtEAit4?=c)ex
zd3Q6|I&|Xe@g~~ZJxB5X&%0aDt^E&bOOx6jCdcnz4p(B@{r~^}AGcQje>YE!=6?VC
z<td8&|9*b_|KsW+u-M-(&;Ebh6ht#Od|2%Swg*Q1`}6bvr_G^Y`+hyx1s4AI<Ng2l
zON_u`<b*FiJs&stg6)72-_K8i+XL#C!o<HGt$~Z<Gmlic_e+dnR)G71@0Xgw?E$f2
zlAt=1RI`ay59;&4?D=_r$N$ee;{JpBQ$O!-hYA1xa;OyUE@I6kR^7)9{{R2|{tmYb
zodGKQX%+u4H-6k0h_&AT`|b7rPurqs=69I=V0y07e^5IEG`0X51Ngiv_5ZsC8uYQ7
zR^>YV%^k=-5V#)-j!XLc`#)MAn4aPA4Al1p^}mT8P#C5PLE|I_3=9kj1jb1~-hVe!
ziT}-PxtMoz6pp={tMF~0(hWg-A#OVca_ieADts`vznh~t_1zqW?*#P^KwiM=rr8QV
zKyC*40pyn<<#t3E{(!pu7*;<b)D7l(V{oT;LSYEW4}&>;N8Ls!4u^^Shjs4%zn$s%
z|MS7F|G!^f`v2$MjsL%2ocaIb=IZ}n50(B0jU^4ba$>I1|Bvf^{{MQk7rm*E!T<O7
zH+Y`!)0T+;?-yzhigf*Xcj|x8co2qT@d$wW)IV>p`~PvH|A6}8<677M|GvM)<5GN5
z|G?wM;C{&*Mf&?=p34895BK16AW?FEKR^9X${ZoJ^3TV0p8rAf%h(+G@AtR=pfOL-
zeDIIk>;C_Ke(L|fUth2(1&jT8bM5~Jq_F~Oxe=c^U-oAI|MwGhTpHvUP~ZK_{(}FY
zantt;H2%L|pam{>K=}lhe?j_wT%qo`K0ddD<h~xM{{QdK50KR;7*r0uU#yQcJ%jq=
zpfM>F4N%VC&yW9u)-Qm}rUwR%L1A-0Xzu^rJXNgj2f6FlL+s@&s7(62I}N8kkX~xy
zFZ*&a#??VV`TgRI|L^81<8;TjGu<F%7#I{UUys(|)JILXgUkZ0tH73?e_UJi|J{7t
z<Fuf8MGVJ-1pfa14;t$MnN1fwPwoG&2RlJFqu^f;_x#6|{z2~g^>8<edKd?k7QY^?
zrd#-f-12dq$A3_~!`uL(|9*P>|HBF!tnL8K^?=+7QiqNq=|3H-K9F6s!e39eqLxGd
z{{H{C&KtuWpz+_|FVCSn8_xgx`N{u}t5N3;Y2{{Qb3yH}?-!>b)ldJx<CZ9D7U})}
zaedkU|9^kM-HXlu)pPjrJ+gV!W`pV|(D*)R?(N&D&j0Th8zRDgo(j1Aj4R!M`~j+)
ziCt?(ZMWk#Pw_u^T>*671msR^807XZ2Z|B?9VC4r&>BF{Joo>9f3P_lEcWmBSMXYm
z_gLnvafb_a=1cLKKUe8Lc)kjc+d*^Apz<D+_VAiT@A#Oj^dDCp3l4P9%+&Ai|Gyrq
z!&^@el%GI#0;nv6xfPuTt?>k<`QOh^{Qq{g|37HX@!df5U$Dlj;{UHF+x~;rJ)*lC
z#0T}~KWz^C|NG?`5cfYQZ-DywA6C<NJ|3%EG4l~9Eu*jJfI9U5pLaL^e_ZVdE@wXP
zO!^O+$A!rK|NrOBWpF=XpwbM?UQoLS)c%3F3r2&|40v4#Y(3|Ewg1TTp&)f%k2iw-
z1~Y3Q==Y2C|6^;P{k*>wqfPpGPddoiaQyq(iGg?j0?q$FZmxv83B~}e1x61~P`-u9
zgXll+Z&7`18oHeX`EwQjf5XVX{~&9Mkk=8<RsIiJ7Y=ea41?y~37Su2KBUAP&>9HP
z`YxEeK=i*K@BaV1x8*;mfA#f9<^P{|)}!QKkQ&gsc}nc1rrvKCrr|6@L2gCIptjw|
zjR6Cm{^4P;K=VInF9B$+1G-z$`JnNZ@8>~l#s**v4qtklukru$-t7N>KRrTsGMo<@
zKl*mEb<m9E!owfQU+?Fu|NpW-54`>gG!FOs#cA-Gyssx){=Z*lK6ug$lAE#lV^j>Y
zy)taVWGK%;Lfk&2=dZzY@Zj|XLoSVd2e}=z1_HDe;@uq3nusCehworF&s5?Etw90#
X0pypVw0;J(<_F~VQ9K#~l!O2PT{{M(

literal 0
HcmV?d00001

diff --git a/internal/static/index.html b/internal/static/index.html
new file mode 100644
index 0000000..4f9d222
--- /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 0000000..85442b8
--- /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 0000000..421fc81
--- /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 e42a7bd..3c6b7be 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 eaabde3..0000000
--- 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 5d8b6a4..570b6dd 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 d9de98f..651a45d 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 ff6ddde..7193f1d 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 feee6b9..224406a 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
-- 
GitLab