diff --git a/src/plugins/api-server/backend/main.ts b/src/plugins/api-server/backend/main.ts index f5b6c5fb..b1c011d4 100644 --- a/src/plugins/api-server/backend/main.ts +++ b/src/plugins/api-server/backend/main.ts @@ -23,6 +23,8 @@ export const backend = createBackend({ this.songInfo = songInfo; }); + ctx.ipc.on('ytmd:player-api-loaded', () => ctx.ipc.send('ytmd:setup-time-changed-listener')); + this.run(config.hostname, config.port); }, stop() { diff --git a/src/plugins/discord/main.ts b/src/plugins/discord/main.ts index 5b62353c..3d2b1121 100644 --- a/src/plugins/discord/main.ts +++ b/src/plugins/discord/main.ts @@ -1,10 +1,13 @@ -import { app, dialog, ipcMain } from 'electron'; +import { app, dialog } from 'electron'; import { Client as DiscordClient } from '@xhayper/discord-rpc'; import { dev } from 'electron-is'; import { ActivityType, GatewayActivityButton } from 'discord-api-types/v10'; -import registerCallback, { type SongInfo } from '@/providers/song-info'; +import registerCallback, { + type SongInfo, + SongInfoEvent, +} from '@/providers/song-info'; import { createBackend, LoggerPrefix } from '@/utils'; import { t } from '@/i18n'; @@ -243,25 +246,28 @@ export const backend = createBackend< // If the page is ready, register the callback ctx.window.once('ready-to-show', () => { - let lastSongInfo: SongInfo; - registerCallback((songInfo) => { - lastSongInfo = songInfo; - if (this.config) this.updateActivity(songInfo, this.config); - }); - connect(); let lastSent = Date.now(); - ipcMain.on('ytmd:time-changed', (_, t: number) => { - const currentTime = Date.now(); - // if lastSent is more than 5 seconds ago, send the new time - if (currentTime - lastSent > 5000) { - lastSent = currentTime; - if (lastSongInfo) { - lastSongInfo.elapsedSeconds = t; - if (this.config) this.updateActivity(lastSongInfo, this.config); + registerCallback((songInfo, event) => { + if (event !== SongInfoEvent.TimeChanged) { + info.lastSongInfo = songInfo; + if (this.config) this.updateActivity(songInfo, this.config); + } else { + const currentTime = Date.now(); + // if lastSent is more than 5 seconds ago, send the new time + if (currentTime - lastSent > 5000) { + lastSent = currentTime; + if (songInfo) { + info.lastSongInfo = songInfo; + if (this.config) this.updateActivity(songInfo, this.config); + } } } }); + connect(); }); + ctx.ipc.on('ytmd:player-api-loaded', () => + ctx.ipc.send('ytmd:setup-time-changed-listener'), + ); app.on('window-all-closed', clear); }, stop() { diff --git a/src/plugins/downloader/main/index.ts b/src/plugins/downloader/main/index.ts index ee7fba01..3bb4b76e 100644 --- a/src/plugins/downloader/main/index.ts +++ b/src/plugins/downloader/main/index.ts @@ -30,12 +30,13 @@ import registerCallback, { getImage, MediaType, type SongInfo, + SongInfoEvent, } from '@/providers/song-info'; import { getNetFetchAsFetch } from '@/plugins/utils/main'; import { t } from '@/i18n'; -import { YoutubeFormatList, type Preset, DefaultPresetList } from '../types'; +import { DefaultPresetList, type Preset, YoutubeFormatList } from '../types'; import type { DownloaderPluginConfig } from '../index'; @@ -68,7 +69,12 @@ const sendError = (error: Error, source?: string) => { sendFeedback_(win); // Reset feedback const songNameMessage = source ? `\nin ${source}` : ''; - const cause = error.cause ? `\n\n${String(error.cause)}` : ''; + const cause = error.cause + ? `\n\n${ + // eslint-disable-next-line @typescript-eslint/no-base-to-string,@typescript-eslint/restrict-template-expressions + error.cause instanceof Error ? error.cause.toString() : error.cause + }` + : ''; const message = `${error.toString()}${songNameMessage}${cause}`; console.error(message); @@ -174,7 +180,12 @@ function downloadSongOnFinishSetup({ const defaultDownloadFolder = app.getPath('downloads'); - registerCallback((songInfo: SongInfo) => { + registerCallback((songInfo: SongInfo, event) => { + if (event === SongInfoEvent.TimeChanged) { + const elapsedSeconds = songInfo.elapsedSeconds ?? 0; + if (elapsedSeconds > time) time = elapsedSeconds; + return; + } if ( !songInfo.isPaused && songInfo.url !== currentUrl && @@ -213,10 +224,6 @@ function downloadSongOnFinishSetup({ ipcMain.on('ytmd:player-api-loaded', () => { ipc.send('ytmd:setup-time-changed-listener'); }); - - ipcMain.on('ytmd:time-changed', (_, t: number) => { - if (t > time) time = t; - }); } async function downloadSongUnsafe( diff --git a/src/plugins/lumiastream/index.ts b/src/plugins/lumiastream/index.ts index 2181646d..351337f5 100644 --- a/src/plugins/lumiastream/index.ts +++ b/src/plugins/lumiastream/index.ts @@ -30,7 +30,7 @@ export default createPlugin({ config: { enabled: false, }, - backend() { + backend({ ipc }) { const secToMilisec = (t?: number) => t ? Math.round(Number(t) * 1e3) : undefined; const previousStatePaused = null; @@ -65,6 +65,10 @@ export default createPlugin({ }); }; + ipc.on('ytmd:player-api-loaded', () => + ipc.send('ytmd:setup-time-changed-listener'), + ); + registerCallback((songInfo) => { if (!songInfo.title && !songInfo.artist) { return; diff --git a/src/plugins/notifications/interactive.ts b/src/plugins/notifications/interactive.ts index e20c411b..a7c96336 100644 --- a/src/plugins/notifications/interactive.ts +++ b/src/plugins/notifications/interactive.ts @@ -8,7 +8,10 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac import { notificationImage, secondsToMinutes, ToastStyles } from './utils'; import getSongControls from '@/providers/song-controls'; -import registerCallback, { SongInfo } from '@/providers/song-info'; +import registerCallback, { + type SongInfo, + SongInfoEvent, +} from '@/providers/song-info'; import { changeProtocolHandler } from '@/providers/protocol-handler'; import { setTrayOnClick, setTrayOnDoubleClick } from '@/tray'; import { mediaIcons } from '@/types/media-icons'; @@ -258,15 +261,14 @@ export default ( let currentSeconds = 0; on('ytmd:player-api-loaded', () => send('ytmd:setup-time-changed-listener')); - on('ytmd:time-changed', (t: number) => { - currentSeconds = t; - }); - let savedSongInfo: SongInfo; let lastUrl: string | undefined; // Register songInfoCallback - registerCallback((songInfo) => { + registerCallback((songInfo, event) => { + if (event === SongInfoEvent.TimeChanged) { + currentSeconds = songInfo.elapsedSeconds ?? 0; + } if (!songInfo.artist && !songInfo.title) { return; } diff --git a/src/plugins/notifications/main.ts b/src/plugins/notifications/main.ts index 0782c6a0..dbf3d94b 100644 --- a/src/plugins/notifications/main.ts +++ b/src/plugins/notifications/main.ts @@ -5,7 +5,10 @@ import is from 'electron-is'; import { notificationImage } from './utils'; import interactive from './interactive'; -import registerCallback, { type SongInfo } from '@/providers/song-info'; +import registerCallback, { + type SongInfo, + SongInfoEvent, +} from '@/providers/song-info'; import type { NotificationsPluginConfig } from './index'; import type { BackendContext } from '@/types/contexts'; @@ -30,8 +33,9 @@ const setup = () => { let oldNotification: Notification; let currentUrl: string | undefined; - registerCallback((songInfo: SongInfo) => { + registerCallback((songInfo: SongInfo, event) => { if ( + event !== SongInfoEvent.TimeChanged && !songInfo.isPaused && (songInfo.url !== currentUrl || config.unpauseNotification) ) { diff --git a/src/plugins/scrobbler/main.ts b/src/plugins/scrobbler/main.ts index 73053f13..91a02a75 100644 --- a/src/plugins/scrobbler/main.ts +++ b/src/plugins/scrobbler/main.ts @@ -3,6 +3,7 @@ import { BrowserWindow } from 'electron'; import registerCallback, { MediaType, type SongInfo, + SongInfoEvent, } from '@/providers/song-info'; import { createBackend } from '@/utils'; @@ -70,7 +71,8 @@ export const backend = createBackend< await this.createSessions(config, setConfig); this.setConfig = setConfig; - registerCallback((songInfo: SongInfo) => { + registerCallback((songInfo: SongInfo, event) => { + if (event === SongInfoEvent.TimeChanged) return; // Set remove the old scrobble timer clearTimeout(scrobbleTimer); if (!songInfo.isPaused) { diff --git a/src/plugins/shortcuts/mpris.ts b/src/plugins/shortcuts/mpris.ts index bdd96d96..928e2c32 100644 --- a/src/plugins/shortcuts/mpris.ts +++ b/src/plugins/shortcuts/mpris.ts @@ -1,20 +1,23 @@ import { BrowserWindow, ipcMain } from 'electron'; import MprisPlayer, { - Track, - LoopStatus, - type PlayBackStatus, - type PlayerOptions, - PLAYBACK_STATUS_STOPPED, - PLAYBACK_STATUS_PAUSED, - PLAYBACK_STATUS_PLAYING, LOOP_STATUS_NONE, LOOP_STATUS_PLAYLIST, LOOP_STATUS_TRACK, + LoopStatus, + PLAYBACK_STATUS_PAUSED, + PLAYBACK_STATUS_PLAYING, + PLAYBACK_STATUS_STOPPED, + type PlayBackStatus, + type PlayerOptions, type Position, + Track, } from '@jellybrick/mpris-service'; -import registerCallback, { type SongInfo } from '@/providers/song-info'; +import registerCallback, { + type SongInfo, + SongInfoEvent, +} from '@/providers/song-info'; import getSongControls from '@/providers/song-controls'; import config from '@/config'; import { LoggerPrefix } from '@/utils'; @@ -134,10 +137,6 @@ function registerMPRIS(win: BrowserWindow) { player.seeked(secToMicro(t)); }); - ipcMain.on('ytmd:time-changed', (_, t: number) => { - player.setPosition(secToMicro(t)); - }); - ipcMain.on('ytmd:repeat-changed', (_, mode: RepeatMode) => { switch (mode) { case 'NONE': { @@ -319,7 +318,11 @@ function registerMPRIS(win: BrowserWindow) { } }); - registerCallback((songInfo: SongInfo) => { + registerCallback((songInfo: SongInfo, event) => { + if (event === SongInfoEvent.TimeChanged) { + player.setPosition(secToMicro(songInfo.elapsedSeconds ?? 0)); + return; + } if (player) { const data: Track = { 'mpris:length': secToMicro(songInfo.songDuration), diff --git a/src/plugins/taskbar-mediacontrol/index.ts b/src/plugins/taskbar-mediacontrol/index.ts index abdb38a8..0e634a67 100644 --- a/src/plugins/taskbar-mediacontrol/index.ts +++ b/src/plugins/taskbar-mediacontrol/index.ts @@ -8,7 +8,10 @@ import previousIcon from '@assets/media-icons-black/previous.png?asset&asarUnpac import { createPlugin } from '@/utils'; import getSongControls from '@/providers/song-controls'; -import registerCallback, { type SongInfo } from '@/providers/song-info'; +import registerCallback, { + type SongInfo, + SongInfoEvent, +} from '@/providers/song-info'; import { mediaIcons } from '@/types/media-icons'; import { t } from '@/i18n'; @@ -102,11 +105,13 @@ export default createPlugin({ ]); }; - registerCallback((songInfo) => { - // Update currentsonginfo for win.on('show') - currentSongInfo = songInfo; - // Update thumbar - setThumbar(songInfo); + registerCallback((songInfo, event) => { + if (event !== SongInfoEvent.TimeChanged) { + // Update currentsonginfo for win.on('show') + currentSongInfo = songInfo; + // Update thumbar + setThumbar(songInfo); + } }); // Need to set thumbar again after win.show diff --git a/src/plugins/touchbar/index.ts b/src/plugins/touchbar/index.ts index 8b75d15e..501452fb 100644 --- a/src/plugins/touchbar/index.ts +++ b/src/plugins/touchbar/index.ts @@ -2,7 +2,7 @@ import { nativeImage, type NativeImage, TouchBar } from 'electron'; import { createPlugin } from '@/utils'; import getSongControls from '@/providers/song-controls'; -import registerCallback from '@/providers/song-info'; +import registerCallback, { SongInfoEvent } from '@/providers/song-info'; import { t } from '@/i18n'; import youtubeMusicIcon from '@assets/youtube-music.png?asset&asarUnpack'; @@ -81,7 +81,8 @@ export default createPlugin({ controls = [previous, playPause, next, dislike, like]; // Register the callback - registerCallback((songInfo) => { + registerCallback((songInfo, event) => { + if (event === SongInfoEvent.TimeChanged) return; // Song information changed, so lets update the touchBar // Set the song title diff --git a/src/plugins/tuna-obs/index.ts b/src/plugins/tuna-obs/index.ts index edc2da87..07d82f04 100644 --- a/src/plugins/tuna-obs/index.ts +++ b/src/plugins/tuna-obs/index.ts @@ -28,18 +28,6 @@ export default createPlugin({ }, backend: { liteMode: false, - data: { - cover: '', - cover_url: '', - title: '', - artists: [] as string[], - status: '', - progress: 0, - duration: 0, - album_url: '', - album: undefined, - url: '', - } as Data, start({ ipc }) { const secToMilisec = (t: number) => Math.round(Number(t) * 1e3); @@ -85,31 +73,24 @@ export default createPlugin({ ipc.on('ytmd:player-api-loaded', () => ipc.send('ytmd:setup-time-changed-listener'), ); - ipc.on('ytmd:time-changed', (t: number) => { - if (!this.data.title) { - return; - } - - this.data.progress = secToMilisec(t); - post(this.data); - }); registerCallback((songInfo) => { if (!songInfo.title && !songInfo.artist) { return; } - this.data.duration = secToMilisec(songInfo.songDuration); - this.data.progress = secToMilisec(songInfo.elapsedSeconds ?? 0); - this.data.cover = songInfo.imageSrc ?? ''; - this.data.cover_url = songInfo.imageSrc ?? ''; - this.data.album_url = songInfo.imageSrc ?? ''; - this.data.title = songInfo.title; - this.data.artists = [songInfo.artist]; - this.data.status = songInfo.isPaused ? 'stopped' : 'playing'; - this.data.album = songInfo.album; - this.data.url = songInfo.url ?? ''; - post(this.data); + post({ + duration: secToMilisec(songInfo.songDuration), + progress: secToMilisec(songInfo.elapsedSeconds ?? 0), + cover: songInfo.imageSrc ?? '', + cover_url: songInfo.imageSrc ?? '', + album_url: songInfo.imageSrc ?? '', + title: songInfo.title, + artists: [songInfo.artist], + status: songInfo.isPaused ? 'stopped' : 'playing', + album: songInfo.album, + url: songInfo.url ?? '', + }); }); }, }, diff --git a/src/providers/song-info.ts b/src/providers/song-info.ts index 7d1a94fa..8a69f1f4 100644 --- a/src/providers/song-info.ts +++ b/src/providers/song-info.ts @@ -149,8 +149,17 @@ const handleData = async ( return songInfo; }; +export enum SongInfoEvent { + VideoSrcChanged = 'ytmd:video-src-changed', + PlayOrPaused = 'ytmd:play-or-paused', + TimeChanged = 'ytmd:time-changed', +} + // This variable will be filled with the callbacks once they register -export type SongInfoCallback = (songInfo: SongInfo, event?: string) => void; +export type SongInfoCallback = ( + songInfo: SongInfo, + event: SongInfoEvent, +) => void; const callbacks: Set = new Set(); // This function will allow plugins to register callback that will be triggered when data changes @@ -173,7 +182,7 @@ const registerProvider = (win: BrowserWindow) => { if (tempSongInfo) { for (const c of callbacks) { - c(tempSongInfo, 'ytmd:video-src-changed'); + c(tempSongInfo, SongInfoEvent.VideoSrcChanged); } } }); @@ -199,11 +208,29 @@ const registerProvider = (win: BrowserWindow) => { if (tempSongInfo) { for (const c of callbacks) { - c(tempSongInfo, 'ytmd:play-or-paused'); + c(tempSongInfo, SongInfoEvent.PlayOrPaused); } } }, ); + + ipcMain.on('ytmd:time-changed', async (_, seconds: number) => { + const tempSongInfo = await dataMutex.runExclusive(() => { + if (!songInfo) { + return null; + } + + songInfo.elapsedSeconds = seconds; + + return songInfo; + }); + + if (tempSongInfo) { + for (const c of callbacks) { + c(tempSongInfo, SongInfoEvent.TimeChanged); + } + } + }); }; const suffixesToRemove = [ diff --git a/src/tray.ts b/src/tray.ts index 2f609bc1..6059d0e0 100644 --- a/src/tray.ts +++ b/src/tray.ts @@ -1,4 +1,4 @@ -import { Menu, screen, nativeImage, Tray } from 'electron'; +import { Menu, nativeImage, screen, Tray } from 'electron'; import is from 'electron-is'; import defaultTrayIconAsset from '@assets/youtube-music-tray.png?asset&asarUnpack'; @@ -7,7 +7,7 @@ import pausedTrayIconAsset from '@assets/youtube-music-tray-paused.png?asset&asa import config from './config'; import { restart } from './providers/app-controls'; -import registerCallback from './providers/song-info'; +import registerCallback, { SongInfoEvent } from './providers/song-info'; import getSongControls from './providers/song-controls'; import { t } from '@/i18n'; @@ -125,7 +125,9 @@ export const setUpTray = (app: Electron.App, win: Electron.BrowserWindow) => { const trayMenu = Menu.buildFromTemplate(template); tray.setContextMenu(trayMenu); - registerCallback((songInfo) => { + registerCallback((songInfo, event) => { + if (event === SongInfoEvent.TimeChanged) return; + if (tray) { if (typeof songInfo.isPaused === 'undefined') { tray.setImage(defaultTrayIcon);