Skip to content

Commit

Permalink
1.1.6
Browse files Browse the repository at this point in the history
  • Loading branch information
skick1234 committed Jul 14, 2022
1 parent bcb86c6 commit 324a98a
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 46 deletions.
67 changes: 49 additions & 18 deletions lib/parseItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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),
};
};

Expand All @@ -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,
};
};
66 changes: 45 additions & 21 deletions lib/util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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, ''));

Expand Down Expand Up @@ -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;
};
Expand All @@ -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;
Expand All @@ -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) {
Expand All @@ -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] === '[') {
Expand Down Expand Up @@ -216,35 +227,43 @@ 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);
}
}

// We ran through the whole string and ended up with an unclosed bracket
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 = [];
let continuation = null;

// 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 };
Expand Down Expand Up @@ -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],
}),
{},
);
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -29,7 +29,7 @@
"lint:fix": "eslint --fix ./"
},
"dependencies": {
"miniget": "^4.2.1"
"miniget": "^4.2.2"
},
"devDependencies": {
"eslint": "^7.32.0"
Expand Down
20 changes: 15 additions & 5 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
declare module '@distube/ytsr' {
declare module "@distube/ytsr" {
namespace ytsr {
interface Options {
safeSearch?: boolean;
Expand All @@ -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 {
Expand All @@ -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: {
Expand All @@ -37,7 +42,7 @@ declare module '@distube/ytsr' {
}

interface Playlist {
type: 'playlist';
type: "playlist";
id: string;
name: string;
url: string;
Expand All @@ -49,6 +54,7 @@ declare module '@distube/ytsr' {
ownerBadges: string[];
verified: boolean;
} | null;
publishedAt: string | null;
}

interface VideoResult {
Expand All @@ -65,7 +71,11 @@ declare module '@distube/ytsr' {
}

function ytsr(id: string): Promise<ytsr.VideoResult>;
function ytsr(id: string, options: ytsr.Options & { type: 'playlist' }): Promise<ytsr.PlaylistResult>;
function ytsr(id: string, options: ytsr.Options & { type: "playlist" }): Promise<ytsr.PlaylistResult>;
function ytsr(
id: string,
options: ytsr.Options & { type: "video" | "playlist" }
): Promise<ytsr.VideoResult | ytsr.PlaylistResult>;
function ytsr(id: string, options: ytsr.Options): Promise<ytsr.VideoResult>;

export = ytsr;
Expand Down

0 comments on commit 324a98a

Please sign in to comment.