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);
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('CAPTCHA - Solving a cryptographic challenge before downloading.')

View File

@ -1,35 +1,29 @@
function validateVideo(input: string): string | false {
try {
const url = new URL(input);
let videoId: string = ''
const hostnames = [
'youtube.com',
'www.youtube.com',
'm.youtube.com'
];
// basic hostname check
if (hostnames.includes(url.hostname)) {
// basic url
if (url.pathname === '/watch') {
const videoId = url.searchParams.get('v');
return videoId || false;
}
// embed url
const embedMatch = url.pathname.match(/^\/embed\/([a-zA-Z0-9_-]+)/);
if (embedMatch) {
return embedMatch[1];
}
return false;
if (!url.searchParams.get('v')) return false
videoId = url.searchParams.get('v')!
} else if (url.pathname.startsWith('/shorts/')) {
videoId = url.pathname.replace('/shorts/', '')
} else return false
// removed - embed url
}
// short urls
if (url.hostname === 'youtu.be') {
const videoId = url.pathname.replace(/^\//, '');
return videoId || false;
videoId = url.pathname.replace(/^\//, '');
}
if (videoId && videoId.match(/[\w\-_]{11}/)) return videoId
return false;
} catch {
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 {
const url = new URL(input);
const hostnames = [
@ -67,19 +61,43 @@ async function validateChannel(input: string): Promise<string | false> {
'www.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)) {
// @ urls
const atMatch = url.pathname.match(/^\/@([a-zA-Z0-9.-]+)/);
if (atMatch) {
const channelId = await (await fetch(`https://yt.jaybee.digital/api/channels?part=channels&handle=${atMatch[1]}`)).json()
return channelId['items'][0]['id']
let whatIsIt = ''
// many thanks to Benjamin Loison (@Benjamin-Loison) for his PHP implementation of this
// https://github.com/Benjamin-Loison/YouTube-operational-API/blob/main/channels.php
// 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 channelMatch = url.pathname.match(/^\/(channel|c)\/([a-zA-Z0-9_-]+)/);
if (channelMatch) {
return channelMatch[2];
const channelJson = await channelReq.json()
let channelId: string | Record<any, any> = getByPath(channelJson, whereIsIt[whatIsIt]!);
if (whatIsIt == 'handle') {
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 }