mirror of
https://github.com/th-ch/youtube-music.git
synced 2026-01-16 12:42:06 +00:00
Merge branch 'master' into download-playlist-badge-count
This commit is contained in:
@ -95,7 +95,7 @@
|
|||||||
"mpris-service": "^2.1.2",
|
"mpris-service": "^2.1.2",
|
||||||
"node-fetch": "^2.6.6",
|
"node-fetch": "^2.6.6",
|
||||||
"node-notifier": "^9.0.1",
|
"node-notifier": "^9.0.1",
|
||||||
"ytdl-core": "^4.9.2",
|
"ytdl-core": "^4.10.0",
|
||||||
"ytpl": "^2.2.3"
|
"ytpl": "^2.2.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
const { contextBridge } = require("electron");
|
const { ipcRenderer } = require("electron");
|
||||||
|
|
||||||
const { defaultConfig } = require("../../config");
|
const { defaultConfig } = require("../../config");
|
||||||
const { getSongMenu } = require("../../providers/dom-elements");
|
const { getSongMenu } = require("../../providers/dom-elements");
|
||||||
@ -13,15 +13,17 @@ const downloadButton = ElementFromFile(
|
|||||||
);
|
);
|
||||||
let pluginOptions = {};
|
let pluginOptions = {};
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations, observer) => {
|
const observer = new MutationObserver(() => {
|
||||||
if (!menu) {
|
if (!menu) {
|
||||||
menu = getSongMenu();
|
menu = getSongMenu();
|
||||||
|
if (!menu) return;
|
||||||
}
|
}
|
||||||
|
if (menu.contains(downloadButton)) return;
|
||||||
|
const menuUrl = document.querySelector('tp-yt-paper-listbox [tabindex="0"] #navigation-endpoint')?.href;
|
||||||
|
if (menuUrl && !menuUrl.includes('watch?')) return;
|
||||||
|
|
||||||
if (menu && !menu.contains(downloadButton)) {
|
menu.prepend(downloadButton);
|
||||||
menu.prepend(downloadButton);
|
progress = document.querySelector("#ytmcustom-download");
|
||||||
progress = document.querySelector("#ytmcustom-download");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const reinit = () => {
|
const reinit = () => {
|
||||||
@ -43,10 +45,16 @@ global.download = () => {
|
|||||||
let metadata;
|
let metadata;
|
||||||
let videoUrl = getSongMenu()
|
let videoUrl = getSongMenu()
|
||||||
// selector of first button which is always "Start Radio"
|
// selector of first button which is always "Start Radio"
|
||||||
?.querySelector('ytmusic-menu-navigation-item-renderer.iron-selected[tabindex="0"] #navigation-endpoint')
|
?.querySelector('ytmusic-menu-navigation-item-renderer[tabindex="0"] #navigation-endpoint')
|
||||||
?.getAttribute("href");
|
?.getAttribute("href");
|
||||||
if (videoUrl) {
|
if (videoUrl) {
|
||||||
videoUrl = baseUrl + "/" + videoUrl;
|
if (videoUrl.startsWith('watch?')) {
|
||||||
|
videoUrl = baseUrl + "/" + videoUrl;
|
||||||
|
}
|
||||||
|
if (videoUrl.includes('?playlist=')) {
|
||||||
|
ipcRenderer.send('download-playlist-request', videoUrl);
|
||||||
|
return;
|
||||||
|
}
|
||||||
metadata = null;
|
metadata = null;
|
||||||
} else {
|
} else {
|
||||||
metadata = global.songInfo;
|
metadata = global.songInfo;
|
||||||
@ -78,10 +86,13 @@ global.download = () => {
|
|||||||
|
|
||||||
function observeMenu(options) {
|
function observeMenu(options) {
|
||||||
pluginOptions = { ...pluginOptions, ...options };
|
pluginOptions = { ...pluginOptions, ...options };
|
||||||
observer.observe(document, {
|
|
||||||
childList: true,
|
document.addEventListener('apiLoaded', () => {
|
||||||
subtree: true,
|
observer.observe(document.querySelector('ytmusic-popup-container'), {
|
||||||
});
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}, { once: true, passive: true })
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = observeMenu;
|
module.exports = observeMenu;
|
||||||
|
|||||||
@ -11,92 +11,33 @@ const { sendError } = require("./back");
|
|||||||
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
const { defaultMenuDownloadLabel, getFolder, presets, setBadge } = require("./utils");
|
||||||
|
|
||||||
let downloadLabel = defaultMenuDownloadLabel;
|
let downloadLabel = defaultMenuDownloadLabel;
|
||||||
let playingPlaylistId = undefined;
|
let playingUrl = undefined;
|
||||||
let callbackIsRegistered = false;
|
let callbackIsRegistered = false;
|
||||||
|
|
||||||
|
// Playlist radio modifier needs to be cut from playlist ID
|
||||||
|
const INVALID_PLAYLIST_MODIFIER = 'RDAMPL';
|
||||||
|
|
||||||
|
const getPlaylistID = aURL => {
|
||||||
|
const result = aURL?.searchParams.get("list") || aURL?.searchParams.get("playlist");
|
||||||
|
if (result?.startsWith(INVALID_PLAYLIST_MODIFIER)) {
|
||||||
|
return result.slice(6)
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
module.exports = (win, options) => {
|
module.exports = (win, options) => {
|
||||||
if (!callbackIsRegistered) {
|
if (!callbackIsRegistered) {
|
||||||
ipcMain.on("video-src-changed", async (_, data) => {
|
ipcMain.on("video-src-changed", async (_, data) => {
|
||||||
playingPlaylistId = JSON.parse(data)?.videoDetails?.playlistId;
|
playingUrl = JSON.parse(data)?.microformat?.microformatDataRenderer?.urlCanonical;
|
||||||
});
|
});
|
||||||
|
ipcMain.on("download-playlist-request", async (_event, url) => downloadPlaylist(url, win, options));
|
||||||
callbackIsRegistered = true;
|
callbackIsRegistered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
label: downloadLabel,
|
label: downloadLabel,
|
||||||
click: async () => {
|
click: () => downloadPlaylist(undefined, win, options),
|
||||||
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}'`);
|
|
||||||
let playlist;
|
|
||||||
try {
|
|
||||||
playlist = await ytpl(playlistId, {
|
|
||||||
limit: options.playlistMaxItems || Infinity,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
sendError(win, e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const playlistTitle = playlist.title;
|
|
||||||
|
|
||||||
const folder = getFolder(options.downloadFolder);
|
|
||||||
const playlistFolder = join(folder, playlistTitle);
|
|
||||||
if (existsSync(playlistFolder)) {
|
|
||||||
sendError(
|
|
||||||
win,
|
|
||||||
new Error(`The folder ${playlistFolder} already exists`)
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
mkdirSync(playlistFolder, { recursive: true });
|
|
||||||
|
|
||||||
dialog.showMessageBox({
|
|
||||||
type: "info",
|
|
||||||
buttons: ["OK"],
|
|
||||||
title: "Started Download",
|
|
||||||
message: `Downloading Playlist "${playlistTitle}"`,
|
|
||||||
detail: `(${playlist.items.length} songs)`,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (is.dev()) {
|
|
||||||
console.log(
|
|
||||||
`Downloading playlist "${playlistTitle}" (${playlist.items.length} songs)`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
win.setProgressBar(2); // starts with indefinite bar
|
|
||||||
|
|
||||||
let downloadCount = 0;
|
|
||||||
setBadge(playlist.items.length);
|
|
||||||
|
|
||||||
let dirWatcher = chokidar.watch(playlistFolder);
|
|
||||||
dirWatcher.on('add', () => {
|
|
||||||
downloadCount++;
|
|
||||||
if (downloadCount >= playlist.items.length) {
|
|
||||||
win.setProgressBar(-1); // close progress bar
|
|
||||||
setBadge(0); // close badge counter
|
|
||||||
dirWatcher.close().then(() => dirWatcher = null);
|
|
||||||
} else {
|
|
||||||
win.setProgressBar(downloadCount / playlist.items.length);
|
|
||||||
setBadge(playlist.items.length - downloadCount);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
playlist.items.forEach((song) => {
|
|
||||||
win.webContents.send(
|
|
||||||
"downloader-download-playlist",
|
|
||||||
song.url,
|
|
||||||
playlistTitle,
|
|
||||||
options
|
|
||||||
);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: "Choose download folder",
|
label: "Choose download folder",
|
||||||
@ -125,3 +66,83 @@ module.exports = (win, options) => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
async function downloadPlaylist(givenUrl, win, options) {
|
||||||
|
if (givenUrl) {
|
||||||
|
try {
|
||||||
|
givenUrl = new URL(givenUrl);
|
||||||
|
} catch {
|
||||||
|
givenUrl = undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const playlistId = getPlaylistID(givenUrl)
|
||||||
|
|| getPlaylistID(new URL(win.webContents.getURL()))
|
||||||
|
|| getPlaylistID(new URL(playingUrl));
|
||||||
|
|
||||||
|
if (!playlistId) {
|
||||||
|
sendError(win, new Error("No playlist ID found"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`trying to get playlist ID: '${playlistId}'`);
|
||||||
|
let playlist;
|
||||||
|
try {
|
||||||
|
playlist = await ytpl(playlistId, {
|
||||||
|
limit: options.playlistMaxItems || Infinity,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
sendError(win, e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const playlistTitle = playlist.title;
|
||||||
|
|
||||||
|
const folder = getFolder(options.downloadFolder);
|
||||||
|
const playlistFolder = join(folder, playlistTitle);
|
||||||
|
if (existsSync(playlistFolder)) {
|
||||||
|
sendError(
|
||||||
|
win,
|
||||||
|
new Error(`The folder ${playlistFolder} already exists`)
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mkdirSync(playlistFolder, { recursive: true });
|
||||||
|
|
||||||
|
dialog.showMessageBox({
|
||||||
|
type: "info",
|
||||||
|
buttons: ["OK"],
|
||||||
|
title: "Started Download",
|
||||||
|
message: `Downloading Playlist "${playlistTitle}"`,
|
||||||
|
detail: `(${playlist.items.length} songs)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (is.dev()) {
|
||||||
|
console.log(
|
||||||
|
`Downloading playlist "${playlistTitle}" - ${playlist.items.length} songs (${playlistId})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const steps = 1 / playlist.items.length;
|
||||||
|
let progress = 0;
|
||||||
|
|
||||||
|
win.setProgressBar(2); // starts with indefinite bar
|
||||||
|
|
||||||
|
let dirWatcher = chokidar.watch(playlistFolder);
|
||||||
|
dirWatcher.on('add', () => {
|
||||||
|
progress += steps;
|
||||||
|
if (progress >= 0.9999) {
|
||||||
|
win.setProgressBar(-1); // close progress bar
|
||||||
|
dirWatcher.close().then(() => dirWatcher = null);
|
||||||
|
} else {
|
||||||
|
win.setProgressBar(progress);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
playlist.items.forEach((song) => {
|
||||||
|
win.webContents.send(
|
||||||
|
"downloader-download-playlist",
|
||||||
|
song.url,
|
||||||
|
playlistTitle,
|
||||||
|
options
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -46,7 +46,7 @@ const downloadVideoToMP3 = async (
|
|||||||
cleanupName(videoDetails?.author?.name) ||
|
cleanupName(videoDetails?.author?.name) ||
|
||||||
"",
|
"",
|
||||||
title: videoDetails?.media?.song || videoDetails?.title || "",
|
title: videoDetails?.media?.song || videoDetails?.title || "",
|
||||||
imageSrcYTPL: thumbnails ?
|
imageSrcYTPL: thumbnails ?
|
||||||
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
urlToJPG(thumbnails[thumbnails.length - 1].url, videoDetails?.videoId)
|
||||||
: ""
|
: ""
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ const observePopupContainer = () => {
|
|||||||
menu = getSongMenu();
|
menu = getSongMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (menu && !menu.contains(slider)) {
|
if (menu && menu.lastElementChild.lastElementChild.innerText.startsWith('Stats') && !menu.contains(slider)) {
|
||||||
menu.prepend(slider);
|
menu.prepend(slider);
|
||||||
if (!observingSlider) {
|
if (!observingSlider) {
|
||||||
setupSliderListener();
|
setupSliderListener();
|
||||||
|
|||||||
@ -8361,10 +8361,10 @@ yocto-queue@^1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-1.0.0.tgz#7f816433fb2cbc511ec8bf7d263c3b58a1a3c251"
|
||||||
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
integrity sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==
|
||||||
|
|
||||||
ytdl-core@^4.9.2:
|
ytdl-core@^4.10.0:
|
||||||
version "4.9.2"
|
version "4.10.0"
|
||||||
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.9.2.tgz#c2d1ec44ee3cabff35e5843c6831755e69ffacf0"
|
resolved "https://registry.yarnpkg.com/ytdl-core/-/ytdl-core-4.10.0.tgz#0835cb411677684539fac2bcc10553f6f58db3e1"
|
||||||
integrity sha512-aTlsvsN++03MuOtyVD4DRF9Z/9UAeeuiNbjs+LjQBAiw4Hrdp48T3U9vAmRPyvREzupraY8pqRoBfKGqpq+eHA==
|
integrity sha512-RCCoSVTmMeBPH5NFR1fh3nkDU9okvWM0ZdN6plw6I5+vBBZVUEpOt8vjbSgprLRMmGUsmrQZJhvG1CHOat4mLA==
|
||||||
dependencies:
|
dependencies:
|
||||||
m3u8stream "^0.8.4"
|
m3u8stream "^0.8.4"
|
||||||
miniget "^4.0.0"
|
miniget "^4.0.0"
|
||||||
|
|||||||
Reference in New Issue
Block a user