Allow downloading age restricted videos

* Bypass age restriction using `androidTvInfo`
* Bump youtubei.js fix #1084
* Add more detailed error messages, including song name or url
This commit is contained in:
Araxeus
2023-03-24 14:52:16 +03:00
parent 4364d3be71
commit f722cf86dd
3 changed files with 66 additions and 16 deletions

View File

@ -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": {

View File

@ -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;
}

View File

@ -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