Skip to content

Commit

Permalink
fixed settings window live update, added api config, added api routes…
Browse files Browse the repository at this point in the history
… - track info + track controls (play, pause, prev/next), added play state to discord rpc
  • Loading branch information
Venipa committed Jan 17, 2022
1 parent 60cd8b3 commit 8e16a17
Show file tree
Hide file tree
Showing 13 changed files with 295 additions and 93 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ytmdesktop2",
"version": "0.5.0",
"version": "0.5.1",
"private": false,
"author": "Venipa <[email protected]>",
"license": "MIT",
Expand Down
4 changes: 2 additions & 2 deletions src/api/createApiWorker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { EMPTY_URL, isDevelopment } from "@/app/utils/devUtils";
import logger from "@/utils/Logger";
import { BrowserWindow, ipcMain } from "electron";
import path from "path";
import { apiChannelName, ApiPayload } from "./apiWorkerHelper";
import { apiChannelName } from "./apiWorkerHelper";
export interface ApiWorker {
send(name: string, ...args: any[]): void;
invoke<T = any>(name: string, ...args: any[]): Promise<T>;
Expand Down Expand Up @@ -48,7 +48,7 @@ export const createApiWorker = async (): Promise<ApiWorker> => {
});
}
destroy() {
worker.destroy();
if (!worker.isDestroyed()) worker.destroy();
}
})();
};
39 changes: 34 additions & 5 deletions src/api/main.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import { isDevelopment } from '@/app/utils/devUtils';
import { TrackData } from '@/app/utils/trackData';
import logger from '@/utils/Logger';
import { ipcRenderer } from 'electron';
import createApp, {Router} from 'express';
import createApp, { Router } from 'express';
import expressWs from 'express-ws';
import { createServer } from 'http';

import { apiChannelName } from './apiWorkerHelper';

import type { SettingsStore } from "@/app/plugins/settingsProvider.plugin";
const {app, getWss} = expressWs(createApp());
// Pass a http.Server instance to the listen method
let appConfig: SettingsStore;
const log = logger.child("api-server");
const router = Router() as expressWs.Router;
Expand All @@ -20,17 +19,47 @@ router.ws("/", (_ws, _req) => {
_ws.on("unexpected-response", log.debug.bind(log));
_ws.on("error", log.debug.bind(log));
}
_ws.on("open", async () => {
const track: TrackData = await ipcRenderer.invoke("api/track");
if (track)
{

const data = JSON.stringify({
event: "track:change",
data: [{...track}]
});
_ws.send(data, {binary: false})
} else {
const data = null
_ws.send(data, {binary: false})
};
})
});
router.ws("/ping", (s, _req) => {
s.on("message", () => s.send("Pong!"));
});
app.use("/socket", router);
app.get("/", (req, res) => {
app.get("/", async (req, res) => {
try {

const availableEvents: string[] = await ipcRenderer.invoke("api/routes");
res.json({
name: "YTMDesktop2 Api",
beta: appConfig?.app.beta,
player: appConfig?.player
player: appConfig?.player,
routes: availableEvents
})
} catch(err) {
res.status(500).json(err);
}
})
app.get("/track", async (req, res) => {
const track = await ipcRenderer.invoke("api/track");
res.json(track);
})
app.post("/track/*", async (req, res) => {
const track = await ipcRenderer.invoke("api/" + req.path.replace(/^\//g, ""));
res.json(track);
})
app.on("error", log.error);
const initialize = async ({config}: {config: SettingsStore}) => {
Expand Down
37 changes: 26 additions & 11 deletions src/app/main.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,29 @@
import logger from '@/utils/Logger';
import { app, BrowserView, BrowserWindow, BrowserWindowConstructorOptions, ipcMain, protocol, shell } from 'electron';
import { debounce } from 'lodash-es';
import path from 'path';
import { createProtocol } from 'vue-cli-plugin-electron-builder/lib';
import logger from "@/utils/Logger";
import {
app,
BrowserView,
BrowserWindow,
BrowserWindowConstructorOptions,
ipcMain,
protocol,
shell,
} from "electron";
import { debounce } from "lodash-es";
import path from "path";
import { createProtocol } from "vue-cli-plugin-electron-builder/lib";

import { defaultUrl, isDevelopment } from './utils/devUtils';
import { BrowserWindowViews, createWindowContext, getViewObject } from './utils/mappedWindow';
import { createEventCollection, createPluginCollection } from './utils/serviceCollection';
import { createApiView, createView } from './utils/view';
import { rootWindowInjectUtils } from './utils/webContentUtils';
import { defaultUrl, isDevelopment } from "./utils/devUtils";
import {
BrowserWindowViews,
createWindowContext,
getViewObject,
} from "./utils/mappedWindow";
import {
createEventCollection,
createPluginCollection,
} from "./utils/serviceCollection";
import { createApiView, createView } from "./utils/view";
import { rootWindowInjectUtils } from "./utils/webContentUtils";

function parseScriptPath(p: string) {
return path.resolve(__dirname, p);
Expand Down Expand Up @@ -325,10 +340,10 @@ export default async function() {
try {
if (!settingsWindow || settingsWindow.isDestroyed()) {
settingsWindow = await createAppWindow();
mainWindow.views.settingsWindow = settingsWindow as any;
} else {
settingsWindow.show();
}
mainWindow.views.settingsWindow = settingsWindow.getBrowserView();
} catch (err) {
log.error(err);
}
Expand Down
80 changes: 76 additions & 4 deletions src/app/plugins/apiProvider.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,36 @@
import { App, BrowserWindow } from "electron";
import { BaseProvider, AfterInit } from "../utils/baseProvider";
import { BaseProvider, AfterInit, OnDestroy } from "../utils/baseProvider";
import { ApiWorker, createApiWorker } from "@/api/createApiWorker";
import SettingsProvider from "./settingsProvider.plugin";

export default class ApiProvider extends BaseProvider implements AfterInit {
import { IpcContext, IpcHandle, IpcOn } from "../utils/onIpcEvent";
import TrackProvider from "./trackProvider.plugin";
const API_ROUTES = {
TRACK_CURRENT: "api/track",
TRACK_CONTROL_NEXT: "api/track/next",
TRACK_CONTROL_PREV: "api/track/prev",
TRACK_CONTROL_PLAY: "api/track/play",
TRACK_CONTROL_PAUSE: "api/track/pause",
TRACK_CONTROL_TOGGLE_PLAY: "api/track/toggle-play-state",
};
@IpcContext
export default class ApiProvider extends BaseProvider
implements AfterInit, OnDestroy {
private _thread: ApiWorker;
private _renderer: BrowserWindow;
constructor(private _app: App) {
super("api");
}
OnDestroy() {
this._thread?.destroy();
}
get app() {
return this._app;
}
sendMessage(...args: any[]) {
return this._thread?.send("socket", ...args);
}
async AfterInit() {
if (this._thread) await this._thread.destroy();
if (this._thread) this._thread.destroy();
this._thread = await createApiWorker();
const config = this.getProvider("settings") as SettingsProvider;
const rendererId = await this._thread.invoke<number>("initialize", {
Expand All @@ -26,4 +40,62 @@ export default class ApiProvider extends BaseProvider implements AfterInit {
(x) => x.id === rendererId
);
}

@IpcOn("settingsProvider.change", {
filter: (key: string) => key === "api.enabled",
debounce: 1000,
})
private async __onApiEnabled(key: string, value: boolean) {
if (!value) {
this._thread.destroy();
} else {
await this.AfterInit();
}
}
@IpcHandle("api/routes")
private async __getRoutes() {
return Object.values(API_ROUTES).map((x) => x.replace(/^\/api\//, ""));
}
@IpcHandle(API_ROUTES.TRACK_CURRENT)
async getTrackInformation() {
return (this.getProvider("track") as TrackProvider)?.trackData;
}
@IpcHandle(API_ROUTES.TRACK_CONTROL_NEXT)
async nextTrack() {
await this.views.youtubeView.webContents.executeJavaScript(
`(el => el && el.click())(document.querySelector(".ytmusic-player-bar.next-button"))`
);
}
@IpcHandle(API_ROUTES.TRACK_CONTROL_PREV)
async prevTrack() {
await this.views.youtubeView.webContents.executeJavaScript(
`(el => el && el.click())(document.querySelector(".ytmusic-player-bar.previous-button"))`
);
}
@IpcHandle(API_ROUTES.TRACK_CONTROL_PLAY)
async playTrack() {
await this.views.youtubeView.webContents.executeJavaScript(
`(el => el && el.title !== "Play" && el.click())(document.querySelector(".ytmusic-player-bar#play-pause-button"))`
);
}
@IpcHandle(API_ROUTES.TRACK_CONTROL_PAUSE)
async pauseTrack() {
await this.views.youtubeView.webContents.executeJavaScript(
`(el => el && el.title === "Play" && el.click())(document.querySelector(".ytmusic-player-bar#play-pause-button"))`
);
}
@IpcHandle(API_ROUTES.TRACK_CONTROL_TOGGLE_PLAY)
async toggleTrackPlayback() {
await this.views.youtubeView.webContents
.executeJavaScript(
`(el => el ? el.title === "Play" : null)(document.querySelector(".ytmusic-player-bar#play-pause-button"))`
)
.then((x) => {
return typeof x === "boolean"
? x
? this.pauseTrack()
: this.playTrack()
: null;
});
}
}
16 changes: 16 additions & 0 deletions src/app/plugins/client/track-play-state.plugin.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const requiredClasses = ["video-stream", "html5-main-video"];

export default () => {
const defaultPlay = HTMLVideoElement.prototype.play;
const defaultPause = HTMLVideoElement.prototype.pause;
HTMLVideoElement.prototype.play = function() {
defaultPlay.call(this, ...arguments);
if (requiredClasses.every((x) => this.classList.contains(x)))
window.api.emit("track:play-state", !this.paused, this.currentTime);
};
HTMLVideoElement.prototype.pause = function() {
defaultPause.call(this, ...arguments);
if (requiredClasses.every((x) => this.classList.contains(x)))
window.api.emit("track:play-state", !this.paused, this.currentTime);
};
};
39 changes: 28 additions & 11 deletions src/app/plugins/discordProvider.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,32 @@
import { IpcContext, IpcHandle, IpcOn } from "../utils/onIpcEvent";
import { Client as DiscordClient, Presence } from "discord-rpc";
import SettingsProvider from "./settingsProvider.plugin";
import { BaseProvider, AfterInit } from "../utils/baseProvider";
import TrackProvider from "./trackProvider.plugin";
import { debounce } from "lodash-es";
import { discordEmbedFromTrack, TrackData } from "../utils/trackData";
import { App } from "electron";
import { YoutubeMatcher } from "../utils/youtubeMatcher";
import { Client as DiscordClient, Presence } from 'discord-rpc';
import { App } from 'electron';
import { debounce } from 'lodash-es';

import { AfterInit, BaseProvider } from '../utils/baseProvider';
import { IpcContext, IpcHandle, IpcOn } from '../utils/onIpcEvent';
import { discordEmbedFromTrack, TrackData } from '../utils/trackData';
import { YoutubeMatcher } from '../utils/youtubeMatcher';
import SettingsProvider from './settingsProvider.plugin';
import TrackProvider from './trackProvider.plugin';

const DISCORD_UPDATE_INTERVAL = 1000 * 15;
const DEFAULT_PRESENCE: Presence = {
largeImageKey: "logo",
largeImageText: "Youtube Music for Desktop",
};
const CLIENT_ID = process.env.VUE_APP_DISCORD_CLIENT_ID;
@IpcContext
export default class EventProvider extends BaseProvider implements AfterInit {
export default class DiscordProvider extends BaseProvider implements AfterInit {
private _updateHandle: any;
private isConnected: boolean = false;
private client: DiscordClient;
private presence: Presence;
private _presence: Presence;
get presence() {
return this._presence;
}
set presence(val: Presence) {
this._presence = val;
}
get settingsInstance(): SettingsProvider {
return this.getProvider("settings");
}
Expand Down Expand Up @@ -82,6 +90,12 @@ export default class EventProvider extends BaseProvider implements AfterInit {
if (!settings.discord.enabled) return;
this.createClient();
}
async updatePlayState(val: boolean, progress: number = 0) {
if (this.trackService.trackData)
await this.setActivity(
discordEmbedFromTrack(this.trackService.trackData, val, progress)
);
}
async setActivity(presence: Partial<Presence>) {
if (!this.presence) return;
this.presence = { ...presence, ...DEFAULT_PRESENCE };
Expand All @@ -105,6 +119,9 @@ export default class EventProvider extends BaseProvider implements AfterInit {
if (YoutubeMatcher.Thumbnail.test(presence.largeImageKey)) {
this.presence.largeImageKey = presence.largeImageKey;
}
if (this.presence.startTimestamp === null)
delete this.presence.startTimestamp;
if (this.presence.endTimestamp === null) delete this.presence.endTimestamp;
if (this.client)
return await this.client
.setActivity(this.presence || DEFAULT_PRESENCE, process.pid)
Expand Down
24 changes: 20 additions & 4 deletions src/app/plugins/trackProvider.plugin.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { App, ipcMain } from "electron";
import { BaseProvider, AfterInit } from "../utils/baseProvider";
import { TrackData } from "../utils/trackData";
import { IpcContext, IpcOn } from "../utils/onIpcEvent";
import { App, ipcMain } from 'electron';

import { AfterInit, BaseProvider } from '../utils/baseProvider';
import { IpcContext, IpcOn } from '../utils/onIpcEvent';
import { TrackData } from '../utils/trackData';
import DiscordProvider from './discordProvider.plugin';

const tracks: { [id: string]: TrackData } = {};
@IpcContext
export default class TrackProvider extends BaseProvider implements AfterInit {
Expand Down Expand Up @@ -49,4 +52,17 @@ export default class TrackProvider extends BaseProvider implements AfterInit {
this._activeTrackId = trackId;
if (this.trackData) ipcMain.emit("track:change", this.trackData);
}
@IpcOn("track:play-state")
private __onPlayStateChange(_ev, val: boolean, progressSeconds: number = 0) {
this.logger.debug(
[
"play state change",
val ? "playing" : "paused",
", progress: ",
progressSeconds,
].join(" ")
);
const discordProvider = this.getProvider("discordRPC") as DiscordProvider;
discordProvider.updatePlayState(val, progressSeconds);
}
}
Loading

0 comments on commit 8e16a17

Please sign in to comment.