Browse Source

Initial commit for ActivityWatch integration

Kristian Vos 3 years ago
parent
commit
756a9a68d8

+ 106 - 0
frontend/src/aw.js

@@ -0,0 +1,106 @@
+import Toast from "toasters";
+
+let gotPong = false;
+let pingTries = 0;
+let uuid = null;
+
+// let sendingVideoDataToast = new Toast({
+// 	content: "Sending video data to ActivityWatch.",
+// 	persistant: true
+// });
+
+// let deniedToast = new Toast({
+// 	content:
+// 		"Another Musare instance is already sending data to ActivityWatch Musare extension. Please only use 1 active tab for stations and editsong.",
+// 	persistant: true
+// });
+
+// let competitorToast = new Toast({
+// 	content:
+// 		"Another Musare instance is already sending data to ActivityWatch Musare extension. Please only use 1 active tab for stations and editsong.",
+// 	persistant: true
+// });
+
+export default {
+	sendVideoData(videoData) {
+		this.sendEvent("videoData", videoData);
+	},
+
+	sendEvent(type, data) {
+		document.dispatchEvent(
+			new CustomEvent("ActivityWatchMusareEvent", {
+				detail: {
+					type,
+					source: uuid,
+					data
+				}
+			})
+		);
+	},
+
+	init() {
+		uuid = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(
+			/[xy]/g,
+			function abc(c) {
+				// eslint-disable-next-line
+				const r = (Math.random() * 16) | 0;
+				// eslint-disable-next-line
+				const v = c == "x" ? r : (r & 0x3) | 0x8;
+				return v.toString(16);
+			}
+		);
+
+		document.addEventListener("ActivityWatchMusareEvent", event => {
+			const data = event.detail;
+
+			if (data.type === "pong") {
+				gotPong = true;
+				new Toast({
+					content:
+						"Got pong, connected to ActivityWatch Musare extension",
+					timeout: 8000
+				});
+			}
+
+			if (data.type === "denied") {
+				new Toast({
+					content:
+						"Another Musare instance is already sending data to ActivityWatch Musare extension. Please only use 1 active tab for stations and editsong.",
+					timeout: 4000
+				});
+			}
+
+			if (data.type === "competitor") {
+				if (data.competitor !== uuid)
+					new Toast({
+						content:
+							"Another Musare instance is trying and failing to send data to the ActivityWatch Musare instance. Please only use 1 active tab for stations and editsong.",
+						timeout: 4000
+					});
+			}
+		});
+
+		this.attemptPing();
+
+		// setInterval(() => {
+		// }, 1000);
+	},
+
+	attemptPing() {
+		if (!gotPong) {
+			if (pingTries < 10) {
+				pingTries += 1;
+				this.sendEvent("ping", null);
+				setTimeout(() => {
+					this.attemptPing.apply(this);
+				}, 1000);
+			} else {
+				new Toast({
+					content:
+						"Couldn't connect to ActivityWatch Musare extension.",
+					timeout: 8000
+				});
+			}
+		}
+	}
+};

+ 57 - 1
frontend/src/components/modals/EditSong.vue

@@ -509,6 +509,7 @@
 import { mapState, mapGetters, mapActions } from "vuex";
 import Toast from "toasters";
 
+import aw from "@/aw";
 import validation from "@/validation";
 import keyboardShortcuts from "@/keyboardShortcuts";
 import Modal from "../Modal.vue";
@@ -548,6 +549,9 @@ export default {
 			keydownGenreInputTimeout: 0,
 			artistAutosuggestItems: [],
 			genreAutosuggestItems: [],
+			activityWatchVideoDataInterval: null,
+			activityWatchVideoLastStatus: "",
+			activityWatchVideoLastStartDuration: "",
 			genres: [
 				"Blues",
 				"Country",
@@ -612,6 +616,10 @@ export default {
 		//   this.song.skipDuration
 		// );
 
+		this.activityWatchVideoDataInterval = setInterval(() => {
+			this.sendActivityWatchVideoData();
+		}, 1000);
+
 		this.useHTTPS = await lofig.get("cookie.secure");
 
 		this.socket.dispatch(
@@ -640,7 +648,7 @@ export default {
 								this.song.skipDuration >
 								this.song.duration
 						) {
-							this.video.paused = false;
+							this.video.paused = true;
 							this.video.player.stopVideo();
 							this.drawCanvas();
 						}
@@ -932,6 +940,7 @@ export default {
 	beforeDestroy() {
 		this.playerReady = false;
 		clearInterval(this.interval);
+		clearInterval(this.activityWatchVideoDataInterval);
 
 		const shortcutNames = [
 			"editSong.pauseResume",
@@ -1401,6 +1410,53 @@ export default {
 		resetGenreHelper() {
 			this.$refs.genreHelper.resetBox();
 		},
+		sendActivityWatchVideoData() {
+			if (!this.video.paused) {
+				if (this.activityWatchVideoLastStatus !== "playing") {
+					this.activityWatchVideoLastStatus = "playing";
+					console.log(
+						this.song.skipDuration,
+						parseFloat(this.youtubeVideoCurrentTime),
+						typeof this.song.skipDuration,
+						typeof this.youtubeVideoCurrentTime,
+						this.song.skipDuration > 0,
+						parseFloat(this.youtubeVideoCurrentTime) === 0
+					);
+					if (
+						this.song.skipDuration > 0 &&
+						parseFloat(this.youtubeVideoCurrentTime) === 0
+					) {
+						this.activityWatchVideoLastStartDuration =
+							this.song.skipDuration +
+							parseFloat(this.youtubeVideoCurrentTime);
+					} else {
+						this.activityWatchVideoLastStartDuration = parseFloat(
+							this.youtubeVideoCurrentTime
+						);
+					}
+				}
+
+				const videoData = {
+					title: this.song.title,
+					artists: this.song.artists
+						? this.song.artists.join(", ")
+						: null,
+					youtubeId: this.song.songId,
+					muted: this.muted,
+					volume: this.volumeSliderValue / 100,
+					startedDuration:
+						this.activityWatchVideoLastStartDuration <= 0
+							? 0
+							: this.activityWatchVideoLastStartDuration,
+					source: `editSong#${this.song.songId}`,
+					hostname: window.location.hostname
+				};
+
+				aw.sendVideoData(videoData);
+			} else {
+				this.activityWatchVideoLastStatus = "not_playing";
+			}
+		},
 		...mapActions("modals/editSong", [
 			"stopVideo",
 			"loadVideoById",

+ 3 - 0
frontend/src/main.js

@@ -3,6 +3,7 @@ import Vue from "vue";
 import VueTippy, { TippyComponent } from "vue-tippy";
 import VueRouter from "vue-router";
 
+import aw from "@/aw";
 import ws from "@/ws";
 import store from "./store";
 
@@ -174,6 +175,8 @@ lofig.folder = "../config/default.json";
 		}
 	});
 
+	aw.init();
+
 	const websocketsDomain = await lofig.get("websocketsDomain");
 	ws.init(websocketsDomain);
 

+ 50 - 1
frontend/src/pages/Station/index.vue

@@ -636,6 +636,7 @@ import { mapState, mapActions, mapGetters } from "vuex";
 import Toast from "toasters";
 import { ContentLoader } from "vue-content-loader";
 
+import aw from "@/aw";
 import ws from "@/ws";
 import keyboardShortcuts from "@/keyboardShortcuts";
 
@@ -695,7 +696,11 @@ export default {
 			showPlaylistDropdown: false,
 			theme: "var(--primary-color)",
 			seekerbarPercentage: 0,
-			frontendDevMode: "production"
+			frontendDevMode: "production",
+			activityWatchVideoDataInterval: null,
+			activityWatchVideoLastStatus: "",
+			activityWatchVideoLastYouTubeId: "",
+			activityWatchVideoLastStartDuration: ""
 		};
 	},
 	computed: {
@@ -734,6 +739,9 @@ export default {
 		this.stationIdentifier = this.$route.params.id;
 
 		window.stationInterval = 0;
+		this.activityWatchVideoDataInterval = setInterval(() => {
+			this.sendActivityWatchVideoData();
+		}, 1000);
 
 		if (this.socket.readyState === 1) this.join();
 		ws.onConnect(() => this.join());
@@ -1033,6 +1041,8 @@ export default {
 		shortcutNames.forEach(shortcutName => {
 			keyboardShortcuts.unregisterShortcut(shortcutName);
 		});
+
+		clearInterval(this.activityWatchVideoDataInterval);
 	},
 	methods: {
 		isOwnerOnly() {
@@ -1865,6 +1875,45 @@ export default {
 				}
 			);
 		},
+		sendActivityWatchVideoData() {
+			if (!this.stationPaused && !this.localPaused && this.currentSong) {
+				if (this.activityWatchVideoLastStatus !== "playing") {
+					this.activityWatchVideoLastStatus = "playing";
+					this.activityWatchVideoLastStartDuration =
+						this.currentSong.skipDuration + this.getTimeElapsed();
+				}
+
+				if (
+					this.activityWatchVideoLastYouTubeId !==
+					this.currentSong.songId
+				) {
+					this.activityWatchVideoLastYouTubeId = this.currentSong.songId;
+					this.activityWatchVideoLastStartDuration =
+						this.currentSong.skipDuration + this.getTimeElapsed();
+				}
+
+				const videoData = {
+					title: this.currentSong ? this.currentSong.title : null,
+					artists:
+						this.currentSong && this.currentSong.artists
+							? this.currentSong.artists.join(", ")
+							: null,
+					youtubeId: this.currentSong.songId,
+					muted: this.muted,
+					volume: this.volumeSliderValue / 100,
+					startedDuration:
+						this.activityWatchVideoLastStartDuration <= 0
+							? 0
+							: this.activityWatchVideoLastStartDuration / 1000,
+					source: `station#${this.station.name}`,
+					hostname: window.location.hostname
+				};
+
+				aw.sendVideoData(videoData);
+			} else {
+				this.activityWatchVideoLastStatus = "not_playing";
+			}
+		},
 		...mapActions("modalVisibility", ["openModal"]),
 		...mapActions("station", [
 			"joinStation",