mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-11 18:41:47 +00:00
2
src/plugins/shortcuts/mpris-service.d.ts
vendored
2
src/plugins/shortcuts/mpris-service.d.ts
vendored
@ -56,7 +56,7 @@ declare module '@jellybrick/mpris-service' {
|
|||||||
playbackStatus: string;
|
playbackStatus: string;
|
||||||
loopStatus: string;
|
loopStatus: string;
|
||||||
shuffle: boolean;
|
shuffle: boolean;
|
||||||
metadata: object;
|
metadata: Track;
|
||||||
volume: number;
|
volume: number;
|
||||||
canControl: boolean;
|
canControl: boolean;
|
||||||
canPause: boolean;
|
canPause: boolean;
|
||||||
|
|||||||
@ -1,37 +1,98 @@
|
|||||||
import { BrowserWindow, ipcMain } from 'electron';
|
import { BrowserWindow, ipcMain } from 'electron';
|
||||||
|
|
||||||
import mpris, { Track } from '@jellybrick/mpris-service';
|
import MprisPlayer, { Track } from '@jellybrick/mpris-service';
|
||||||
|
|
||||||
import registerCallback from '@/providers/song-info';
|
import registerCallback, { type SongInfo } from '@/providers/song-info';
|
||||||
import getSongControls from '@/providers/song-controls';
|
import getSongControls from '@/providers/song-controls';
|
||||||
import config from '@/config';
|
import config from '@/config';
|
||||||
|
import { LoggerPrefix } from '@/utils';
|
||||||
|
|
||||||
|
class YTPlayer extends MprisPlayer {
|
||||||
|
/**
|
||||||
|
* @type {number} The current position in microseconds
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private currentPosition: number;
|
||||||
|
|
||||||
|
constructor(opts: {
|
||||||
|
name: string;
|
||||||
|
identity: string;
|
||||||
|
supportedMimeTypes?: string[];
|
||||||
|
supportedInterfaces?: string[];
|
||||||
|
}) {
|
||||||
|
super(opts);
|
||||||
|
|
||||||
|
this.currentPosition = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPosition(t: number) {
|
||||||
|
this.currentPosition = t;
|
||||||
|
}
|
||||||
|
|
||||||
|
getPosition(): number {
|
||||||
|
return this.currentPosition;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoopStatus(status: string) {
|
||||||
|
this.loopStatus = status;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPlaying(): boolean {
|
||||||
|
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PLAYING;
|
||||||
|
}
|
||||||
|
|
||||||
|
isPaused(): boolean {
|
||||||
|
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_PAUSED;
|
||||||
|
}
|
||||||
|
|
||||||
|
isStopped(): boolean {
|
||||||
|
return this.playbackStatus === YTPlayer.PLAYBACK_STATUS_STOPPED;
|
||||||
|
}
|
||||||
|
|
||||||
|
setPlaybackStatus(status: string) {
|
||||||
|
this.playbackStatus = status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function setupMPRIS() {
|
function setupMPRIS() {
|
||||||
const instance = new mpris({
|
const instance = new YTPlayer({
|
||||||
name: 'youtube-music',
|
name: 'youtube-music',
|
||||||
identity: 'YouTube Music',
|
identity: 'YouTube Music',
|
||||||
supportedMimeTypes: ['audio/mpeg'],
|
supportedMimeTypes: ['audio/mpeg'],
|
||||||
supportedInterfaces: ['player'],
|
supportedInterfaces: ['player'],
|
||||||
});
|
});
|
||||||
instance.canRaise = true;
|
instance.canRaise = true;
|
||||||
instance.supportedUriSchemes = ['https'];
|
instance.supportedUriSchemes = ['http', 'https'];
|
||||||
instance.desktopEntry = 'youtube-music';
|
instance.desktopEntry = 'youtube-music';
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerMPRIS(win: BrowserWindow) {
|
function registerMPRIS(win: BrowserWindow) {
|
||||||
const songControls = getSongControls(win);
|
const songControls = getSongControls(win);
|
||||||
const { playPause, next, previous, volumeMinus10, volumePlus10, shuffle } =
|
const {
|
||||||
songControls;
|
playPause,
|
||||||
|
next,
|
||||||
|
previous,
|
||||||
|
volumeMinus10,
|
||||||
|
volumePlus10,
|
||||||
|
shuffle,
|
||||||
|
switchRepeat,
|
||||||
|
} = songControls;
|
||||||
try {
|
try {
|
||||||
// TODO: "Typing" for this arguments
|
let currentSongInfo: SongInfo | null = null;
|
||||||
const secToMicro = (n: unknown) => Math.round(Number(n) * 1e6);
|
const secToMicro = (n: number) => Math.round(Number(n) * 1e6);
|
||||||
const microToSec = (n: unknown) => Math.round(Number(n) / 1e6);
|
const microToSec = (n: number) => Math.round(Number(n) / 1e6);
|
||||||
|
|
||||||
const seekTo = (e: { position: unknown }) =>
|
const seekTo = (event: {
|
||||||
win.webContents.send('ytmd:seek-to', microToSec(e.position));
|
trackId: string;
|
||||||
const seekBy = (o: unknown) =>
|
position: number;
|
||||||
win.webContents.send('ytmd:seek-by', microToSec(o));
|
}) => {
|
||||||
|
if (event.trackId === currentSongInfo?.videoId) {
|
||||||
|
win.webContents.send('ytmd:seek-to', microToSec(event.position ?? 0));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const seekBy = (offset: number) =>
|
||||||
|
win.webContents.send('ytmd:seek-by', microToSec(offset));
|
||||||
|
|
||||||
const player = setupMPRIS();
|
const player = setupMPRIS();
|
||||||
|
|
||||||
@ -44,21 +105,22 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
|
|
||||||
ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t)));
|
ipcMain.on('ytmd:seeked', (_, t: number) => player.seeked(secToMicro(t)));
|
||||||
|
|
||||||
let currentSeconds = 0;
|
ipcMain.on('ytmd:time-changed', (_, t: number) => {
|
||||||
ipcMain.on('ytmd:time-changed', (_, t: number) => (currentSeconds = t));
|
player.setPosition(secToMicro(t));
|
||||||
|
});
|
||||||
|
|
||||||
ipcMain.on('ytmd:repeat-changed', (_, mode: string) => {
|
ipcMain.on('ytmd:repeat-changed', (_, mode: string) => {
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 'NONE': {
|
case 'NONE': {
|
||||||
player.loopStatus = mpris.LOOP_STATUS_NONE;
|
player.setLoopStatus(YTPlayer.LOOP_STATUS_NONE);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ONE': {
|
case 'ONE': {
|
||||||
player.loopStatus = mpris.LOOP_STATUS_TRACK;
|
player.setLoopStatus(YTPlayer.LOOP_STATUS_TRACK);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case 'ALL': {
|
case 'ALL': {
|
||||||
player.loopStatus = mpris.LOOP_STATUS_PLAYLIST;
|
player.setLoopStatus(YTPlayer.LOOP_STATUS_PLAYLIST);
|
||||||
// No default
|
// No default
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -67,18 +129,17 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
player.on('loopStatus', (status: string) => {
|
player.on('loopStatus', (status: string) => {
|
||||||
// SwitchRepeat cycles between states in that order
|
// SwitchRepeat cycles between states in that order
|
||||||
const switches = [
|
const switches = [
|
||||||
mpris.LOOP_STATUS_NONE,
|
YTPlayer.LOOP_STATUS_NONE,
|
||||||
mpris.LOOP_STATUS_PLAYLIST,
|
YTPlayer.LOOP_STATUS_PLAYLIST,
|
||||||
mpris.LOOP_STATUS_TRACK,
|
YTPlayer.LOOP_STATUS_TRACK,
|
||||||
];
|
];
|
||||||
const currentIndex = switches.indexOf(player.loopStatus);
|
const currentIndex = switches.indexOf(player.loopStatus);
|
||||||
const targetIndex = switches.indexOf(status);
|
const targetIndex = switches.indexOf(status);
|
||||||
|
|
||||||
// Get a delta in the range [0,2]
|
// Get a delta in the range [0,2]
|
||||||
const delta = (targetIndex - currentIndex + 3) % 3;
|
const delta = (targetIndex - currentIndex + 3) % 3;
|
||||||
songControls.switchRepeat(delta);
|
switchRepeat(delta);
|
||||||
});
|
});
|
||||||
player.getPosition = () => secToMicro(currentSeconds);
|
|
||||||
|
|
||||||
player.on('raise', () => {
|
player.on('raise', () => {
|
||||||
win.setSkipTaskbar(false);
|
win.setSkipTaskbar(false);
|
||||||
@ -86,22 +147,23 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
player.on('play', () => {
|
player.on('play', () => {
|
||||||
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PLAYING) {
|
if (!player.isPlaying()) {
|
||||||
player.playbackStatus = mpris.PLAYBACK_STATUS_PLAYING;
|
player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PLAYING);
|
||||||
playPause();
|
playPause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
player.on('pause', () => {
|
player.on('pause', () => {
|
||||||
if (player.playbackStatus !== mpris.PLAYBACK_STATUS_PAUSED) {
|
if (player.playbackStatus !== YTPlayer.PLAYBACK_STATUS_PAUSED) {
|
||||||
player.playbackStatus = mpris.PLAYBACK_STATUS_PAUSED;
|
player.setPlaybackStatus(YTPlayer.PLAYBACK_STATUS_PAUSED);
|
||||||
playPause();
|
playPause();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
player.on('playpause', () => {
|
player.on('playpause', () => {
|
||||||
player.playbackStatus =
|
player.setPlaybackStatus(
|
||||||
player.playbackStatus === mpris.PLAYBACK_STATUS_PLAYING
|
player.isPlaying()
|
||||||
? mpris.PLAYBACK_STATUS_PAUSED
|
? YTPlayer.PLAYBACK_STATUS_PAUSED
|
||||||
: mpris.PLAYBACK_STATUS_PLAYING;
|
: YTPlayer.PLAYBACK_STATUS_PLAYING
|
||||||
|
);
|
||||||
playPause();
|
playPause();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -170,21 +232,32 @@ function registerMPRIS(win: BrowserWindow) {
|
|||||||
'xesam:title': songInfo.title,
|
'xesam:title': songInfo.title,
|
||||||
'xesam:url': songInfo.url,
|
'xesam:url': songInfo.url,
|
||||||
'xesam:artist': [songInfo.artist],
|
'xesam:artist': [songInfo.artist],
|
||||||
'mpris:trackid': '/',
|
'mpris:trackid': songInfo.videoId,
|
||||||
};
|
};
|
||||||
if (songInfo.album) {
|
if (songInfo.album) {
|
||||||
data['xesam:album'] = songInfo.album;
|
data['xesam:album'] = songInfo.album;
|
||||||
}
|
}
|
||||||
|
currentSongInfo = songInfo;
|
||||||
|
|
||||||
player.metadata = data;
|
player.metadata = data;
|
||||||
player.seeked(secToMicro(songInfo.elapsedSeconds));
|
|
||||||
player.playbackStatus = songInfo.isPaused
|
const currentElapsedMicroSeconds = secToMicro(songInfo.elapsedSeconds ?? 0);
|
||||||
? mpris.PLAYBACK_STATUS_PAUSED
|
player.setPosition(currentElapsedMicroSeconds);
|
||||||
: mpris.PLAYBACK_STATUS_PLAYING;
|
player.seeked(currentElapsedMicroSeconds);
|
||||||
|
|
||||||
|
player.setPlaybackStatus(
|
||||||
|
songInfo.isPaused ?
|
||||||
|
YTPlayer.PLAYBACK_STATUS_PAUSED :
|
||||||
|
YTPlayer.PLAYBACK_STATUS_PLAYING
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.warn('Error in MPRIS', error);
|
console.error(
|
||||||
|
LoggerPrefix,
|
||||||
|
'Error in MPRIS'
|
||||||
|
);
|
||||||
|
console.trace(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -29,8 +29,9 @@ export const setupTimeChangedListener = singleton(() => {
|
|||||||
const progressObserver = new MutationObserver((mutations) => {
|
const progressObserver = new MutationObserver((mutations) => {
|
||||||
for (const mutation of mutations) {
|
for (const mutation of mutations) {
|
||||||
const target = mutation.target as Node & { value: string };
|
const target = mutation.target as Node & { value: string };
|
||||||
window.ipcRenderer.send('ytmd:time-changed', target.value);
|
const numberValue = Number(target.value);
|
||||||
songInfo.elapsedSeconds = Number(target.value);
|
window.ipcRenderer.send('ytmd:time-changed', numberValue);
|
||||||
|
songInfo.elapsedSeconds = numberValue;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const progressBar = document.querySelector('#progress-bar');
|
const progressBar = document.querySelector('#progress-bar');
|
||||||
|
|||||||
Reference in New Issue
Block a user