diff --git a/.dockerignore b/.dockerignore index 0918a81f..30461e40 100644 --- a/.dockerignore +++ b/.dockerignore @@ -19,4 +19,5 @@ public/saved_vods client-vue/node_modules client-vue/dist client-vue/.env -client-vue/.env.production \ No newline at end of file +client-vue/.env.production +client-broker \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index b169c900..4f3472d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -53,6 +53,7 @@ ENV TCD_BIN_DIR=/usr/bin ENV TCD_FFMPEG_PATH=/usr/bin/ffmpeg ENV TCD_MEDIAINFO_PATH=/usr/bin/mediainfo ENV TCD_DOCKER=1 +ENV TCD_WEBSOCKET_ENABLED=1 USER nobody WORKDIR /var/www/twitchautomator diff --git a/README.md b/README.md index c1397c0e..d104a395 100644 --- a/README.md +++ b/README.md @@ -26,16 +26,20 @@ Reminder that i don't use docker myself on my capturing setup, so any specific e Known issues: - TwitchDownloaderCLI doesn't work in alpine/docker: /bin/sh: ./TwitchDownloaderCLI: not found -### Manual build (recommended) -Run `docker-compose up --build -d` in the app directory. The docker-compose.yml file is required. +### Docker hub -If you want the public webapp to have a custom base folder, you must provide `BASE_URL` and `VUE_APP_BASE_URL` in the environment variable settings. +1. Download the [docker-compose.yml](https://raw.githubusercontent.com/MrBrax/TwitchAutomator/master/docker-compose.yml) file and place it in a directory. +2. Run `docker-compose pull` and `docker-compose up -d` to start it. +3. Visit the webapp at `localhost:8082` -### Docker hub +Hub: https://hub.docker.com/r/mrbrax/twitchautomator -Pull the image from https://hub.docker.com/r/mrbrax/twitchautomator +*The dockerhub build is preconfigured to be hosted at the root (`/`) and such, does not work when placed in a subdirectory.* -Docker hub doesn't seem to fully support docker-compose apps, so the cron stuff won't work. +### Manual build +Run `docker-compose up --build -d` in the app directory. The `docker-compose.yml` file is required. + +If you want the public webapp to have a custom base folder, you must provide `BASE_URL` and `VUE_APP_BASE_URL` in the environment variable settings. ## Standalone setup diff --git a/client-broker/.dockerignore b/client-broker/.dockerignore new file mode 100644 index 00000000..40b878db --- /dev/null +++ b/client-broker/.dockerignore @@ -0,0 +1 @@ +node_modules/ \ No newline at end of file diff --git a/client-broker/Dockerfile b/client-broker/Dockerfile new file mode 100644 index 00000000..1bffe444 --- /dev/null +++ b/client-broker/Dockerfile @@ -0,0 +1,6 @@ +FROM alpine:3.7 +RUN apk --no-cache add nodejs yarn +COPY . /var/broker +WORKDIR /var/broker +RUN yarn install +ENTRYPOINT [ "yarn", "start" ] \ No newline at end of file diff --git a/client-broker/package.json b/client-broker/package.json new file mode 100644 index 00000000..e259e6d8 --- /dev/null +++ b/client-broker/package.json @@ -0,0 +1,9 @@ +{ + "name": "client-broker", + "version": "1.0.0", + "main": "server.js", + "license": "MIT", + "dependencies": { + "ws": "^7.4.3" + } +} diff --git a/client-broker/server.js b/client-broker/server.js new file mode 100644 index 00000000..d692efe2 --- /dev/null +++ b/client-broker/server.js @@ -0,0 +1,77 @@ +// const { cli } = require('webpack'); +const WebSocket = require('ws'); + +class ClientBroker { + constructor(){ + this.clients = []; + this.wss = null; + } + start () { + const a = process.argv.slice(2); + const serverPort = a[0] ? parseInt(a[0]) : 8765; + + console.log(`Starting on port ${serverPort}...`); + try { + this.wss = new WebSocket.Server({ port: serverPort }); + } catch (error) { + console.error("Fatal error when starting broker server", error); + return false; + } + + this.wss.on('error', (error) => { + console.log("Websocket server error", error); + }); + + this.wss.on('connection', this.onConnect.bind(this)); + } + + onConnect(ws, req){ + const clientIP = req.connection.remoteAddress; + ws.clientIP = clientIP; + // console.log(clientIP); + this.clients.push(ws); + + ws.on("message", (message) => this.onMessage(ws, message)); + ws.on("pong", (heartbeat) => { + ws.isAlive = true; + console.log(`Pong from ${clientIP}`); + }); + ws.on("error", (err) => { + console.error("Client error", err) + }); + } + + onMessage(ws, message){ + // console.log("message", ws, message); + + if(message == "ping"){ + console.log(`Pong to ${ws.clientIP}`); + ws.send("pong"); + return; + } + + let data; + + try { + data = JSON.parse(message); + } catch (error) { + console.error(`Invalid data from ${ws.clientIP}: ${message}`) + return; + } + + if(data.server){ + this.wss.clients.forEach((client) => { + client.send(JSON.stringify({ + action: "server", + data: data.data + })); + }); + } + + console.log(`json from ${ws.clientIP}:`, data); + console.debug(`Clients: ${this.wss.clients.size}`); + } +} + +const cb = new ClientBroker(); +cb.start(); \ No newline at end of file diff --git a/client-broker/yarn.lock b/client-broker/yarn.lock new file mode 100644 index 00000000..04e8fb52 --- /dev/null +++ b/client-broker/yarn.lock @@ -0,0 +1,8 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +ws@^7.4.3: + version "7.4.3" + resolved "https://registry.yarnpkg.com/ws/-/ws-7.4.3.tgz#1f9643de34a543b8edb124bdcbc457ae55a6e5cd" + integrity sha512-hr6vCR76GsossIRsr8OLR9acVVm1jyfEWvhbNjtgPOrfvAlKzvyeg/P6r8RuDjRyrcQoPQT7K0DGEPc7Ae6jzA== diff --git a/client-vue/package.json b/client-vue/package.json index 6fa158f3..d7f2148a 100644 --- a/client-vue/package.json +++ b/client-vue/package.json @@ -1,6 +1,6 @@ { "name": "twitchautomator-client", - "version": "0.1.3", + "version": "0.2.0", "private": true, "scripts": { "serve": "vue-cli-service serve", @@ -19,6 +19,7 @@ "core-js": "^3.6.5", "date-fns": "^2.16.1", "normalize.css": "^8.0.1", + "register-service-worker": "^1.7.1", "vue": "3.0.5", "vue-axios": "^3.2.2", "vue-router": "4.0.3", @@ -29,6 +30,7 @@ "@typescript-eslint/parser": "^4.14.0", "@vue/cli-plugin-babel": "~4.5.0", "@vue/cli-plugin-eslint": "~4.5.0", + "@vue/cli-plugin-pwa": "~4.5.0", "@vue/cli-plugin-router": "~4.5.0", "@vue/cli-plugin-typescript": "~4.5.0", "@vue/cli-plugin-vuex": "~4.5.0", diff --git a/client-vue/public/index.html b/client-vue/public/index.html index 5cb709b7..165d9ee6 100644 --- a/client-vue/public/index.html +++ b/client-vue/public/index.html @@ -5,6 +5,7 @@ + <%= htmlWebpackPlugin.options.title %> diff --git a/client-vue/public/manifest/site.webmanifest b/client-vue/public/manifest.json similarity index 72% rename from client-vue/public/manifest/site.webmanifest rename to client-vue/public/manifest.json index 538f7bd1..267593b8 100644 --- a/client-vue/public/manifest/site.webmanifest +++ b/client-vue/public/manifest.json @@ -3,12 +3,12 @@ "short_name": "TwitchAutomator", "icons": [ { - "src": "android-chrome-192x192.png", + "src": "manifest/android-chrome-192x192.png", "sizes": "192x192", "type": "image/png" }, { - "src": "android-chrome-512x512.png", + "src": "manifest/android-chrome-512x512.png", "sizes": "512x512", "type": "image/png" } @@ -16,5 +16,5 @@ "theme_color": "#0072bc", "background_color": "#ffffff", "display": "standalone", - "start_url": "../dashboard" + "start_url": "." } diff --git a/client-vue/public/robots.txt b/client-vue/public/robots.txt new file mode 100644 index 00000000..eb053628 --- /dev/null +++ b/client-vue/public/robots.txt @@ -0,0 +1,2 @@ +User-agent: * +Disallow: diff --git a/client-vue/src/App.vue b/client-vue/src/App.vue index c09584df..73facbed 100644 --- a/client-vue/src/App.vue +++ b/client-vue/src/App.vue @@ -37,7 +37,7 @@ export default defineComponent({ this.fetchData(); }, methods: { - fetchData() { + async fetchData() { // client config const currentClientConfig = localStorage.getItem("twitchautomator_config") ? JSON.parse(localStorage.getItem("twitchautomator_config") as string) @@ -47,10 +47,9 @@ export default defineComponent({ // clear config this.$store.commit("updateConfig", []); - return this.$http.get(`/api/v0/settings/list`).then((response) => { - this.$store.commit("updateConfig", response.data.data.config); - this.$store.commit("updateVersion", response.data.data.version); - }); + const response = await this.$http.get(`/api/v0/settings/list`); + this.$store.commit("updateConfig", response.data.data.config); + this.$store.commit("updateVersion", response.data.data.version); }, }, components: { diff --git a/client-vue/src/assets/_mobile.scss b/client-vue/src/assets/_mobile.scss index d1d464e0..378c9076 100644 --- a/client-vue/src/assets/_mobile.scss +++ b/client-vue/src/assets/_mobile.scss @@ -259,6 +259,10 @@ display: none; } + #jobs-status { + display: none; + } + .jobs_list { display: none; } diff --git a/client-vue/src/components/DurationDisplay.vue b/client-vue/src/components/DurationDisplay.vue index e9572c3c..22f04fd3 100644 --- a/client-vue/src/components/DurationDisplay.vue +++ b/client-vue/src/components/DurationDisplay.vue @@ -37,7 +37,8 @@ export default defineComponent({ if ((dur.seconds && dur.seconds > 0) || (dur.minutes && dur.minutes > 0)) str += `${dur.seconds}s `; this.timeString = str.trim(); } else { - this.timeString = dur.hours?.toString().padStart(2, "0") + ":" + dur.minutes?.toString().padStart(2, "0") + ":" + dur.seconds?.toString().padStart(2, "0"); + this.timeString = + dur.hours?.toString().padStart(2, "0") + ":" + dur.minutes?.toString().padStart(2, "0") + ":" + dur.seconds?.toString().padStart(2, "0"); } }, }, diff --git a/client-vue/src/components/SideMenuStreamer.vue b/client-vue/src/components/SideMenuStreamer.vue index 943198b8..62d45baf 100644 --- a/client-vue/src/components/SideMenuStreamer.vue +++ b/client-vue/src/components/SideMenuStreamer.vue @@ -28,7 +28,7 @@ {{ streamer.current_game.game_name }} for - - - - - - + + + + + + + + + + + + + + - {{ formatDate(vod.dt_started_at.date) }} - {{ humanDate(vod.dt_started_at.date, true) }} + + + {{ formatDate(vod.dt_started_at.date) }} + + + {{ humanDate(vod.dt_started_at.date, true) }} diff --git a/client-vue/src/components/Vod.vue b/client-vue/src/components/Vod.vue index 008e1ce4..a1dcc3b0 100644 --- a/client-vue/src/components/Vod.vue +++ b/client-vue/src/components/Vod.vue @@ -173,7 +173,8 @@
  • - The ID was {{ vod?.twitch_vod_id }}. + The ID was {{ vod?.twitch_vod_id }}. The VOD probably never got saved.
  • @@ -195,7 +196,7 @@
  • Current duration:
  • Watch live: - Twitch + Twitch
  • Cronjobs

    - - + + +
    {{ cron }}
    {{ cron_name }} {{ cron_status }}

    @@ -180,6 +181,19 @@ export default defineComponent({ }); */ }, + runCron(type: string) { + this.$http + .get(`/api/v0/cron/${type}`) + .then((response) => { + const json = response.data; + console.debug("cronData", json); + if (json.message) alert(json.message); + this.fetchData(); + }) + .catch((err) => { + console.error("about error", err.response); + }); + }, }, }); diff --git a/client-vue/src/views/Dashboard.vue b/client-vue/src/views/Dashboard.vue index 125869da..be29a5f6 100644 --- a/client-vue/src/views/Dashboard.vue +++ b/client-vue/src/views/Dashboard.vue @@ -49,7 +49,12 @@
    - {{ loading ? "Loading..." : `Refreshing in ${timer} seconds.` }} + +
    @@ -94,6 +99,9 @@ export default defineComponent({ logModule: "", oldData: {} as Record, notificationSub: Function as any, + ws: {} as WebSocket, + wsConnected: false, + wsKeepalive: 0, }; }, created() { @@ -111,11 +119,17 @@ export default defineComponent({ }); }, mounted() { - this.interval = setInterval(() => { - this.fetchTicker(); - }, 1000); this.processNotifications(); + + if (this.$store.state.config.websocket_enabled) { + this.connectWebsocket(); + } else { + console.debug("No websocket url"); + this.interval = setInterval(() => { + this.fetchTicker(); + }, 1000); + } }, unmounted() { if (this.interval) { @@ -127,8 +141,80 @@ export default defineComponent({ console.log("unsubscribing from notifications, unmounted"); this.notificationSub(); } + + if (this.ws) { + this.disconnectWebsocket(); + } }, methods: { + connectWebsocket() { + if (this.ws) this.disconnectWebsocket(); + const proto = window.location.protocol === "https:" ? "wss://" : "ws://"; + const websocket_url_public = proto + window.location.host + this.$store.state.config.basepath + "/socket/"; + const websocket_url = process.env.NODE_ENV === "development" ? "ws://localhost:8765/socket/" : websocket_url_public; + console.log(`Connecting to ${websocket_url}`); + this.ws = new WebSocket(websocket_url); + this.ws.onopen = (ev: Event) => { + console.log(`Connected to websocket!`); + this.ws.send(JSON.stringify({ action: "helloworld" })); + this.wsConnected = true; + this.wsKeepalive = setInterval(() => { + console.debug("send ping"); + this.ws.send("ping"); + }, 10000); + }; + this.ws.onmessage = (ev: MessageEvent) => { + // console.log("ws message", ev); + let text = ev.data; + + if (text == "pong") { + console.log("pong recieved"); + return; + } + + let json: any = {}; + try { + json = JSON.parse(text); + } catch (error) { + console.error("Couldn't parse json", text); + return; + } + // console.log("json return", json); + // this.$emit("websocketData", json); + if (json.data.action && ["start_capture", "finish_capture", "chapter_update"].indexOf(json.data.action) !== -1) { + console.log("Websocket update"); + this.fetchStreamers().then((sl) => { + this.$store.commit("updateStreamerList", sl); + this.loading = false; + }); + } else { + console.log(`Websocket wrong action (${json.data.action})`); + } + }; + this.ws.onerror = (ev: Event) => { + console.error("Websocket error", ev); + this.wsConnected = false; + clearInterval(this.wsKeepalive); + }; + this.ws.onclose = (ev: CloseEvent) => { + console.log(`Disconnected from websocket!`, ev); + setTimeout(() => { + if (!ev.wasClean) { + this.connectWebsocket(); + } + }, 10000); + this.wsConnected = false; + clearInterval(this.wsKeepalive); + }; + return this.ws; + }, + disconnectWebsocket() { + if (this.ws && this.ws.close) { + console.log("Closing websocket..."); + this.ws.close(undefined, "pageleave"); + if (this.wsKeepalive) clearInterval(this.wsKeepalive); + } + }, async fetchStreamers() { let response; try { @@ -256,8 +342,8 @@ export default defineComponent({ }*/ // console.log( "values", Object.(mutation.payload[0])); const streamerPronounciation: { [key: string]: string } = { - "pokelawls": "pookelawls", - "xQcOW": "eckscueseeow" + pokelawls: "pookelawls", + xQcOW: "eckscueseeow", }; // console.debug("notification payload", mutation); diff --git a/client-vue/src/views/Settings.vue b/client-vue/src/views/Settings.vue index 1863884b..13d5c0a0 100644 --- a/client-vue/src/views/Settings.vue +++ b/client-vue/src/views/Settings.vue @@ -59,10 +59,10 @@