add support for shorts urls, channel handles, channel /c/ and /user/

This commit is contained in:
localhost 2025-09-15 22:03:05 +02:00
parent 3d79e7ed93
commit aff62f48ec
2 changed files with 51 additions and 25 deletions

View File

@ -128,6 +128,7 @@ app.ws('/savechannel', {
const channelId = await validateChannel(ws.data.query.url); const channelId = await validateChannel(ws.data.query.url);
if (!channelId) return sendError(ws, 'Invalid channel URL.'); if (!channelId) return sendError(ws, 'Invalid channel URL.');
if (typeof channelId !== 'string') return sendError(ws, `Failed to fetch channel ID - ${channelId.error}`)
ws.send('DATA - This process is automatic. Your video will start archiving shortly.') ws.send('DATA - This process is automatic. Your video will start archiving shortly.')
ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.') ws.send('CAPTCHA - Solving a cryptographic challenge before downloading.')

View File

@ -1,35 +1,29 @@
function validateVideo(input: string): string | false { function validateVideo(input: string): string | false {
try { try {
const url = new URL(input); const url = new URL(input);
let videoId: string = ''
const hostnames = [ const hostnames = [
'youtube.com', 'youtube.com',
'www.youtube.com', 'www.youtube.com',
'm.youtube.com' 'm.youtube.com'
]; ];
// basic hostname check
if (hostnames.includes(url.hostname)) { if (hostnames.includes(url.hostname)) {
// basic url
if (url.pathname === '/watch') { if (url.pathname === '/watch') {
const videoId = url.searchParams.get('v'); if (!url.searchParams.get('v')) return false
return videoId || false; videoId = url.searchParams.get('v')!
} } else if (url.pathname.startsWith('/shorts/')) {
videoId = url.pathname.replace('/shorts/', '')
// embed url } else return false
const embedMatch = url.pathname.match(/^\/embed\/([a-zA-Z0-9_-]+)/); // removed - embed url
if (embedMatch) {
return embedMatch[1];
}
return false;
} }
// short urls // short urls
if (url.hostname === 'youtu.be') { if (url.hostname === 'youtu.be') {
const videoId = url.pathname.replace(/^\//, ''); videoId = url.pathname.replace(/^\//, '');
return videoId || false;
} }
if (videoId && videoId.match(/[\w\-_]{11}/)) return videoId
return false; return false;
} catch { } catch {
return false; return false;
@ -59,7 +53,7 @@ function validatePlaylist(input: string): string | false {
} }
} }
async function validateChannel(input: string): Promise<string | false> { async function validateChannel(input: string): Promise<string | { error: string; } | false> {
try { try {
const url = new URL(input); const url = new URL(input);
const hostnames = [ const hostnames = [
@ -67,19 +61,43 @@ async function validateChannel(input: string): Promise<string | false> {
'www.youtube.com', 'www.youtube.com',
'm.youtube.com' 'm.youtube.com'
]; ];
const whereIsIt: Record<string, (string|number)[]> = {
channel: ['metadata', 'channelMetadataRenderer', 'externalId'],
handle: ['responseContext', 'serviceTrackingParams', 0, 'params'],
user: ['metadata', 'channelMetadataRenderer', 'externalId']
}
if (hostnames.includes(url.hostname)) { if (hostnames.includes(url.hostname)) {
// @ urls let whatIsIt = ''
const atMatch = url.pathname.match(/^\/@([a-zA-Z0-9.-]+)/);
if (atMatch) { // many thanks to Benjamin Loison (@Benjamin-Loison) for his PHP implementation of this
const channelId = await (await fetch(`https://yt.jaybee.digital/api/channels?part=channels&handle=${atMatch[1]}`)).json() // https://github.com/Benjamin-Loison/YouTube-operational-API/blob/main/channels.php
return channelId['items'][0]['id'] // and the stackoverflow answer with all the possible options, thank you.
// https://stackoverflow.com/a/75843807
if (url.pathname.startsWith('/channel/')) { // /channel/[id]
return url.pathname.match(/UC[\w\-_]{22}/gm)?.[0] || false
} else if (url.pathname.startsWith('/c/')) { // /c/[custom]
whatIsIt = 'channel'
} else if (url.pathname.startsWith('/user/')) {
whatIsIt = 'user'
} else if (url.pathname.match(/@[\w\-_.]{3,}/gm)) { // /@[handle]
whatIsIt = 'handle'
} else return false
const channelReq = await fetch(`${process.env.METADATA}/getWebpageJson?url=${url}`)
if (!channelReq.ok) return {
error: `Failed to fetch Youtube with status ${channelReq.status}. Please retry.`
} }
// /channel/ and /c/ const channelJson = await channelReq.json()
const channelMatch = url.pathname.match(/^\/(channel|c)\/([a-zA-Z0-9_-]+)/); let channelId: string | Record<any, any> = getByPath(channelJson, whereIsIt[whatIsIt]!);
if (channelMatch) { if (whatIsIt == 'handle') {
return channelMatch[2]; channelId = channelId.find((c:any) => c.key == 'browse_id').value
}
return typeof channelId == 'string' ? channelId : {
error: 'Failed to extract channel ID from the Youtube provided JSON.'
} }
} }
@ -89,4 +107,11 @@ async function validateChannel(input: string): Promise<string | false> {
} }
} }
function getByPath(obj: Record<any, any>, path: (string|number)[]) {
return path.reduce((acc, key) => {
if (acc === undefined || acc === null) return undefined
return acc[key]
}, obj)
}
export { validateVideo, validatePlaylist, validateChannel } export { validateVideo, validatePlaylist, validateChannel }