1
0
mirror of https://github.com/musix-org/musix-oss synced 2025-06-17 04:26:00 +00:00
This commit is contained in:
MatteZ02
2020-03-03 22:30:50 +02:00
parent edfcc6f474
commit 30022c7634
11800 changed files with 1984416 additions and 1 deletions

101
node_modules/simple-youtube-api/src/Request.js generated vendored Normal file
View File

@ -0,0 +1,101 @@
const fetch = require('node-fetch');
const Constants = require('./util/Constants');
class Request {
constructor(youtube) {
this.youtube = youtube;
}
/**
* Make a request to the YouTube API
* @param {string} endpoint The endpoint to query
* @param {object} [qs={}] Query strings
* @returns {Promise<object>}
*/
make(endpoint, qs = {}) {
qs = Object.assign({ key: this.youtube.key }, qs);
const params = Object.keys(qs).filter(k => qs[k]).map(k => `${k}=${qs[k]}`);
return fetch(encodeURI(`https://www.googleapis.com/youtube/v3/${endpoint}${params.length ? `?${params.join('&')}` : ''}`))
.then(result => result.json())
.then(result => {
if (result.error) return Promise.reject(result.error);
return result;
});
}
/**
* Get a resource from the YouTube API
* @param {string} type The type of resource to get
* @param {object} [qs={}] Any other query options
* @returns {Promise<object>}
*/
getResource(type, qs = {}) {
qs = Object.assign({ part: Constants.PARTS[type] }, qs);
return this.make(Constants.ENDPOINTS[type], qs).then(result =>
result.items.length ? result.items[0] : Promise.reject(new Error(`resource ${result.kind} not found`))
);
}
/**
* Get a resource from the YouTube API, by ID
* @param {string} type The type of resource to get
* @param {string} id The ID of the resource to get
* @param {object} [qs={}] Any other query options
* @returns {Promise<object>}
*/
getResourceByID(type, id, qs = {}) {
return this.getResource(type, Object.assign(qs, { id }));
}
/**
* Get a video from the YouTube API
* @param {string} id The video to get
* @param {object} [options] Any request options
* @returns {Promise<object>}
*/
getVideo(id, options) {
return this.getResourceByID('Videos', id, options);
}
/**
* Get a playlist from the YouTube API
* @param {string} id The playlist to get
* @param {object} [options] Any request options
* @returns {Promise<object>}
*/
getPlaylist(id, options) {
return this.getResourceByID('Playlists', id, options);
}
/**
* Get a channel from the YouTube API
* @param {string} id The channel to get
* @param {object} [options] Any request options
* @returns {Promise<object>}
*/
getChannel(id, options) {
return this.getResourceByID('Channels', id, options);
}
/**
* Fetch a paginated resource.
* @param {string} endpoint The endpoint to query.
* @param {number} [count=Infinity] How many results to retrieve.
* @param {Object} [options={}] Additional options to send.
* @param {Array} [fetched=[]] Previously fetched resources.
* @param {?string} [pageToken] The page token to retrieve.
* @returns {Promise<Array<object>>}
*/
getPaginated(endpoint, count = Infinity, options = {}, fetched = [], pageToken = null) {
if(count < 1) return Promise.reject('Cannot fetch less than 1.');
const limit = count > 50 ? 50 : count;
return this.make(endpoint, Object.assign(options, { pageToken, maxResults: limit })).then(result => {
const results = fetched.concat(result.items);
if(result.nextPageToken && limit !== count) return this.getPaginated(endpoint, count - limit, options, results, result.nextPageToken);
return results;
});
}
}
module.exports = Request;

232
node_modules/simple-youtube-api/src/index.js generated vendored Normal file
View File

@ -0,0 +1,232 @@
const Request = require('./Request');
const Video = require('./structures/Video');
const Playlist = require('./structures/Playlist');
const Channel = require('./structures/Channel');
const util = require('./util');
const Constants = require('./util/Constants');
/**
* Information about a thumbnail
* @typedef {Object} Thumbnail
* @property {string} url The URL of this thumbnail
* @property {number} width The width of this thumbnail
* @property {number} height The height of this thumbnail
*/
/**
* The YouTube API module
*/
class YouTube {
/**
* @param {string} key The YouTube Data API v3 key to use
*/
constructor(key) {
if (typeof key !== 'string') throw new Error('The YouTube API key you provided was not a string.');
/**
* The YouTube Data API v3 key
* @type {?string}
*/
this.key = key;
Object.defineProperty(this, 'key', { enumerable: false });
this.request = new Request(this);
}
/**
* Make a request to the YouTube API
* @param {string} endpoint The endpoint of the API
* @param {Object} qs The query string options
* @returns {Promise<Object>}
*/
/**
* Get a video by URL or ID
* @param {string} url The video URL or ID
* @param {Object} [options = {}] Options to request with the video.
* @returns {Promise<?Video>}
* @example
* API.getVideo('https://www.youtube.com/watch?v=dQw4w9WgXcQ')
* .then(video => {
* if (video) console.log(`The video's title is ${video.title}`);
* else console.log('video not found :(');
* })
* .catch(console.error);
*/
getVideo(url, options = {}) {
const id = Video.extractID(url);
if (!id) return Promise.reject(new Error(`No video ID found in URL: ${url}`));
return this.getVideoByID(id, options);
}
/**
* Get a video by ID
* @param {string} id The video ID
* @param {Object} [options = {}] Options to request with the video.
* @returns {Promise<?Video>}
* @example
* API.getVideoByID('3odIdmuFfEY')
* .then(video => {
* if (video) console.log(`The video's title is ${video.title}`);
* else console.log('video not found :(');
* })
* .catch(console.error);
*/
getVideoByID(id, options = {}) {
return this.request.getVideo(id, options).then(result => result ? new Video(this, result) : null);
}
/**
* Get a playlist by URL or ID
* @param {string} url The playlist URL or ID
* @param {Object} [options = {}] Options to request with the playlist.
* @returns {Promise<?Playlist>}
* @example
* API.getPlaylist('https://www.youtube.com/playlist?list=PLuY9odN8x9puRuCxiddyRzJ3F5jR-Gun9')
* .then(playlist => {
* if (playlist) console.log(`The playlist's title is ${playlist.title}`);
* else console.log('playlist not found :(');
* })
* .catch(console.error);
*/
getPlaylist(url, options = {}) {
const id = Playlist.extractID(url);
if (!id) return Promise.reject(new Error(`No playlist ID found in URL: ${url}`));
return this.getPlaylistByID(id, options);
}
/**
* Get a playlist by ID
* @param {string} id The playlist ID
* @param {Object} [options = {}] Options to request with the playlist.
* @returns {Promise<?Playlist>}
* @example
* API.getPlaylistByID('PL2BN1Zd8U_MsyMeK8r9Vdv1lnQGtoJaSa')
* .then(playlist => {
* if (playlist) console.log(`The playlist's title is ${playlist.title}`);
* else console.log('playlist not found :(');
* })
* .catch(console.error);
*/
getPlaylistByID(id, options = {}) {
return this.request.getPlaylist(id, options).then(result => result ? new Playlist(this, result) : null);
}
/**
* Get a channel by URL or ID
* @param {string} url The channel URL or ID
* @param {Object} [options = {}] Options to request with the channel.
* @returns {Promise<?Channel>}
* @example
* API.getChannel('https://www.youtube.com/channel/UC477Kvszl9JivqOxN1dFgPQ')
* .then(channel => {
* if (channel) console.log(`The channel's title is ${channel.title}`);
* else console.log('channel not found :(');
* })
* .catch(console.error);
*/
getChannel(url, options = {}) {
const id = Channel.extractID(url);
if (!id) return Promise.reject(new Error(`No channel ID found in URL: ${url}`));
return this.getChannelByID(id, options);
}
/**
* Get a channel by ID
* @param {string} id The channel ID
* @param {Object} [options = {}] Options to request with the channel.
* @returns {Promise<?Channel>}
* @example
* API.getChannelByID('UC477Kvszl9JivqOxN1dFgPQ')
* .then(channel => {
* if (channel) console.log(`The channel's title is ${channel.title}`);
* else console.log('channel not found :(');
* })
* .catch(console.error);
*/
getChannelByID(id, options = {}) {
return this.request.getChannel(id, options).then(result => result ? new Channel(this, result) : null);
}
/**
* Search YouTube for videos, playlists, and channels
* @param {string} query The string to search for
* @param {number} [limit = 5] Maximum results to obtain
* @param {Object} [options] Additional options to pass to the API request
* @returns {Promise<Array<Video|Playlist|Channel|null>>}
* @example
* API.search('Centuries')
* .then(results => {
* console.log(`I got ${results.length} results`);
* })
* .catch(console.error);
*/
search(query, limit = 5, options = {}) {
return this.request.getPaginated(Constants.ENDPOINTS.Search, limit, Object.assign(options, { q: query, part: Constants.PARTS.Search }))
.then(result => result.map(item => {
if (item.id.kind === Constants.KINDS.Video) return new Video(this, item);
if (item.id.kind === Constants.KINDS.Playlist) return new Playlist(this, item);
if (item.id.kind === Constants.KINDS.Channel) return new Channel(this, item);
return null;
}));
}
/**
* Search YouTube for videos
* @param {string} query The string to search for
* @param {number} [limit = 5] Maximum results to obtain
* @param {Object} [options] Additional options to pass to the API request
* @returns {Promise<Video[]>}
* @example
* API.searchVideos('Centuries')
* .then(results => {
* console.log(`I got ${results.length} videos`);
* })
* .catch(console.error);
*/
searchVideos(query, limit = 5, options = {}) {
return this.search(query, limit, Object.assign(options, { type: 'video' }));
}
/**
* Search YouTube for playlists
* @param {string} query The string to search for
* @param {number} [limit = 5] Maximum results to obtain
* @param {Object} [options] Additional options to pass to the API request
* @returns {Promise<Playlist[]>}
* @example
* API.searchPlaylists('Centuries')
* .then(results => {
* console.log(`I got ${results.length} playlists`);
* })
* .catch(console.error);
*/
searchPlaylists(query, limit = 5, options = {}) {
return this.search(query, limit, Object.assign(options, { type: 'playlist' }));
}
/**
* Search YouTube for channels
* @param {string} query The string to search for
* @param {number} [limit = 5] Maximum results to obtain
* @param {Object} [options] Additional options to pass to the API request
* @returns {Promise<Channel[]>}
* @example
* API.searchChannels('Centuries')
* .then(results => {
* console.log(`I got ${results.length} channels`);
* })
* .catch(console.error);
*/
searchChannels(query, limit = 5, options = {}) {
return this.search(query, limit, Object.assign(options, { type: 'channel' }));
}
}
YouTube.Video = Video;
YouTube.Playlist = Playlist;
YouTube.Channel = Channel;
YouTube.util = util;
module.exports = YouTube;

View File

@ -0,0 +1,212 @@
const { parseURL } = require('../util');
const Constants = require('../util/Constants');
/**
* Represents a YouTube channel
* @class
*/
class Channel {
/**
* @param {YouTube} youtube The YouTube instance creating this
* @param {Object} data The data of the channel
*/
constructor(youtube, data) {
/**
* The YouTube instance that created this
* @type {YouTube}
*/
this.youtube = youtube;
Object.defineProperty(this, 'youtube', { enumerable: false });
/**
* The type to filter search results
* @type {string}
*/
this.type = 'channel';
this._patch(data);
}
_patch(data) {
if (!data) return;
/**
* Raw data from the YouTube API
* @type {object}
*/
this.raw = data;
/**
* Whether this is a full channel object.
* @type {boolean}
*/
this.full = data.kind === Constants.KINDS.Channel;
/**
* The YouTube resource from which this channel was created.
* @type {string}
*/
this.kind = data.kind;
/**
* This channel's ID
* @type {string}
* @name Channel#id
*/
/**
* This channel's title
* @type {?string}
* @name Channel#title
*/
switch (data.kind) {
case Constants.KINDS.Playlist:
case Constants.KINDS.PlaylistItem:
case Constants.KINDS.Video:
if (data.snippet) {
this.id = data.snippet.channelId;
this.title = data.snippet.channelTitle;
break;
} else {
throw new Error('Attempted to make a channel out of a resource with no channel data.');
}
case Constants.KINDS.SearchResult:
if (data.id.kind === Constants.KINDS.Channel) {
this.id = data.id.channelId;
break;
} else if (data.snippet) {
this.id = data.snippet.channelId;
this.title = data.snippet.channelTitle;
break;
} else {
throw new Error('Attempted to make a channel out of a search result with no channel data.');
}
case Constants.KINDS.Channel:
this.id = data.id;
if (data.snippet) {
this.title = data.snippet.title;
/**
* This channel's description
* @type {?string}
* @name Channel#description
*/
this.description = data.snippet.description;
/**
* The channel's custom URL if it has one
* @type {?string}
*/
this.customURL = data.snippet.customUrl;
/**
* The channel's creation date
* @type {?Date}
* @name Channel#publishedAt
*/
this.publishedAt = new Date(data.snippet.publishedAt);
/**
* The channel's thumbnails: available types are 'default', 'medium', and 'high'
* @type {?Object.<string, Thumbnail>}
*/
this.thumbnails = data.snippet.thumbnails;
/**
* The channel's default language
* @type {?string}
*/
this.defaultLanguage = data.snippet.defaultLanguage;
/**
* Information about the channel as specified in the `hl` query parameter
* @type {?{title: string, description: string}}
*/
this.localized = data.snippet.localized;
/**
* The country of the channel
* @type {?string}
*/
this.country = data.snippet.country;
}
if (data.contentDetails) {
/**
* Playlists associated with this channel; all values are playlist IDs
* @type {?Object}
* @property {?string} likes The channel's liked videos
* @property {?string} favorites The channel's favorited videos (note: favorited videos are deprecated)
* @property {?string} uploads The channel's uploaded videos
*/
this.relatedPlaylists = data.contentDetails.relatedPlaylists;
}
if (data.statistics) {
/**
* The number of times the channel has been viewed
* @type {?number}
*/
this.viewCount = data.statistics.viewCount;
/**
* The number of comments on the channel
* @type {?number}
*/
this.commentCount = data.statistics.commentCount;
/**
* The number of subscribers the channel has
* @type {?number}
*/
this.subscriberCount = data.statistics.subscriberCount;
/**
* Whether the channel's subscriber count is public
* @type {?boolean}
*/
this.hiddenSubscriberCount = data.statistics.hiddenSubscriberCount;
/**
* The number of videos this channel has uploaded
* @type {?number}
*/
this.videoCount = data.statistics.videoCount;
}
break;
default:
throw new Error(`Unknown channel kind: ${data.kind}.`);
}
return this;
}
/**
* Fetch the full representation of this channel.
* @param {object} [options] Any extra query params
* @returns {Channel}
*/
fetch(options) {
return this.youtube.request.getChannel(this.id, options).then(this._patch.bind(this));
}
/**
* The URL to this channel
* @type {string}
*/
get url() {
return `https://www.youtube.com/channel/${this.id}`;
}
/**
* Get a channel ID from a string (URL or ID)
* @param {string} url The string to get the ID from
* @returns {?string}
*/
static extractID(url) {
return parseURL(url).channel;
}
}
module.exports = Channel;

View File

@ -0,0 +1,180 @@
const { parseURL } = require('../util');
const Constants = require('../util/Constants');
const Video = require('./Video');
const Channel = require('./Channel');
/** Represents a YouTube playlist */
class Playlist {
/**
* @param {YouTube} youtube The YouTube instance creating this
* @param {Object} data The data of the playlist
*/
constructor(youtube, data) {
/**
* The YouTube instance that created this
* @type {YouTube}
*/
this.youtube = youtube;
Object.defineProperty(this, 'youtube', { enumerable: false });
/**
* The type to filter search results
* @type {string}
*/
this.type = 'playlist';
/**
* Videos in this playlist. Available after calling {@link Playlist#getVideos}.
* @type {Array<Video>}
*/
this.videos = [];
this._patch(data);
}
_patch(data) {
if (!data) return;
this.raw = data;
/**
* The channel this playlist is in
* @type {Channel}
*/
this.channel = new Channel(this.youtube, data);
/**
* This playlist's ID
* @type {string}
* @name Playlist#id
*/
switch (data.kind) {
case Constants.KINDS.SearchResult:
if (data.id.kind === Constants.KINDS.Playlist) this.id = data.id.playlistId;
else throw new Error('Attempted to make a playlist out of a non-playlist search result.');
break;
case Constants.KINDS.Playlist:
this.id = data.id;
break;
case Constants.KINDS.PlaylistItem:
if (data.snippet) this.id = data.snippet.playlistId;
else throw new Error('Attempted to make a playlist out of a resource with no playlist data.');
return this; // don't pull extra info from playlist item info
default:
throw new Error(`Unknown playlist kind: ${data.kind}.`);
}
if (data.snippet) {
/**
* This playlist's title
* @type {?string}
*/
this.title = data.snippet.title;
/**
* This playlist's description
* @type {?string}
*/
this.description = data.snippet.description;
/**
* The date/time this playlist was published
* @type {?Date}
*/
this.publishedAt = new Date(data.snippet.publishedAt);
/**
* Thumbnails for this playlist
* @type {?Object.<string, Thumbnail>}
*/
this.thumbnails = data.snippet.thumbnails;
/**
* Channel title of this playlist
* @type {?string}
*/
this.channelTitle = data.snippet.channelTitle;
/**
* The language in this playlist's title and description
* @type {?string}
*/
this.defaultLanguage = data.snippet.defaultLanguage;
/**
* Information about the playlist as specified in the `hl` parameter
* @type {?{title: string, description: string}}
*/
this.localized = data.snippet.localized;
}
if (data.status) {
/**
* The privacy status of this video
* @type {string}
*/
this.privacy = data.status.privacyStatus;
}
if (data.contentDetails) {
/**
* The total number of videos in this playlist
* @type {number}
*/
this.length = data.contentDetails.itemCount;
}
if (data.player) {
/**
* A string with an iframe tag for embedding this playlist
* @type {string}
*/
this.embedHTML = data.player.embedHtml;
}
return this;
}
/**
* The URL to this playlist
* @type {string}
*/
get url() {
return `https://www.youtube.com/playlist?list=${this.id}`;
}
/**
* Fetch the full representation of this playlist.
* @param {object} [options] Any extra query params
* @returns {Playlist}
*/
fetch(options) {
return this.youtube.request.getPlaylist(this.id, options).then(this._patch.bind(this));
}
/**
* Gets videos in the playlist
* @param {Number} [limit] Maximum number of videos to obtain. Fetches all if not provided.
* @param {Object} [options] Options to retrieve for each video.
* @returns {Promise<Video[]>}
*/
getVideos(limit, options) {
return this.youtube.request.getPaginated(
Constants.ENDPOINTS.PlaylistItems,
limit,
Object.assign({ playlistId: this.id, part: Constants.PARTS.PlaylistItems }, options)
).then(items => this.videos = items.map(i => new Video(this.youtube, i)));
}
/**
* Get a playlist ID from a string (URL or ID)
* @param {string} url The string to get the ID from
* @returns {?string}
*/
static extractID(url) {
return parseURL(url).playlist;
}
}
module.exports = Playlist;

181
node_modules/simple-youtube-api/src/structures/Video.js generated vendored Normal file
View File

@ -0,0 +1,181 @@
const duration = require('iso8601-duration');
const { parseURL } = require('../util');
const Constants = require('../util/Constants');
const Channel = require('./Channel');
/** Represents a YouTube video */
class Video {
/**
* @param {YouTube} youtube The YouTube instance creating this
* @param {Object} data The data of the video
*/
constructor(youtube, data) {
/**
* The YouTube instance that created this
* @type {YouTube}
*/
this.youtube = youtube;
Object.defineProperty(this, 'youtube', { enumerable: false });
/**
* The type to filter search results
* @type {string}
*/
this.type = 'video';
this._patch(data);
}
_patch(data) {
if (!data) return;
/**
* The raw data from the YouTube API.
* @type {object}
*/
this.raw = data;
/**
* Whether this is a full (returned from the videos API end point) or partial video (returned
* as part of another resource).
* @type {boolean}
*/
this.full = data.kind === Constants.KINDS.Video;
/**
* The resource that this video was created from.
* @type {string}
*/
this.kind = data.kind;
/**
* This video's ID
* @type {string}
* @name Video#id
*/
switch (data.kind) {
case Constants.KINDS.PlaylistItem:
if (data.snippet) {
if (data.snippet.resourceId.kind === Constants.KINDS.Video) this.id = data.snippet.resourceId.videoId;
else throw new Error('Attempted to make a video out of a non-video playlist item.');
break;
} else {
throw new Error('Attempted to make a video out of a playlist item with no video data.');
}
case Constants.KINDS.Video:
this.id = data.id;
break;
case Constants.KINDS.SearchResult:
if (data.id.kind === Constants.KINDS.Video) this.id = data.id.videoId;
else throw new Error('Attempted to make a video out of a non-video search result.');
break;
default:
throw new Error(`Unknown video kind: ${data.kind}.`);
}
if (data.snippet) {
/**
* This video's title
* @type {string}
*/
this.title = data.snippet.title;
/**
* This video's description
* @type {string}
*/
this.description = data.snippet.description;
/**
* The thumbnails of this video.
* @type {Object.<'default', 'medium', 'high', 'standard', 'maxres'>}
*/
this.thumbnails = data.snippet.thumbnails;
/**
* The date/time this video was published
* @type {Date}
*/
this.publishedAt = new Date(data.snippet.publishedAt);
/**
* The channel this video is in.
* @type {Channel}
*/
this.channel = new Channel(this.youtube, data);
}
if(data.contentDetails) {
/**
* An object containing time period information. All properties are integers, and do not include the lower
* precision ones.
* @typedef {Object} DurationObject
* @property {number} [hours] How many hours the video is long
* @property {number} [minutes] How many minutes the video is long
* @property {number} [seconds] How many seconds the video is long
*/
/**
* The duration of the video
* @type {?DurationObject}
*/
this.duration = data.contentDetails.duration ? duration.parse(data.contentDetails.duration) : null;
}
return this;
}
/**
* The maxiumum available resolution thumbnail.
* @type {object}
*/
get maxRes() {
const t = this.thumbnails;
return t.maxres || t.standard || t.high || t.medium || t.default;
}
/**
* The URL to this video
* @type {string}
*/
get url() {
return `https://www.youtube.com/watch?v=${this.id}`;
}
/**
* The short URL to this video
* @type {string}
*/
get shortURL() {
return `https://youtu.be/${this.id}`;
}
/**
* The duration of the video in seconds
* @type {number}
*/
get durationSeconds() {
return this.duration ? duration.toSeconds(this.duration) : -1;
}
/**
* Fetch the full representation of this video.
* @param {object} [options] Any extra query params
* @returns {Video}
*/
fetch(options) {
return this.youtube.request.getVideo(this.id, options).then(this._patch.bind(this));
}
/**
* Get a video ID from a string (URL or ID)
* @param {string} url The string to get the ID from
* @returns {?string}
*/
static extractID(url) {
return parseURL(url).video;
}
}
module.exports = Video;

23
node_modules/simple-youtube-api/src/util/Constants.js generated vendored Normal file
View File

@ -0,0 +1,23 @@
exports.PARTS = {
Search: 'snippet',
Videos: 'snippet,contentDetails',
Playlists: 'snippet',
PlaylistItems: 'snippet,status',
Channels: 'snippet'
};
exports.KINDS = {
Video: 'youtube#video',
PlaylistItem: 'youtube#playlistItem',
Playlist: 'youtube#playlist',
SearchResult: 'youtube#searchResult',
Channel: 'youtube#channel'
};
exports.ENDPOINTS = {
PlaylistItems: 'playlistItems',
Channels: 'channels',
Videos: 'videos',
Playlists: 'playlists',
Search: 'search'
};

41
node_modules/simple-youtube-api/src/util/index.js generated vendored Normal file
View File

@ -0,0 +1,41 @@
const { parse } = require('url');
/**
* Parse a string as a potential YouTube resource URL.
* @param {string} url
* @returns {{video: ?string, channel: ?string, playlist: ?string}}
*/
exports.parseURL = (url) => {
const parsed = parse(url, true);
switch (parsed.hostname) {
case 'www.youtube.com':
case 'youtube.com':
case 'm.youtube.com':
case 'music.youtube.com': {
const idRegex = /^[a-zA-Z0-9-_]+$/;
if (parsed.pathname === '/watch') {
if (!idRegex.test(parsed.query.v)) return {};
const response = { video: parsed.query.v };
if (parsed.query.list) response.playlist = parsed.query.list;
return response;
} else if (parsed.pathname === '/playlist') {
if(!idRegex.test(parsed.query.list)) return {};
return { playlist: parsed.query.list };
} else if (parsed.pathname.startsWith('/channel/')) {
const id = parsed.pathname.replace('/channel/', '');
if (!idRegex.test(id)) return {};
return { channel: id };
} else if (parsed.pathname.startsWith('/browse/')) {
const id = parsed.pathname.replace('/browse/', '');
if (!idRegex.test(id)) return {};
return { channel: id };
}
return {};
}
case 'youtu.be':
return { video: /^\/[a-zA-Z0-9-_]+$/.test(parsed.pathname) ? parsed.pathname.slice(1) : null };
default:
return {};
}
};