add support for shorts urls, channel handles, channel /c/ and /user/
This commit is contained in:
parent
3d79e7ed93
commit
aff62f48ec
|
@ -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.')
|
||||
|
|
|
@ -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'
|
||||
];
|
||||
|
||||
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']
|
||||
const whereIsIt: Record<string, (string|number)[]> = {
|
||||
channel: ['metadata', 'channelMetadataRenderer', 'externalId'],
|
||||
handle: ['responseContext', 'serviceTrackingParams', 0, 'params'],
|
||||
user: ['metadata', 'channelMetadataRenderer', 'externalId']
|
||||
}
|
||||
|
||||
// /channel/ and /c/
|
||||
const channelMatch = url.pathname.match(/^\/(channel|c)\/([a-zA-Z0-9_-]+)/);
|
||||
if (channelMatch) {
|
||||
return channelMatch[2];
|
||||
if (hostnames.includes(url.hostname)) {
|
||||
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.`
|
||||
}
|
||||
|
||||
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 }
|
Loading…
Reference in New Issue