Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix loadeddata/metadata video events rarely not firing (+other small fixes) #477

Merged
13 changes: 10 additions & 3 deletions plugins/disable-autoplay/front.js
Original file line number Diff line number Diff line change
@@ -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 })
};
20 changes: 9 additions & 11 deletions plugins/downloader/menu.js
Original file line number Diff line number Diff line change
@@ -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;
}
Expand All @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion plugins/playback-speed/front.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const observePopupContainer = () => {

const observeVideo = () => {
$('video').addEventListener('ratechange', forcePlaybackRate)
$('video').addEventListener('loadeddata', forcePlaybackRate)
$('video').addEventListener('srcChanged', forcePlaybackRate)
}

const setupWheelListener = () => {
Expand Down
15 changes: 5 additions & 10 deletions plugins/sponsorblock/back.js
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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);
});
};

Expand Down
44 changes: 31 additions & 13 deletions plugins/video-toggle/front.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ function $(selector) { return document.querySelector(selector); }

let options;

let api;

const switchButtonDiv = ElementFromFile(
templatePath(__dirname, "button_template.html")
);
Expand All @@ -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"
Expand All @@ -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");
}

Expand All @@ -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);
Expand All @@ -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() {
Expand All @@ -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];
}
}
36 changes: 30 additions & 6 deletions providers/song-info-front.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
};
102 changes: 50 additions & 52 deletions providers/song-info.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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 = [
Expand All @@ -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);
Expand Down