diff --git a/src/renderer/helpers/api/local.js b/src/renderer/helpers/api/local.js index ba9a260f7c3cf..1a619448895b6 100644 --- a/src/renderer/helpers/api/local.js +++ b/src/renderer/helpers/api/local.js @@ -744,20 +744,38 @@ export function parseLocalChannelVideos(videos, channelId, channelName) { } /** - * @param {import('youtubei.js').YTNodes.ReelItem[]} shorts + * @param {(import('youtubei.js').YTNodes.ReelItem | import('youtubei.js').YTNodes.ShortsLockupView)[]} shorts * @param {string} channelId * @param {string} channelName */ export function parseLocalChannelShorts(shorts, channelId, channelName) { return shorts.map(short => { - return { - type: 'video', - videoId: short.id, - title: short.title.text, - author: channelName, - authorId: channelId, - viewCount: short.views.isEmpty() ? null : parseLocalSubscriberCount(short.views.text), - lengthSeconds: '' + if (short.type === 'ReelItem') { + /** @type {import('youtubei.js').YTNodes.ReelItem} */ + const reelItem = short + + return { + type: 'video', + videoId: reelItem.id, + title: reelItem.title.text, + author: channelName, + authorId: channelId, + viewCount: reelItem.views.isEmpty() ? null : parseLocalSubscriberCount(reelItem.views.text), + lengthSeconds: '' + } + } else { + /** @type {import('youtubei.js').YTNodes.ShortsLockupView} */ + const shortsLockupView = short + + return { + type: 'video', + videoId: shortsLockupView.on_tap_endpoint.payload.videoId, + title: shortsLockupView.overlay_metadata.primary_text.text, + author: channelName, + authorId: channelId, + viewCount: shortsLockupView.overlay_metadata.secondary_text ? parseLocalSubscriberCount(shortsLockupView.overlay_metadata.secondary_text.text) : null, + lengthSeconds: '' + } } }) } @@ -869,7 +887,7 @@ function handleSearchResponse(response) { } /** - * @param {import('youtubei.js').YTNodes.PlaylistVideo|import('youtubei.js').YTNodes.ReelItem} video + * @param {import('youtubei.js').YTNodes.PlaylistVideo|import('youtubei.js').YTNodes.ReelItem|import('youtubei.js').YTNodes.ShortsLockupView} video */ export function parseLocalPlaylistVideo(video) { if (video.type === 'ReelItem') { @@ -883,6 +901,42 @@ export function parseLocalPlaylistVideo(video) { viewCount: parseLocalSubscriberCount(short.views.text), lengthSeconds: '' } + } else if (video.type === 'ShortsLockupView') { + /** @type {import('youtubei.js').YTNodes.ShortsLockupView} */ + const shortsLockupView = video + + let viewCount = null + + // the accessiblity text is the only place with the view count + if (shortsLockupView.accessibility_text) { + // the `.*\s+` at the start of the regex, ensures we match the last occurence + // just in case the video title also contains that pattern + const match = shortsLockupView.accessibility_text.match(/.*\s+(\d+(?:[,.]\d+)?\s?(?:[BKMbkm]|million)?|no)\s+views?/) + + if (match) { + const count = match[1] + + // as it's rare that a video has no views, + // checking the length allows us to avoid running toLowerCase unless we have to + if (count.length === 2 && count === 'no') { + viewCount = 0 + } else { + const views = parseLocalSubscriberCount(count) + + if (!isNaN(views)) { + viewCount = views + } + } + } + } + + return { + type: 'video', + videoId: shortsLockupView.on_tap_endpoint.payload.videoId, + title: shortsLockupView.overlay_metadata.primary_text.text, + viewCount, + lengthSeconds: '' + } } else { /** @type {import('youtubei.js').YTNodes.PlaylistVideo} */ const video_ = video @@ -1365,7 +1419,7 @@ export function filterLocalFormats(formats, allowAv1 = false) { * @param {string} text */ export function parseLocalSubscriberCount(text) { - const match = text.match(/(\d+)(?:[,.](\d+))?\s?([BKMbkm])\b/) + const match = text.match(/(\d+)(?:[,.](\d+))?\s?([BKMbkm]|million)\b/) if (match) { let multiplier = 0 @@ -1377,6 +1431,7 @@ export function parseLocalSubscriberCount(text) { break case 'M': case 'm': + case 'million': multiplier = 6 break case 'B': diff --git a/src/renderer/views/Playlist/Playlist.js b/src/renderer/views/Playlist/Playlist.js index 7e838e955c0b0..91be3039be0e9 100644 --- a/src/renderer/views/Playlist/Playlist.js +++ b/src/renderer/views/Playlist/Playlist.js @@ -305,9 +305,11 @@ export default defineComponent({ channelName = subtitle.substring(0, index).trim() } + const playlistItems = result.items.map(parseLocalPlaylistVideo) + this.playlistTitle = result.info.title this.playlistDescription = result.info.description ?? '' - this.firstVideoId = result.items[0].id + this.firstVideoId = playlistItems[0].videoId this.playlistThumbnail = result.info.thumbnails[0].url this.viewCount = result.info.views.toLowerCase() === 'no views' ? 0 : extractNumberFromString(result.info.views) this.videoCount = extractNumberFromString(result.info.total_items) @@ -323,7 +325,7 @@ export default defineComponent({ channelId: this.channelId }) - this.playlistItems = result.items.map(parseLocalPlaylistVideo) + this.playlistItems = playlistItems let shouldGetNextPage = false if (result.has_continuation) {