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