From 040c3a524e015dfc11c1fe9df6fe10191ff41bf8 Mon Sep 17 00:00:00 2001 From: Jonas Date: Sun, 18 Aug 2024 16:53:32 +0200 Subject: [PATCH] feat: add streamdeck+ support --- manifest.json | 20 ++++++++ package.json | 5 +- src/actions/play-pause.action.ts | 79 ++++++++++++++++++++++++++++++-- src/actions/vol-change.action.ts | 76 +++++++++++++++++++++++++++--- 4 files changed, 168 insertions(+), 12 deletions(-) diff --git a/manifest.json b/manifest.json index b9cc029..47f6e4c 100644 --- a/manifest.json +++ b/manifest.json @@ -37,6 +37,16 @@ "FontSize": "16" } ], + "Controllers": ["Keypad", "Encoder"], + "DisableAutomaticStates": false, + "Encoder": { + "layout": "$B1", + "StackColor": "#AABBCC", + "TriggerDescription": { + "Rotate": "Switch song", + "Push": "Plays or Pauses the current song" + } + }, "SupportedInMultiActions": true, "Tooltip": "Plays or Pauses the current song", "UUID": "fun.shiro.ytmdc.play-pause" @@ -150,6 +160,16 @@ "FontSize": "16" } ], + "Controllers": ["Keypad", "Encoder"], + "DisableAutomaticStates": false, + "Encoder": { + "layout": "$B1", + "StackColor": "#AABBCC", + "TriggerDescription": { + "Rotate": "Volume control", + "Push": "Mute" + } + }, "SupportedInMultiActions": true, "Tooltip": "Volume Up", "UUID": "fun.shiro.ytmdc.volume-up" diff --git a/package.json b/package.json index 246b774..c050015 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ }, "homepage": "https://github.com/XeroxDev/YTMD-StreamDeck#readme", "dependencies": { - "streamdeck-typescript": "^3.1.4", + "streamdeck-typescript": "^3.2.0", "ytmdesktop-ts-companion": "^1.0.5" }, "keywords": [ @@ -58,7 +58,8 @@ "ts-node": "^10.9.2", "tsify": "^5.0.4", "typescript": "^5.3.3", - "watchify": "^4.0.0" + "watchify": "^4.0.0", + "node-canvas": "^2.0.0" }, "husky": { "hooks": { diff --git a/src/actions/play-pause.action.ts b/src/actions/play-pause.action.ts index b8d8353..2a4cbf2 100644 --- a/src/actions/play-pause.action.ts +++ b/src/actions/play-pause.action.ts @@ -1,4 +1,6 @@ import { + DialUpEvent, + DialRotateEvent, DidReceiveSettingsEvent, KeyUpEvent, SDOnActionEvent, @@ -10,6 +12,7 @@ import {YTMD} from '../ytmd'; import {DefaultAction} from './default.action'; import {PlayPauseSettings} from "../interfaces/context-settings.interface"; import {SocketState, StateOutput, TrackState} from "ytmdesktop-ts-companion"; +const { createCanvas, loadImage } = require('canvas'); export class PlayPauseAction extends DefaultAction { private trackState: TrackState = TrackState.UNKNOWN; @@ -22,7 +25,10 @@ export class PlayPauseAction extends DefaultAction { onError: (error: any) => void, onConChange: (state: SocketState) => void }[] = []; - + private currentThumbnail: string; + private thumbnail: string; + private ticks = 0; + private lastcheck = 0; constructor(private plugin: YTMD, actionName: string) { super(plugin, actionName); @@ -71,7 +77,28 @@ export class PlayPauseAction extends DefaultAction { found = { context: event.context, - onTick: (state: StateOutput) => this.handlePlayerData(event, state), + onTick: (state: StateOutput) => { + this.handlePlayerData(event, state); + if (this.lastcheck === 0 && this.ticks !== 0) + { + if (this.ticks > 0) this.rest.next().catch(reason => { + console.error(reason); + this.plugin.logMessage(`Error while next. event: ${JSON.stringify(event)}, error: ${JSON.stringify(reason)}`); + this.plugin.showAlert(event.context) + }) + else this.rest.previous().catch(reason => { + console.error(reason); + this.plugin.logMessage(`Error while previous. event: ${JSON.stringify(event)}, error: ${JSON.stringify(reason)}`); + this.plugin.showAlert(event.context) + }) + this.ticks = 0; + this.lastcheck = 3; + } + if (this.lastcheck > 0) + { + this.lastcheck -= 1; + } + }, onConChange: (state: SocketState) => { switch (state) { case SocketState.CONNECTED: @@ -109,7 +136,7 @@ export class PlayPauseAction extends DefaultAction { } @SDOnActionEvent('keyUp') - onKeypressUp({context, payload: {settings}}: KeyUpEvent) { + onKeypressUp({context, payload: {settings}}: KeyUpEvent) { if (!settings?.action) { this.rest.playPause().catch(reason => { console.error(reason); @@ -141,7 +168,6 @@ export class PlayPauseAction extends DefaultAction { }); break; } - this.plugin.setState(this.trackState === TrackState.PLAYING ? StateType.ON : StateType.OFF, context); } @@ -158,11 +184,23 @@ export class PlayPauseAction extends DefaultAction { let remaining = duration - current; const title = this.formatTitle(current, duration, remaining, context, settings); + const cover = this.getSongCover(data); if (this.currentTitle !== title || this.firstTimes >= 1) { this.firstTimes--; this.currentTitle = title; this.plugin.setTitle(this.currentTitle, context); + this.plugin.setFeedback(context, {"icon": this.thumbnail, "value": this.currentTitle, "indicator": { "value": current / duration * 100, "enabled": true}}); + if (this.currentThumbnail !== cover) + { + this.currentThumbnail = cover; + const canvas = createCanvas(48, 48); + const ctx = canvas.getContext('2d'); + loadImage(cover).then((image: any) => { + ctx.drawImage(image, 0, 0, 48, 48) + this.thumbnail = canvas.toDataURL('image/png'); + }); + } } if (this.trackState !== data.player.trackState) { @@ -174,6 +212,24 @@ export class PlayPauseAction extends DefaultAction { } } + private getSongCover(data: StateOutput): string { + let cover = ""; + + if (!data.player || !data.video) return cover; + + const trackState = data.player.trackState; + + switch (trackState) { + case TrackState.PLAYING: + cover = data.video.thumbnails[data.video.thumbnails.length - 1].url ?? cover; + break; + default: + break; + } + + return cover; + } + private formatTitle(current: number, duration: number, remaining: number, context: string, settings: PlayPauseSettings): string { current = current ?? 0; duration = duration ?? 0; @@ -204,4 +260,19 @@ export class PlayPauseAction extends DefaultAction { private handleSettings(e: DidReceiveSettingsEvent) { this.contextFormat[e.context] = e.payload.settings?.displayFormat ?? this.contextFormat[e.context]; } + + @SDOnActionEvent('dialUp') + onDialUp({context, payload: {settings}}: DialUpEvent) { + this.rest.playPause().catch(reason => { + console.error(reason); + this.plugin.logMessage(`Error while playPause toggle. context: ${JSON.stringify(context)}, error: ${JSON.stringify(reason)}`); + this.plugin.showAlert(context) + }); + this.plugin.setState(this.trackState === TrackState.PLAYING ? StateType.ON : StateType.OFF, context); + } + + @SDOnActionEvent('dialRotate') + onDialRotate({context, payload: {settings, ticks}}: DialRotateEvent) { + this.ticks += ticks; + } } diff --git a/src/actions/vol-change.action.ts b/src/actions/vol-change.action.ts index ea0bf27..235c559 100644 --- a/src/actions/vol-change.action.ts +++ b/src/actions/vol-change.action.ts @@ -1,4 +1,4 @@ -import {KeyDownEvent, KeyUpEvent, SDOnActionEvent, WillAppearEvent, WillDisappearEvent,} from 'streamdeck-typescript'; +import {KeyDownEvent, KeyUpEvent, SDOnActionEvent, WillAppearEvent, WillDisappearEvent, DialRotateEvent, DialUpEvent} from 'streamdeck-typescript'; import {YTMD} from '../ytmd'; import {DefaultAction} from './default.action'; import {StateOutput} from "ytmdesktop-ts-companion"; @@ -7,7 +7,10 @@ export class VolChangeAction extends DefaultAction { private keyDown: boolean = false; private currentVolume: number = 50; private events: { context: string, method: (state: StateOutput) => void }[] = []; - + private lastVolume = 0; + private ticks = 0; + private lastcheck = 0; + private iconpath: string; constructor( private plugin: YTMD, @@ -18,15 +21,40 @@ export class VolChangeAction extends DefaultAction { } @SDOnActionEvent('willAppear') - onContextAppear(event: WillAppearEvent): void { - let found = this.events.find(e => e.context === event.context); + onContextAppear({context, payload: {settings}}: WillAppearEvent): void { + let found = this.events.find(e => e.context === context); if (found) { return; } found = { - context: event.context, - method: (state: StateOutput) => this.currentVolume = state.player.volume + context: context, + method: (state: StateOutput) => { + this.currentVolume = state.player.volume; + this.updateIcon(); + this.plugin.setFeedback(context, {"icon": this.iconpath, "title": "Volume", "value": this.currentVolume + "%", "indicator": { "value": this.currentVolume, "enabled": true}}); + if (this.lastcheck === 0 && this.ticks !== 0) + { + let newVolume = this.currentVolume + this.lastVolume; + this.lastVolume = 0; + newVolume += (settings?.steps ?? 2) * this.ticks; + + this.rest.setVolume(newVolume < 0 ? 0 : newVolume > 100 ? 100 : newVolume).catch(reason => { + newVolume = this.currentVolume; + console.error(reason); + this.plugin.logMessage(`Error while setting volume. volume: ${newVolume}, context: ${JSON.stringify(context)}, error: ${JSON.stringify(reason)}`); + this.plugin.showAlert(context) + }).finally(() => { + this.currentVolume = newVolume; + }); + this.ticks = 0 + this.lastcheck = 3; + } + if (this.lastcheck > 0) + { + this.lastcheck -= 1; + } + } }; this.events.push(found); @@ -34,6 +62,21 @@ export class VolChangeAction extends DefaultAction { this.socket.addStateListener(found.method); } + private updateIcon(): void { + if (this.currentVolume >= 66) { + this.iconpath = "icons/volume-up"; + } + else if (this.currentVolume >= 33) { + this.iconpath = "icons/volume-on"; + } + else if (this.currentVolume <= 0) { + this.iconpath = "icons/volume-mute"; + } + else { + this.iconpath = "icons/volume-down"; + } + } + @SDOnActionEvent('willDisappear') onContextDisappear(event: WillDisappearEvent): void { const found = this.events.find(e => e.context === event.context); @@ -69,6 +112,27 @@ export class VolChangeAction extends DefaultAction { } } + @SDOnActionEvent('dialUp') + onDialUp({context, payload: {settings}}: DialUpEvent) { + if (this.currentVolume <= 0) { + this.currentVolume = this.lastVolume; + this.lastVolume = 0; + } else { + this.lastVolume = this.currentVolume; + this.currentVolume = 0; + } + this.rest.setVolume(this.currentVolume).catch(reason => { + console.error(reason); + this.plugin.logMessage(`Error while setting volume. volume: ${this.currentVolume}, context: ${JSON.stringify(context)}, error: ${JSON.stringify(reason)}`); + this.plugin.showAlert(context) + }); + } + + @SDOnActionEvent('dialRotate') + onDialRotate({context, payload: {settings, ticks}}: DialRotateEvent) { + this.ticks += ticks; + } + private wait(ms: number): Promise { return new Promise((resolve) => setTimeout(() => resolve(), ms)); }