diff --git a/lib/parseItem.js b/lib/parseItem.js index b72f2ec..787bc0a 100644 --- a/lib/parseItem.js +++ b/lib/parseItem.js @@ -22,17 +22,19 @@ module.exports = item => { }; const parseVideo = obj => { - if (!obj.videoId || obj.upcomingEventData) return null; const author = obj.ownerText && obj.ownerText.runs[0]; - const authorUrl = author && (author.navigationEndpoint.browseEndpoint.canonicalBaseUrl || - author.navigationEndpoint.commandMetadata.webCommandMetadata.url); - const isLive = Array.isArray(obj.badges) ? - !!obj.badges.find(a => a.metadataBadgeRenderer.label === 'LIVE NOW') : false; - const authorThumbnails = !author ? null : - obj.channelThumbnailSupportedRenderers.channelThumbnailWithLinkRenderer.thumbnail.thumbnails; - const ownerBadges = obj.ownerBadges && JSON.stringify(obj.ownerBadges); - const isOfficial = ownerBadges && !!ownerBadges.includes('OFFICIAL'); - const isVerified = ownerBadges && !!ownerBadges.includes('VERIFIED'); + let authorUrl = null; + if (author) { + authorUrl = author.navigationEndpoint.browseEndpoint.canonicalBaseUrl || + author.navigationEndpoint.commandMetadata.webCommandMetadata.url; + } + const badges = Array.isArray(obj.badges) ? obj.badges.map(a => a.metadataBadgeRenderer.label) : []; + const isLive = badges.some(b => b === 'LIVE NOW'); + const upcoming = obj.upcomingEventData ? Number(`${obj.upcomingEventData.startTime}000`) : null; + const ctsr = obj.channelThumbnailSupportedRenderers; + const authorImg = !ctsr ? { thumbnail: { thumbnails: [] } } : ctsr.channelThumbnailWithLinkRenderer; + const isOfficial = !!(obj.ownerBadges && JSON.stringify(obj.ownerBadges).includes('OFFICIAL')); + const isVerified = !!(obj.ownerBadges && JSON.stringify(obj.ownerBadges).includes('VERIFIED')); const lengthFallback = obj.thumbnailOverlays.find(x => Object.keys(x)[0] === 'thumbnailOverlayTimeStatusRenderer'); const length = obj.lengthText || (lengthFallback && lengthFallback.thumbnailOverlayTimeStatusRenderer.text); @@ -41,25 +43,31 @@ const parseVideo = obj => { name: UTIL.parseText(obj.title), id: obj.videoId, url: BASE_VIDEO_URL + obj.videoId, - thumbnail: UTIL.sortImg(obj.thumbnail.thumbnails)[0].url, + thumbnail: UTIL.prepImg(obj.thumbnail.thumbnails)[0].url, + thumbnails: UTIL.prepImg(obj.thumbnail.thumbnails), + isUpcoming: !!upcoming, + upcoming, isLive, + badges, // Author can be null for shows like whBqghP5Oow author: author ? { name: author.text, channelID: author.navigationEndpoint.browseEndpoint.browseId, url: new URL(authorUrl, BASE_VIDEO_URL).toString(), - bestAvatar: UTIL.sortImg(authorThumbnails)[0], - avatars: UTIL.sortImg(authorThumbnails), + bestAvatar: UTIL.prepImg(authorImg.thumbnail.thumbnails)[0] || null, + avatars: UTIL.prepImg(authorImg.thumbnail.thumbnails), ownerBadges: Array.isArray(obj.ownerBadges) ? obj.ownerBadges.map(a => a.metadataBadgeRenderer.tooltip) : [], verified: isOfficial || isVerified, } : null, - description: obj.descriptionSnippet ? UTIL.parseText(obj.descriptionSnippet) : null, + description: UTIL.parseText(obj.descriptionSnippet), - views: obj.viewCountText ? UTIL.parseIntegerFromText(obj.viewCountText) : null, - duration: isLive ? 'Live' : UTIL.parseText(length), - uploadedAt: obj.publishedTimeText ? UTIL.parseText(obj.publishedTimeText) : null, + views: !obj.viewCountText ? null : UTIL.parseIntegerFromText(obj.viewCountText), + // Duration not provided for live & sometimes with upcoming & sometimes randomly + duration: UTIL.parseText(length), + // UploadedAt not provided for live & upcoming & sometimes randomly + uploadedAt: UTIL.parseText(obj.publishedTimeText), }; }; @@ -68,5 +76,28 @@ const parsePlaylist = obj => ({ id: obj.playlistId, name: UTIL.parseText(obj.title), url: `https://www.youtube.com/playlist?list=${obj.playlistId}`, - length: UTIL.parseIntegerFromText(obj.videoCount), + + // Some Playlists starting with OL only provide a simple string + owner: obj.shortBylineText.simpleText ? null : _parseOwner(obj), + + publishedAt: UTIL.parseText(obj.publishedTimeText), + length: Number(obj.videoCount), }); + +const _parseOwner = obj => { + const owner = (obj.shortBylineText && obj.shortBylineText.runs[0]) || + (obj.longBylineText && obj.longBylineText.runs[0]); + const ownerUrl = owner.navigationEndpoint.browseEndpoint.canonicalBaseUrl || + owner.navigationEndpoint.commandMetadata.webCommandMetadata.url; + const isOfficial = !!(obj.ownerBadges && JSON.stringify(obj.ownerBadges).includes('OFFICIAL')); + const isVerified = !!(obj.ownerBadges && JSON.stringify(obj.ownerBadges).includes('VERIFIED')); + const fallbackURL = owner.navigationEndpoint.commandMetadata.webCommandMetadata.url; + + return { + name: owner.text, + channelID: owner.navigationEndpoint.browseEndpoint.browseId, + url: new URL(ownerUrl || fallbackURL, BASE_VIDEO_URL).toString(), + ownerBadges: Array.isArray(obj.ownerBadges) ? obj.ownerBadges.map(a => a.metadataBadgeRenderer.tooltip) : [], + verified: isOfficial || isVerified, + }; +}; diff --git a/lib/util.js b/lib/util.js index ef71fcc..445072a 100644 --- a/lib/util.js +++ b/lib/util.js @@ -18,7 +18,8 @@ const DEFAULT_CONTEXT = { exports.parseBody = (body, options = {}) => { let json = jsonAfter(body, 'var ytInitialData = ') || jsonAfter(body, 'window["ytInitialData"] = ') || null; const apiKey = between(body, 'INNERTUBE_API_KEY":"', '"') || between(body, 'innertubeApiKey":"', '"'); - const clientVersion = between(body, 'INNERTUBE_CONTEXT_CLIENT_VERSION":"', '"') || + const clientVersion = + between(body, 'INNERTUBE_CONTEXT_CLIENT_VERSION":"', '"') || between(body, 'innertube_context_client_version":"', '"'); const context = buildPostContext(clientVersion, options); // Return multiple values @@ -38,8 +39,8 @@ const buildPostContext = exports.buildPostContext = (clientVersion, options = {} }; // Parsing utility -const parseText = exports.parseText = txt => typeof txt === 'object' ? txt.simpleText || - (Array.isArray(txt.runs) ? txt.runs.map(a => a.text).join('') : '') : ''; +const parseText = exports.parseText = txt => + typeof txt === 'object' ? txt.simpleText || (Array.isArray(txt.runs) ? txt.runs.map(a => a.text).join('') : '') : ''; exports.parseIntegerFromText = x => typeof x === 'string' ? Number(x) : Number(parseText(x).replace(/\D+/g, '')); @@ -121,11 +122,15 @@ exports.checkArgs = (searchString, options = {}) => { const between = (haystack, left, right) => { let pos; pos = haystack.indexOf(left); - if (pos === -1) { return ''; } + if (pos === -1) { + return ''; + } pos += left.length; haystack = haystack.slice(pos); pos = haystack.indexOf(right); - if (pos === -1) { return ''; } + if (pos === -1) { + return ''; + } haystack = haystack.slice(0, pos); return haystack; }; @@ -142,10 +147,14 @@ const between = (haystack, left, right) => { exports.betweenFromRight = (haystack, left, right) => { let pos; pos = haystack.indexOf(right); - if (pos === -1) { return ''; } + if (pos === -1) { + return ''; + } haystack = haystack.slice(0, pos); pos = haystack.lastIndexOf(left); - if (pos === -1) { return ''; } + if (pos === -1) { + return ''; + } pos += left.length; haystack = haystack.slice(pos); return haystack; @@ -162,7 +171,9 @@ exports.betweenFromRight = (haystack, left, right) => { const jsonAfter = (haystack, left) => { try { const pos = haystack.indexOf(left); - if (pos === -1) { return null; } + if (pos === -1) { + return null; + } haystack = haystack.slice(pos + left.length); return JSON.parse(cutAfterJSON(haystack)); } catch (e) { @@ -177,7 +188,7 @@ const jsonAfter = (haystack, left) => { * @param {string} mixedJson mixedJson * @returns {string} * @throws {Error} no json or invalid json -*/ + */ const cutAfterJSON = exports.cutAfterJSON = mixedJson => { let open, close; if (mixedJson[0] === '[') { @@ -216,7 +227,7 @@ const cutAfterJSON = exports.cutAfterJSON = mixedJson => { // All brackets have been closed, thus end of JSON is reached if (counter === 0) { // Return the cut JSON - return mixedJson.substr(0, i + 1); + return mixedJson.substring(0, i + 1); } } @@ -224,8 +235,15 @@ const cutAfterJSON = exports.cutAfterJSON = mixedJson => { throw Error("Can't cut unsupported JSON (no matching closing bracket found)"); }; -// Sorts Images in descending order -exports.sortImg = img => img.sort((a, b) => b.width - a.width); +// Sorts Images in descending order & normalizes the url's +exports.prepImg = img => { + // Resolve url + img.forEach(x => { + x.url = x.url ? new URL(x.url, BASE_URL).toString() : null; + }); + // Sort + return img.sort((a, b) => b.width - a.width); +}; exports.parseWrapper = primaryContents => { let rawItems = []; @@ -233,18 +251,19 @@ exports.parseWrapper = primaryContents => { // Older Format if (primaryContents.sectionListRenderer) { - rawItems = primaryContents.sectionListRenderer.contents - .find(x => Object.keys(x)[0] === 'itemSectionRenderer') + rawItems = primaryContents.sectionListRenderer.contents.find(x => Object.keys(x)[0] === 'itemSectionRenderer') .itemSectionRenderer.contents; - continuation = primaryContents.sectionListRenderer.contents - .find(x => Object.keys(x)[0] === 'continuationItemRenderer'); + continuation = primaryContents.sectionListRenderer.contents.find( + x => Object.keys(x)[0] === 'continuationItemRenderer', + ); // Newer Format } else if (primaryContents.richGridRenderer) { rawItems = primaryContents.richGridRenderer.contents .filter(x => !Object.prototype.hasOwnProperty.call(x, 'continuationItemRenderer')) .map(x => (x.richItemRenderer || x.richSectionRenderer).content); - continuation = primaryContents.richGridRenderer.contents - .find(x => Object.prototype.hasOwnProperty.call(x, 'continuationItemRenderer')); + continuation = primaryContents.richGridRenderer.contents.find(x => + Object.prototype.hasOwnProperty.call(x, 'continuationItemRenderer'), + ); } return { rawItems, continuation }; @@ -272,6 +291,11 @@ exports.parsePage2Wrapper = continuationItems => { return { rawItems, continuation }; }; -const clone = obj => Object.keys(obj).reduce((v, d) => Object.assign(v, { - [d]: obj[d].constructor === Object ? clone(obj[d]) : obj[d], -}), {}); +const clone = obj => + Object.keys(obj).reduce( + (v, d) => + Object.assign(v, { + [d]: obj[d].constructor === Object ? clone(obj[d]) : obj[d], + }), + {}, + ); diff --git a/package.json b/package.json index 7613aa0..2f08bea 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@distube/ytsr", - "version": "1.1.5", + "version": "1.1.6", "description": "A ytsr fork. Made for DisTube.js.org.", "keywords": [ "youtube", @@ -29,7 +29,7 @@ "lint:fix": "eslint --fix ./" }, "dependencies": { - "miniget": "^4.2.1" + "miniget": "^4.2.2" }, "devDependencies": { "eslint": "^7.32.0" diff --git a/typings/index.d.ts b/typings/index.d.ts index 38596bf..ca5a8c7 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -1,4 +1,4 @@ -declare module '@distube/ytsr' { +declare module "@distube/ytsr" { namespace ytsr { interface Options { safeSearch?: boolean; @@ -7,7 +7,8 @@ declare module '@distube/ytsr' { hl?: string; gl?: string; utcOffsetMinutes?: number; - type?: 'video' | 'playlist'; + type?: "video" | "playlist"; + requestOptions?: { [key: string]: object } & { headers?: { [key: string]: string } }; } interface Image { @@ -17,12 +18,16 @@ declare module '@distube/ytsr' { } interface Video { - type: 'video'; + type: "video"; id: string; name: string; url: string; thumbnail: string; + thumbnails: Image[]; + isUpcoming: boolean; + upcoming: number | null; isLive: boolean; + badges: string[]; views: number; duration: string; author: { @@ -37,7 +42,7 @@ declare module '@distube/ytsr' { } interface Playlist { - type: 'playlist'; + type: "playlist"; id: string; name: string; url: string; @@ -49,6 +54,7 @@ declare module '@distube/ytsr' { ownerBadges: string[]; verified: boolean; } | null; + publishedAt: string | null; } interface VideoResult { @@ -65,7 +71,11 @@ declare module '@distube/ytsr' { } function ytsr(id: string): Promise; - function ytsr(id: string, options: ytsr.Options & { type: 'playlist' }): Promise; + function ytsr(id: string, options: ytsr.Options & { type: "playlist" }): Promise; + function ytsr( + id: string, + options: ytsr.Options & { type: "video" | "playlist" } + ): Promise; function ytsr(id: string, options: ytsr.Options): Promise; export = ytsr;