Merge pull request #477 from Araxeus/fix-loadeddata/metdata-events-rarely-not-firing

Fix loadeddata/metadata video events rarely not firing (+other small fixes)
This commit is contained in:
th-ch
2021-11-29 23:52:23 +01:00
committed by GitHub
7 changed files with 137 additions and 97 deletions

View File

@ -1,7 +1,14 @@
module.exports = () => { module.exports = () => {
document.addEventListener('apiLoaded', () => { document.addEventListener('apiLoaded', apiEvent => {
document.querySelector('video').addEventListener('loadeddata', e => { apiEvent.detail.addEventListener('videodatachange', name => {
e.target.pause(); if (name === 'dataloaded') {
apiEvent.detail.pauseVideo();
document.querySelector('video').ontimeupdate = e => {
e.target.pause();
}
} else {
document.querySelector('video').ontimeupdate = null;
}
}) })
}, { once: true, passive: true }) }, { once: true, passive: true })
}; };

View File

@ -1,25 +1,23 @@
const { existsSync, mkdirSync } = require("fs"); const { existsSync, mkdirSync } = require("fs");
const { join } = require("path"); const { join } = require("path");
const { URL } = require("url");
const { dialog } = require("electron"); const { dialog, ipcMain } = require("electron");
const is = require("electron-is"); const is = require("electron-is");
const ytpl = require("ytpl"); const ytpl = require("ytpl");
const chokidar = require('chokidar'); const chokidar = require('chokidar');
const { setOptions } = require("../../config/plugins"); const { setOptions } = require("../../config/plugins");
const registerCallback = require("../../providers/song-info");
const { sendError } = require("./back"); const { sendError } = require("./back");
const { defaultMenuDownloadLabel, getFolder } = require("./utils"); const { defaultMenuDownloadLabel, getFolder } = require("./utils");
let downloadLabel = defaultMenuDownloadLabel; let downloadLabel = defaultMenuDownloadLabel;
let metadataURL = undefined; let playingPlaylistId = undefined;
let callbackIsRegistered = false; let callbackIsRegistered = false;
module.exports = (win, options) => { module.exports = (win, options) => {
if (!callbackIsRegistered) { if (!callbackIsRegistered) {
registerCallback((info) => { ipcMain.on("video-src-changed", async (_, data) => {
metadataURL = info.url; playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId;
}); });
callbackIsRegistered = true; callbackIsRegistered = true;
} }
@ -28,17 +26,17 @@ module.exports = (win, options) => {
{ {
label: downloadLabel, label: downloadLabel,
click: async () => { click: async () => {
const currentURL = metadataURL || win.webContents.getURL(); const currentPagePlaylistId = new URL(win.webContents.getURL()).searchParams.get("list");
const playlistID = new URL(currentURL).searchParams.get("list"); const playlistId = currentPagePlaylistId || playingPlaylistId;
if (!playlistID) { if (!playlistId) {
sendError(win, new Error("No playlist ID found")); sendError(win, new Error("No playlist ID found"));
return; return;
} }
console.log(`trying to get playlist ID: '${playlistID}'`); console.log(`trying to get playlist ID: '${playlistId}'`);
let playlist; let playlist;
try { try {
playlist = await ytpl(playlistID, { playlist = await ytpl(playlistId, {
limit: options.playlistMaxItems || Infinity, limit: options.playlistMaxItems || Infinity,
}); });
} catch (e) { } catch (e) {

View File

@ -49,7 +49,7 @@ const observePopupContainer = () => {
const observeVideo = () => { const observeVideo = () => {
$('video').addEventListener('ratechange', forcePlaybackRate) $('video').addEventListener('ratechange', forcePlaybackRate)
$('video').addEventListener('loadeddata', forcePlaybackRate) $('video').addEventListener('srcChanged', forcePlaybackRate)
} }
const setupWheelListener = () => { const setupWheelListener = () => {

View File

@ -1,8 +1,8 @@
const fetch = require("node-fetch"); const fetch = require("node-fetch");
const is = require("electron-is"); const is = require("electron-is");
const { ipcMain } = require("electron");
const defaultConfig = require("../../config/defaults"); const defaultConfig = require("../../config/defaults");
const registerCallback = require("../../providers/song-info");
const { sortSegments } = require("./segments"); const { sortSegments } = require("./segments");
let videoID; let videoID;
@ -13,15 +13,10 @@ module.exports = (win, options) => {
...options, ...options,
}; };
registerCallback(async (info) => { ipcMain.on("video-src-changed", async (_, data) => {
const newURL = info.url || win.webContents.getURL(); videoID = JSON.parse(data)?.videoDetails?.videoId;
const newVideoID = new URL(newURL).searchParams.get("v"); const segments = await fetchSegments(apiURL, categories);
win.webContents.send("sponsorblock-skip", segments);
if (videoID !== newVideoID) {
videoID = newVideoID;
const segments = await fetchSegments(apiURL, categories);
win.webContents.send("sponsorblock-skip", segments);
}
}); });
}; };

View File

@ -6,6 +6,8 @@ function $(selector) { return document.querySelector(selector); }
let options; let options;
let api;
const switchButtonDiv = ElementFromFile( const switchButtonDiv = ElementFromFile(
templatePath(__dirname, "button_template.html") templatePath(__dirname, "button_template.html")
); );
@ -17,7 +19,9 @@ module.exports = (_options) => {
document.addEventListener('apiLoaded', setup, { once: true, passive: true }); document.addEventListener('apiLoaded', setup, { once: true, passive: true });
} }
function setup() { function setup(e) {
api = e.detail;
$('ytmusic-player-page').prepend(switchButtonDiv); $('ytmusic-player-page').prepend(switchButtonDiv);
$('#song-image.ytmusic-player').style.display = "block" $('#song-image.ytmusic-player').style.display = "block"
@ -35,13 +39,15 @@ function setup() {
setOptions("video-toggle", options); setOptions("video-toggle", options);
}) })
$('video').addEventListener('loadedmetadata', videoStarted); $('video').addEventListener('srcChanged', videoStarted);
observeThumbnail();
} }
function changeDisplay(showVideo) { function changeDisplay(showVideo) {
if (!showVideo && $('ytmusic-player').getAttribute('playback-mode') !== "ATV_PREFERRED") { if (!showVideo) {
$('video').style.top = "0"; $('video').style.top = "0";
$('ytmusic-player').style.margin = "auto 21.5px"; $('ytmusic-player').style.margin = "auto 0px";
$('ytmusic-player').setAttribute('playback-mode', "ATV_PREFERRED"); $('ytmusic-player').setAttribute('playback-mode', "ATV_PREFERRED");
} }
@ -51,11 +57,8 @@ function changeDisplay(showVideo) {
} }
function videoStarted() { function videoStarted() {
if (videoExist()) { if (api.getPlayerResponse().videoDetails.musicVideoType === 'MUSIC_VIDEO_TYPE_OMV') { // or `$('#player').videoMode_`
const thumbnails = $('#movie_player').getPlayerResponse()?.videoDetails?.thumbnail?.thumbnails; forceThumbnail($('#song-image img'));
if (thumbnails && thumbnails.length > 0) {
$('#song-image img').src = thumbnails[thumbnails.length-1].url;
}
switchButtonDiv.style.display = "initial"; switchButtonDiv.style.display = "initial";
if (!options.hideVideo && $('#song-video.ytmusic-player').style.display === "none") { if (!options.hideVideo && $('#song-video.ytmusic-player').style.display === "none") {
changeDisplay(true); 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 // 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 :) // this function fix the problem by overriding that override :)
function forcePlaybackMode() { function forcePlaybackMode() {
@ -83,3 +82,22 @@ function forcePlaybackMode() {
}); });
playbackModeObserver.observe($('ytmusic-player'), { attributeFilter: ["playback-mode"] }) 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];
}
}

View File

@ -9,11 +9,35 @@ ipcRenderer.on("update-song-info", async (_, extractedSongInfo) => {
global.songInfo.image = await getImage(global.songInfo.imageSrc); 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 = () => { module.exports = () => {
document.addEventListener('apiLoaded', e => { document.addEventListener('apiLoaded', apiEvent => {
document.querySelector('video').addEventListener('loadedmetadata', () => { const video = document.querySelector('video');
const data = e.detail.getPlayerResponse(); // name = "dataloaded" and abit later "dataupdated"
ipcRenderer.send("song-info-request", JSON.stringify(data)); apiEvent.detail.addEventListener('videodatachange', (name, _dataEvent) => {
}); if (name !== 'dataloaded') return;
}, { once: true, passive: true }) 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 });
}; };

View File

@ -4,32 +4,6 @@ const fetch = require("node-fetch");
const config = require("../config"); 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 // Fill songInfo with empty values
/** /**
* @typedef {songInfo} SongInfo * @typedef {songInfo} SongInfo
@ -45,23 +19,53 @@ const songInfo = {
songDuration: 0, songDuration: 0,
elapsedSeconds: 0, elapsedSeconds: 0,
url: "", 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) => { const handleData = async (responseText, win) => {
let data = JSON.parse(responseText); const data = JSON.parse(responseText);
songInfo.title = cleanupName(data?.videoDetails?.title); if (!data) return;
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];
// used for options.resumeOnStart const microformat = data.microformat?.microformatDataRenderer;
config.set("url", data?.microformat?.microformatDataRenderer?.urlCanonical); 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);
}
win.webContents.send("update-song-info", JSON.stringify(songInfo)); 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));
}
}; };
// This variable will be filled with the callbacks once they register // This variable will be filled with the callbacks once they register
@ -81,26 +85,20 @@ const registerCallback = (callback) => {
}; };
const registerProvider = (win) => { 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
callbacks.forEach((c) => {
c(songInfo);
});
});
// This will be called when the song-info-front finds a new request with song data // This will be called when the song-info-front finds a new request with song data
ipcMain.on("song-info-request", async (_, responseText) => { ipcMain.on("video-src-changed", async (_, responseText) => {
await handleData(responseText, win); await handleData(responseText, win);
callbacks.forEach((c) => { callbacks.forEach((c) => {
c(songInfo); c(songInfo);
}); });
}); });
ipcMain.on("playPaused", (_, { isPaused, elapsedSeconds }) => {
songInfo.isPaused = isPaused;
songInfo.elapsedSeconds = elapsedSeconds;
callbacks.forEach((c) => {
c(songInfo);
});
})
}; };
const suffixesToRemove = [ const suffixesToRemove = [
@ -114,7 +112,7 @@ const suffixesToRemove = [
function cleanupName(name) { function cleanupName(name) {
if (!name) return name; if (!name) return name;
const lowCaseName = name.toLowerCase(); const lowCaseName = name.toLowerCase();
for (const suffix of suffixesToRemove) { for (const suffix of suffixesToRemove) {
if (lowCaseName.endsWith(suffix)) { if (lowCaseName.endsWith(suffix)) {
return name.slice(0, -suffix.length); return name.slice(0, -suffix.length);