diff --git a/package.json b/package.json index 1f60e8729a..76faad750a 100644 --- a/package.json +++ b/package.json @@ -134,7 +134,7 @@ "node-fetch": "^2.6.8", "simple-youtube-age-restriction-bypass": "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4", "vudio": "^2.1.1", - "youtubei.js": "^3.1.1", + "youtubei.js": "^4.1.0", "ytpl": "^2.3.0" }, "devDependencies": { diff --git a/plugins/downloader/back.js b/plugins/downloader/back.js index 35bedfc1a5..6263930bf3 100644 --- a/plugins/downloader/back.js +++ b/plugins/downloader/back.js @@ -20,7 +20,7 @@ const { const { ipcMain, app, dialog } = require('electron'); const is = require('electron-is'); -const { Innertube, UniversalCache, Utils } = require('youtubei.js'); +const { Innertube, UniversalCache, Utils, ClientType } = require('youtubei.js'); const ytpl = require('ytpl'); // REPLACE with youtubei getplaylist https://github.com/LuanRT/YouTube.js#getplaylistid const filenamify = require('filenamify'); @@ -48,20 +48,22 @@ let yt; let win; let playingUrl = undefined; -const sendError = (error) => { +const sendError = (error, source) => { win.setProgressBar(-1); // close progress bar setBadge(0); // close badge sendFeedback_(win); // reset feedback - console.error(error); + const songNameMessage = source ? `\nin ${source}` : ''; + const cause = error.cause ? `\n\n${error.cause.toString()}` : ''; + const message = `${error.toString()}${songNameMessage}${cause}`; + + console.error(message); dialog.showMessageBox({ type: 'info', buttons: ['OK'], title: 'Error in download!', message: 'Argh! Apologies, download failed…', - detail: `${error.toString()} ${ - error.cause ? `\n\n${error.cause.toString()}` : '' - }`, + detail: message, }); }; @@ -92,20 +94,23 @@ async function downloadSong( trackId = undefined, increasePlaylistProgress = () => {}, ) { + let resolvedName = undefined; try { await downloadSongUnsafe( url, + name=>resolvedName=name, playlistFolder, trackId, increasePlaylistProgress, ); } catch (error) { - sendError(error); + sendError(error, resolvedName || url); } } async function downloadSongUnsafe( url, + setName, playlistFolder = undefined, trackId = undefined, increasePlaylistProgress = () => {}, @@ -122,7 +127,11 @@ async function downloadSongUnsafe( sendFeedback('Downloading...', 2); const id = getVideoId(url); - const info = await yt.music.getInfo(id); + let info = await yt.music.getInfo(id); + + if (!info) { + throw new Error('Video not found'); + } const metadata = getMetadata(info); if (metadata.album === 'N/A') metadata.album = ''; @@ -133,6 +142,34 @@ async function downloadSongUnsafe( const name = `${metadata.artist ? `${metadata.artist} - ` : ''}${ metadata.title }`; + setName(name); + + let playabilityStatus = info.playability_status; + let bypassedResult = null; + if (playabilityStatus.status === "LOGIN_REQUIRED") { + // try to bypass the age restriction + bypassedResult = await getAndroidTvInfo(id); + playabilityStatus = bypassedResult.playability_status; + + if (playabilityStatus.status === "LOGIN_REQUIRED") { + throw new Error( + `[${playabilityStatus.status}] ${playabilityStatus.reason}`, + ); + } + + info = bypassedResult; + } + + if (playabilityStatus.status === "UNPLAYABLE") { + /** + * @typedef {import('youtubei.js/dist/src/parser/classes/PlayerErrorMessage').default} PlayerErrorMessage + * @type {PlayerErrorMessage} + */ + const errorScreen = playabilityStatus.error_screen; + throw new Error( + `[${playabilityStatus.status}] ${errorScreen.reason.text}: ${errorScreen.subreason.text}`, + ); + } const extension = presets[config.get('preset')]?.extension || 'mp3'; @@ -252,7 +289,7 @@ async function iterableStreamToMP3( return ffmpeg.FS('readFile', `${safeVideoName}.mp3`); } catch (e) { - sendError(e); + sendError(e, safeVideoName); } finally { releaseFFmpegMutex(); } @@ -307,7 +344,7 @@ async function writeID3(buffer, metadata, sendFeedback) { writer.addTag(); return Buffer.from(writer.arrayBuffer); } catch (e) { - sendError(e); + sendError(e, `${metadata.artist} - ${metadata.title}`); } } @@ -482,3 +519,16 @@ const getMetadata = (info) => ({ album: info.player_overlays?.browser_media_session?.album?.text, image: info.basic_info.thumbnail[0].url, }); + +// This is used to bypass age restrictions +const getAndroidTvInfo = async (id) => { + const innertube = await Innertube.create({ + clientType: ClientType.TV_EMBEDDED, + generate_session_locally: true, + retrieve_player: true, + }); + const info = await innertube.getBasicInfo(id, 'TV_EMBEDDED'); + // getInfo 404s with the bypass, so we use getBasicInfo instead + // that's fine as we only need the streaming data + return info; +} diff --git a/yarn.lock b/yarn.lock index f641fc4a3f..bfcc1a8ac3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8910,19 +8910,19 @@ __metadata: simple-youtube-age-restriction-bypass: "https://gitpkg.now.sh/api/pkg.tgz?url=zerodytrash/Simple-YouTube-Age-Restriction-Bypass&commit=v2.5.4" vudio: ^2.1.1 xo: ^0.53.1 - youtubei.js: ^3.1.1 + youtubei.js: ^4.1.0 ytpl: ^2.3.0 languageName: unknown linkType: soft -"youtubei.js@npm:^3.1.1": - version: 3.1.1 - resolution: "youtubei.js@npm:3.1.1" +"youtubei.js@npm:^4.1.0": + version: 4.1.0 + resolution: "youtubei.js@npm:4.1.0" dependencies: jintr: ^0.4.1 linkedom: ^0.14.12 undici: ^5.19.1 - checksum: 1280e2ddacec3034ee8e1b398ba80662a6854e184416d3484119e7cf47b69ab2e58b4f1efdf468dcad3e50bdc7bd42b6ee66b95660ffb521efb5f0634ef60fb7 + checksum: fa0090aa5b86c06a765757b0716ad9e5742c401b4fe662460db82495751e1fda3380b78f5fb916699f1707ab9b7c2783312dceac974afea3a5d101be62906bea languageName: node linkType: hard