diff --git a/plugins/disable-autoplay/front.js b/plugins/disable-autoplay/front.js index 532405de25..c34a453a66 100644 --- a/plugins/disable-autoplay/front.js +++ b/plugins/disable-autoplay/front.js @@ -1,7 +1,14 @@ module.exports = () => { - document.addEventListener('apiLoaded', () => { - document.querySelector('video').addEventListener('loadeddata', e => { - e.target.pause(); + document.addEventListener('apiLoaded', apiEvent => { + apiEvent.detail.addEventListener('videodatachange', name => { + if (name === 'dataloaded') { + apiEvent.detail.pauseVideo(); + document.querySelector('video').ontimeupdate = e => { + e.target.pause(); + } + } else { + document.querySelector('video').ontimeupdate = null; + } }) }, { once: true, passive: true }) }; diff --git a/plugins/downloader/menu.js b/plugins/downloader/menu.js index ce7f56435b..9ec83d4caf 100644 --- a/plugins/downloader/menu.js +++ b/plugins/downloader/menu.js @@ -1,25 +1,23 @@ const { existsSync, mkdirSync } = require("fs"); const { join } = require("path"); -const { URL } = require("url"); -const { dialog } = require("electron"); +const { dialog, ipcMain } = require("electron"); const is = require("electron-is"); const ytpl = require("ytpl"); const chokidar = require('chokidar'); const { setOptions } = require("../../config/plugins"); -const registerCallback = require("../../providers/song-info"); const { sendError } = require("./back"); const { defaultMenuDownloadLabel, getFolder } = require("./utils"); let downloadLabel = defaultMenuDownloadLabel; -let metadataURL = undefined; +let playingPlaylistId = undefined; let callbackIsRegistered = false; module.exports = (win, options) => { if (!callbackIsRegistered) { - registerCallback((info) => { - metadataURL = info.url; + ipcMain.on("video-src-changed", async (_, data) => { + playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId; }); callbackIsRegistered = true; } @@ -28,17 +26,17 @@ module.exports = (win, options) => { { label: downloadLabel, click: async () => { - const currentURL = metadataURL || win.webContents.getURL(); - const playlistID = new URL(currentURL).searchParams.get("list"); - if (!playlistID) { + const currentPagePlaylistId = new URL(win.webContents.getURL()).searchParams.get("list"); + const playlistId = currentPagePlaylistId || playingPlaylistId; + if (!playlistId) { sendError(win, new Error("No playlist ID found")); return; } - console.log(`trying to get playlist ID: '${playlistID}'`); + console.log(`trying to get playlist ID: '${playlistId}'`); let playlist; try { - playlist = await ytpl(playlistID, { + playlist = await ytpl(playlistId, { limit: options.playlistMaxItems || Infinity, }); } catch (e) { diff --git a/plugins/playback-speed/front.js b/plugins/playback-speed/front.js index f1ea78e824..c5e430640e 100644 --- a/plugins/playback-speed/front.js +++ b/plugins/playback-speed/front.js @@ -49,7 +49,7 @@ const observePopupContainer = () => { const observeVideo = () => { $('video').addEventListener('ratechange', forcePlaybackRate) - $('video').addEventListener('loadeddata', forcePlaybackRate) + $('video').addEventListener('srcChanged', forcePlaybackRate) } const setupWheelListener = () => { diff --git a/plugins/sponsorblock/back.js b/plugins/sponsorblock/back.js index 0f16f1023f..04667b85af 100644 --- a/plugins/sponsorblock/back.js +++ b/plugins/sponsorblock/back.js @@ -1,8 +1,8 @@ const fetch = require("node-fetch"); const is = require("electron-is"); +const { ipcMain } = require("electron"); const defaultConfig = require("../../config/defaults"); -const registerCallback = require("../../providers/song-info"); const { sortSegments } = require("./segments"); let videoID; @@ -13,15 +13,10 @@ module.exports = (win, options) => { ...options, }; - registerCallback(async (info) => { - const newURL = info.url || win.webContents.getURL(); - const newVideoID = new URL(newURL).searchParams.get("v"); - - if (videoID !== newVideoID) { - videoID = newVideoID; - const segments = await fetchSegments(apiURL, categories); - win.webContents.send("sponsorblock-skip", segments); - } + ipcMain.on("video-src-changed", async (_, data) => { + videoID = JSON.parse(data)?.videoDetails?.videoId; + const segments = await fetchSegments(apiURL, categories); + win.webContents.send("sponsorblock-skip", segments); }); }; diff --git a/plugins/video-toggle/front.js b/plugins/video-toggle/front.js index 588642a0bf..d550a37575 100644 --- a/plugins/video-toggle/front.js +++ b/plugins/video-toggle/front.js @@ -6,6 +6,8 @@ function $(selector) { return document.querySelector(selector); } let options; +let api; + const switchButtonDiv = ElementFromFile( templatePath(__dirname, "button_template.html") ); @@ -17,7 +19,9 @@ module.exports = (_options) => { document.addEventListener('apiLoaded', setup, { once: true, passive: true }); } -function setup() { +function setup(e) { + api = e.detail; + $('ytmusic-player-page').prepend(switchButtonDiv); $('#song-image.ytmusic-player').style.display = "block" @@ -35,13 +39,15 @@ function setup() { setOptions("video-toggle", options); }) - $('video').addEventListener('loadedmetadata', videoStarted); + $('video').addEventListener('srcChanged', videoStarted); + + observeThumbnail(); } function changeDisplay(showVideo) { - if (!showVideo && $('ytmusic-player').getAttribute('playback-mode') !== "ATV_PREFERRED") { + if (!showVideo) { $('video').style.top = "0"; - $('ytmusic-player').style.margin = "auto 21.5px"; + $('ytmusic-player').style.margin = "auto 0px"; $('ytmusic-player').setAttribute('playback-mode', "ATV_PREFERRED"); } @@ -51,11 +57,8 @@ function changeDisplay(showVideo) { } function videoStarted() { - if (videoExist()) { - const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails; - if (thumbnails && thumbnails.length > 0) { - $('#song-image img').src = thumbnails[thumbnails.length-1].url; - } + if (api.getPlayerResponse().videoDetails.musicVideoType === 'MUSIC_VIDEO_TYPE_OMV') { // or `$('#player').videoMode_` + forceThumbnail($('#song-image img')); switchButtonDiv.style.display = "initial"; if (!options.hideVideo && $('#song-video.ytmusic-player').style.display === "none") { changeDisplay(true); @@ -66,10 +69,6 @@ function videoStarted() { } } -function videoExist() { - return $('#player').videoMode_; -} - // on load, after a delay, the page overrides the playback-mode to 'OMV_PREFERRED' which causes weird aspect ratio in the image container // this function fix the problem by overriding that override :) function forcePlaybackMode() { @@ -83,3 +82,22 @@ function forcePlaybackMode() { }); playbackModeObserver.observe($('ytmusic-player'), { attributeFilter: ["playback-mode"] }) } + +function observeThumbnail() { + const playbackModeObserver = new MutationObserver(mutations => { + if (!$('#player').videoMode_) return; + + mutations.forEach(mutation => { + if (!mutation.target.src.startsWith('data:')) return; + forceThumbnail(mutation.target) + }); + }); + playbackModeObserver.observe($('#song-image img'), { attributeFilter: ["src"] }) +} + +function forceThumbnail(img) { + const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails; + if (thumbnails && thumbnails.length > 0) { + img.src = thumbnails[thumbnails.length - 1].url.split("?")[0]; + } +} diff --git a/providers/song-info-front.js b/providers/song-info-front.js index 4ff6041d24..acd41daf93 100644 --- a/providers/song-info-front.js +++ b/providers/song-info-front.js @@ -9,11 +9,35 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => { global.songInfo.image = await getImage(global.songInfo.imageSrc); }); +// used because 'loadeddata' or 'loadedmetadata' weren't firing on song start for some users (https://github.com/th-ch/youtube-music/issues/473) +const srcChangedEvent = new CustomEvent('srcChanged'); + module.exports = () => { - document.addEventListener('apiLoaded', e => { - document.querySelector('video').addEventListener('loadedmetadata', () => { - const data = e.detail.getPlayerResponse(); - ipcRenderer.send("song-info-request", JSON.stringify(data)); - }); - }, { once: true, passive: true }) + document.addEventListener('apiLoaded', apiEvent => { + const video = document.querySelector('video'); + // name = "dataloaded" and abit later "dataupdated" + apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => { + if (name !== 'dataloaded') return; + video.dispatchEvent(srcChangedEvent); + sendSongInfo(); + }) + + for (const status of ['playing', 'pause']) { + video.addEventListener(status, e => { + if (Math.floor(e.target.currentTime) > 0) { + ipcRenderer.send("playPaused", { + isPaused: status === 'pause', + elapsedSeconds: Math.floor(e.target.currentTime) + }); + } + }); + } + + function sendSongInfo() { + const data = apiEvent.detail.getPlayerResponse(); + data.videoDetails.elapsedSeconds = Math.floor(video.currentTime); + data.videoDetails.isPaused = false; + ipcRenderer.send("video-src-changed", JSON.stringify(data)); + } + }, { once: true, passive: true }); }; diff --git a/providers/song-info.js b/providers/song-info.js index 37cd55fa28..ddeee4cd15 100644 --- a/providers/song-info.js +++ b/providers/song-info.js @@ -4,32 +4,6 @@ const fetch = require("node-fetch"); const config = require("../config"); -// Grab the progress using the selector -const getProgress = async (win) => { - // Get current value of the progressbar element - return win.webContents.executeJavaScript( - 'document.querySelector("#progress-bar").value' - ); -}; - -// Grab the native image using the src -const getImage = async (src) => { - const result = await fetch(src); - const buffer = await result.buffer(); - const output = nativeImage.createFromBuffer(buffer); - if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315) - return getImage(src.slice(0, src.lastIndexOf(".jpg")+4)); - } else { - return output; - } -}; - -// To find the paused status, we check if the title contains `-` -const getPausedStatus = async (win) => { - const title = await win.webContents.executeJavaScript("document.title"); - return !title.includes("-"); -}; - // Fill songInfo with empty values /** * @typedef {songInfo} SongInfo @@ -45,23 +19,53 @@ const songInfo = { songDuration: 0, elapsedSeconds: 0, url: "", + videoId: "", + playlistId: "", +}; + +// Grab the native image using the src +const getImage = async (src) => { + const result = await fetch(src); + const buffer = await result.buffer(); + const output = nativeImage.createFromBuffer(buffer); + if (output.isEmpty() && !src.endsWith(".jpg") && src.includes(".jpg")) { // fix hidden webp files (https://github.com/th-ch/youtube-music/issues/315) + return getImage(src.slice(0, src.lastIndexOf(".jpg") + 4)); + } else { + return output; + } }; const handleData = async (responseText, win) => { - let data = JSON.parse(responseText); - songInfo.title = cleanupName(data?.videoDetails?.title); - songInfo.artist =cleanupName(data?.videoDetails?.author); - songInfo.views = data?.videoDetails?.viewCount; - songInfo.imageSrc = data?.videoDetails?.thumbnail?.thumbnails?.pop()?.url; - songInfo.songDuration = data?.videoDetails?.lengthSeconds; - songInfo.image = await getImage(songInfo.imageSrc); - songInfo.uploadDate = data?.microformat?.microformatDataRenderer?.uploadDate; - songInfo.url = data?.microformat?.microformatDataRenderer?.urlCanonical?.split("&")[0]; + const data = JSON.parse(responseText); + if (!data) return; + + const microformat = data.microformat?.microformatDataRenderer; + if (microformat) { + songInfo.uploadDate = microformat.uploadDate; + songInfo.url = microformat.urlCanonical?.split("&")[0]; + songInfo.playlistId = new URL(microformat.urlCanonical).searchParams.get("list"); + // used for options.resumeOnStart + config.set("url", microformat.urlCanonical); + } - // used for options.resumeOnStart - config.set("url", data?.microformat?.microformatDataRenderer?.urlCanonical); + const videoDetails = data.videoDetails; + if (videoDetails) { + songInfo.title = cleanupName(videoDetails.title); + songInfo.artist = cleanupName(videoDetails.author); + songInfo.views = videoDetails.viewCount; + songInfo.songDuration = videoDetails.lengthSeconds; + songInfo.elapsedSeconds = videoDetails.elapsedSeconds; + songInfo.isPaused = videoDetails.isPaused; + songInfo.videoId = videoDetails.videoId; + + const oldUrl = songInfo.imageSrc; + songInfo.imageSrc = videoDetails.thumbnail?.thumbnails?.pop()?.url.split("?")[0]; + if (oldUrl !== songInfo.imageSrc) { + songInfo.image = await getImage(songInfo.imageSrc); + } - win.webContents.send("update-song-info", JSON.stringify(songInfo)); + win.webContents.send("update-song-info", JSON.stringify(songInfo)); + } }; // This variable will be filled with the callbacks once they register @@ -81,26 +85,20 @@ const registerCallback = (callback) => { }; const registerProvider = (win) => { - win.on("page-title-updated", async () => { - // Get and set the new data - songInfo.isPaused = await getPausedStatus(win); - - const elapsedSeconds = await getProgress(win); - songInfo.elapsedSeconds = elapsedSeconds; - - // Trigger the callbacks + // This will be called when the song-info-front finds a new request with song data + ipcMain.on("video-src-changed", async (_, responseText) => { + await handleData(responseText, win); callbacks.forEach((c) => { c(songInfo); }); }); - - // This will be called when the song-info-front finds a new request with song data - ipcMain.on("song-info-request", async (_, responseText) => { - await handleData(responseText, win); + ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => { + songInfo.isPaused = isPaused; + songInfo.elapsedSeconds = elapsedSeconds; callbacks.forEach((c) => { c(songInfo); }); - }); + }) }; const suffixesToRemove = [ @@ -114,7 +112,7 @@ const suffixesToRemove = [ function cleanupName(name) { if (!name) return name; - const lowCaseName = name.toLowerCase(); + const lowCaseName = name.toLowerCase(); for (const suffix of suffixesToRemove) { if (lowCaseName.endsWith(suffix)) { return name.slice(0, -suffix.length);