Skip to content

Commit

Permalink
feat: add streamdeck+ support
Browse files Browse the repository at this point in the history
  • Loading branch information
nosjo authored Aug 18, 2024
1 parent 8383d18 commit 040c3a5
Show file tree
Hide file tree
Showing 4 changed files with 168 additions and 12 deletions.
20 changes: 20 additions & 0 deletions manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand Down
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
Expand Down Expand Up @@ -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": {
Expand Down
79 changes: 75 additions & 4 deletions src/actions/play-pause.action.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
DialUpEvent,
DialRotateEvent,
DidReceiveSettingsEvent,
KeyUpEvent,
SDOnActionEvent,
Expand All @@ -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<PlayPauseAction> {
private trackState: TrackState = TrackState.UNKNOWN;
Expand All @@ -22,7 +25,10 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
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);
Expand Down Expand Up @@ -71,7 +77,28 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {

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:
Expand Down Expand Up @@ -109,7 +136,7 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
}

@SDOnActionEvent('keyUp')
onKeypressUp({context, payload: {settings}}: KeyUpEvent<PlayPauseSettings>) {
onKeypressUp({context, payload: {settings}}: KeyUpEvent) {
if (!settings?.action) {
this.rest.playPause().catch(reason => {
console.error(reason);
Expand Down Expand Up @@ -141,7 +168,6 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
});
break;
}

this.plugin.setState(this.trackState === TrackState.PLAYING ? StateType.ON : StateType.OFF, context);
}

Expand All @@ -158,11 +184,23 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
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) {
Expand All @@ -174,6 +212,24 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
}
}

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;
Expand Down Expand Up @@ -204,4 +260,19 @@ export class PlayPauseAction extends DefaultAction<PlayPauseAction> {
private handleSettings(e: DidReceiveSettingsEvent<PlayPauseSettings>) {
this.contextFormat[e.context] = e.payload.settings?.displayFormat ?? this.contextFormat[e.context];
}

@SDOnActionEvent('dialUp')
onDialUp({context, payload: {settings}}: DialUpEvent<PlayPauseSettings>) {
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;
}
}
76 changes: 70 additions & 6 deletions src/actions/vol-change.action.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -7,7 +7,10 @@ export class VolChangeAction extends DefaultAction<VolChangeAction> {
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,
Expand All @@ -18,22 +21,62 @@ export class VolChangeAction extends DefaultAction<VolChangeAction> {
}

@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);

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);
Expand Down Expand Up @@ -69,6 +112,27 @@ export class VolChangeAction extends DefaultAction<VolChangeAction> {
}
}

@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<void> {
return new Promise((resolve) => setTimeout(() => resolve(), ms));
}
Expand Down

0 comments on commit 040c3a5

Please sign in to comment.