From 4c81b67869dad7e09636cd3a562f2125e983af39 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 26 Jan 2025 00:01:52 +0000 Subject: [PATCH 1/8] Bump p-queue from 7.1.0 to 8.1.0 Bumps [p-queue](https://github.com/sindresorhus/p-queue) from 7.1.0 to 8.1.0. - [Release notes](https://github.com/sindresorhus/p-queue/releases) - [Commits](https://github.com/sindresorhus/p-queue/compare/v7.1.0...v8.1.0) --- updated-dependencies: - dependency-name: p-queue dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- package.json | 2 +- yarn.lock | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/package.json b/package.json index 7fc56b9..0e06fa4 100644 --- a/package.json +++ b/package.json @@ -114,7 +114,7 @@ "ora": "^8.1.0", "p-event": "^5.0.1", "p-limit": "^6.1.0", - "p-queue": "7.1.0", + "p-queue": "8.1.0", "p-retry": "6.2.0", "pagination.djs": "^4.0.10", "parse-duration": "1.0.2", diff --git a/yarn.lock b/yarn.lock index e56451e..5d48dbc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2024,10 +2024,10 @@ esutils@^2.0.2: resolved "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz" integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== -eventemitter3@^4.0.7: - version "4.0.7" - resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz" - integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== +eventemitter3@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.1.tgz#53f5ffd0a492ac800721bb42c66b841de96423c4" + integrity sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== execa@5.1.1: version "5.1.1" @@ -3837,13 +3837,13 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-queue@7.1.0: - version "7.1.0" - resolved "https://registry.npmjs.org/p-queue/-/p-queue-7.1.0.tgz" - integrity sha512-V+0vPJbhYkBqknPp0qnaz+dWcj8cNepfXZcsVIVEHPbFQXMPwrzCNIiM4FoxGtwHXtPzVCPHDvqCr1YrOJX2Gw== +p-queue@8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/p-queue/-/p-queue-8.1.0.tgz#d71929249868b10b16f885d8a82beeaf35d32279" + integrity sha512-mxLDbbGIBEXTJL0zEx8JIylaj3xQ7Z/7eEVjcF9fJX4DBiH9oqe+oahYnlKKxm0Ci9TlWTyhSHgygxMxjIB2jw== dependencies: - eventemitter3 "^4.0.7" - p-timeout "^5.0.0" + eventemitter3 "^5.0.1" + p-timeout "^6.1.2" p-retry@6.2.0: version "6.2.0" @@ -3854,16 +3854,16 @@ p-retry@6.2.0: is-network-error "^1.0.0" retry "^0.13.1" -p-timeout@^5.0.0: - version "5.1.0" - resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-5.1.0.tgz" - integrity sha512-auFDyzzzGZZZdHz3BtET9VEz0SE/uMEAx7uWfGPucfzEwwe/xH0iVeZibQmANYE/hp9T2+UUZT5m+BKyrDp3Ew== - p-timeout@^5.0.2: version "5.0.2" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-5.0.2.tgz" integrity sha512-sEmji9Yaq+Tw+STwsGAE56hf7gMy9p0tQfJojIAamB7WHJYJKf1qlsg9jqBWG8q9VCxKPhZaP/AcXwEoBcYQhQ== +p-timeout@^6.1.2: + version "6.1.4" + resolved "https://registry.yarnpkg.com/p-timeout/-/p-timeout-6.1.4.tgz#418e1f4dd833fa96a2e3f532547dd2abdb08dbc2" + integrity sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg== + pac-proxy-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-5.0.0.tgz" From 2aad2244d8a9233c7c804de966a5c86d04c3d4c2 Mon Sep 17 00:00:00 2001 From: Joe Howard Date: Sun, 26 Jan 2025 19:51:55 -0600 Subject: [PATCH 2/8] Add changes from #1197, #1192, #1139 to Changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index b70f5ef..ff49ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] +- Remove Spotify requirement +- Dependency update ## [2.10.0] - 2024-11-04 - New `/config set-reduce-vol-when-voice` command to automatically turn down the volume when people are speaking in the channel From b6f5b1f45abc1761564c01c32fbda44f9c27c0ba Mon Sep 17 00:00:00 2001 From: Joe Howard Date: Sun, 26 Jan 2025 20:03:21 -0600 Subject: [PATCH 3/8] chore: add type declaration to force linter to recognize non-void return --- src/services/youtube-api.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/youtube-api.ts b/src/services/youtube-api.ts index 216a2c0..6d8c798 100644 --- a/src/services/youtube-api.ts +++ b/src/services/youtube-api.ts @@ -74,7 +74,7 @@ export default class { } async search(query: string, shouldSplitChapters: boolean): Promise { - const {items} = await this.ytsrQueue.add(async () => this.cache.wrap( + const {items} = await this.ytsrQueue.add(async () => this.cache.wrap( ytsr, query, { From 8be58920ba9e057ccee57bd2a166727bd55301f3 Mon Sep 17 00:00:00 2001 From: Joe Howard Date: Sun, 26 Jan 2025 20:08:32 -0600 Subject: [PATCH 4/8] try casting type directly --- src/services/youtube-api.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/services/youtube-api.ts b/src/services/youtube-api.ts index 6d8c798..944d4d8 100644 --- a/src/services/youtube-api.ts +++ b/src/services/youtube-api.ts @@ -74,7 +74,7 @@ export default class { } async search(query: string, shouldSplitChapters: boolean): Promise { - const {items} = await this.ytsrQueue.add(async () => this.cache.wrap( + const {items}: ytsr.VideoResult | void = await this.ytsrQueue.add(async () => this.cache.wrap( ytsr, query, { @@ -85,6 +85,10 @@ export default class { }, )); + if (!items) { + return []; + } + let firstVideo: Video | undefined; for (const item of items) { From 0bc39279a511a6acc41fca2e753c8ecfa833f30a Mon Sep 17 00:00:00 2001 From: Joe Howard Date: Sun, 26 Jan 2025 20:21:44 -0600 Subject: [PATCH 5/8] remove items destruturing --- src/services/youtube-api.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/youtube-api.ts b/src/services/youtube-api.ts index 944d4d8..47b91d8 100644 --- a/src/services/youtube-api.ts +++ b/src/services/youtube-api.ts @@ -74,7 +74,7 @@ export default class { } async search(query: string, shouldSplitChapters: boolean): Promise { - const {items}: ytsr.VideoResult | void = await this.ytsrQueue.add(async () => this.cache.wrap( + const result = await this.ytsrQueue.add(async () => this.cache.wrap( ytsr, query, { @@ -85,13 +85,13 @@ export default class { }, )); - if (!items) { + if (result === undefined) { return []; } let firstVideo: Video | undefined; - for (const item of items) { + for (const item of result.items) { if (item.type === 'video') { firstVideo = item; break; From ebb5803653b7f43977e0094fda8b8319224ead87 Mon Sep 17 00:00:00 2001 From: Michael <13820335+shiftybitshiftr@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:13:20 -0800 Subject: [PATCH 6/8] Update CHANGELOG.md with YT changes --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff49ad6..7d66f07 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Remove Spotify requirement - Dependency update +- Improve YouTube API error handling ## [2.10.0] - 2024-11-04 - New `/config set-reduce-vol-when-voice` command to automatically turn down the volume when people are speaking in the channel From 0fcd6d0a40b64d5972367f17272258b5d5875910 Mon Sep 17 00:00:00 2001 From: Michael <13820335+shiftybitshiftr@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:25:28 -0800 Subject: [PATCH 7/8] Revert "Merge branch 'master' into dependencies/ytdl-core/upgrade/4-15-9" This reverts commit b1a3ae3aaa6e077f22cf8f0b9b8daf3bb2569115, reversing changes made to ebb5803653b7f43977e0094fda8b8319224ead87. --- src/services/youtube-api.ts | 434 ++++++++++++------------------------ 1 file changed, 141 insertions(+), 293 deletions(-) diff --git a/src/services/youtube-api.ts b/src/services/youtube-api.ts index a2affb0..47b91d8 100644 --- a/src/services/youtube-api.ts +++ b/src/services/youtube-api.ts @@ -1,10 +1,8 @@ import {inject, injectable} from 'inversify'; import {toSeconds, parse} from 'iso8601-duration'; -import got, {Got, HTTPError, RequestError} from 'got'; +import got, {Got} from 'got'; import ytsr, {Video} from '@distube/ytsr'; import PQueue from 'p-queue'; -import pRetry from 'p-retry'; -import crypto from 'crypto'; import {SongMetadata, QueuedPlaylist, MediaSource} from './player.js'; import {TYPES} from '../types.js'; import Config from './config.js'; @@ -12,19 +10,6 @@ import KeyValueCacheProvider from './key-value-cache.js'; import {ONE_HOUR_IN_SECONDS, ONE_MINUTE_IN_SECONDS} from '../utils/constants.js'; import {parseTime} from '../utils/time.js'; import getYouTubeID from 'get-youtube-id'; -import debug from '../utils/debug.js'; - -// Define structured error type for better error handling -interface YouTubeError extends Error { - code: 'QUOTA_EXCEEDED' | 'RATE_LIMITED' | 'NOT_FOUND' | 'NETWORK_ERROR' | 'UNKNOWN'; - status?: number; - retryable: boolean; -} - -const YOUTUBE_MAX_RETRY_COUNT = 3; -const YOUTUBE_BASE_RETRY_DELAY_MS = 1000; -const YOUTUBE_SEARCH_CONCURRENCY = 4; -const MAX_CACHE_KEY_LENGTH = 250; interface VideoDetailsResponse { id: string; @@ -74,13 +59,10 @@ export default class { private readonly ytsrQueue: PQueue; private readonly got: Got; - constructor( - @inject(TYPES.Config) config: Config, - @inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider, - ) { + constructor(@inject(TYPES.Config) config: Config, @inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider) { this.youtubeKey = config.YOUTUBE_API_KEY; this.cache = cache; - this.ytsrQueue = new PQueue({concurrency: YOUTUBE_SEARCH_CONCURRENCY}); + this.ytsrQueue = new PQueue({concurrency: 4}); this.got = got.extend({ prefixUrl: 'https://www.googleapis.com/youtube/v3/', @@ -91,283 +73,126 @@ export default class { }); } - public async search(query: string, shouldSplitChapters: boolean): Promise { - try { - const {items} = await this.ytsrQueue.add(async () => - this.cache.wrap( - ytsr, - query, - { - limit: 10, - }, - { - expiresIn: ONE_HOUR_IN_SECONDS, - key: this.createCacheKey('youtube-search', query), - }, - ), - ); + async search(query: string, shouldSplitChapters: boolean): Promise { + const result = await this.ytsrQueue.add(async () => this.cache.wrap( + ytsr, + query, + { + limit: 10, + }, + { + expiresIn: ONE_HOUR_IN_SECONDS, + }, + )); - let firstVideo: Video | undefined; - for (const item of items) { - if (item.type === 'video') { - firstVideo = item; - break; - } - } - - if (!firstVideo) { - throw new Error('No matching videos found.'); - } - - return await this.getVideo(firstVideo.url, shouldSplitChapters); - } catch (error) { - debug('YouTube search error:', error); - throw new Error('Failed to search YouTube. Please try again.'); + if (result === undefined) { + return []; } + + let firstVideo: Video | undefined; + + for (const item of result.items) { + if (item.type === 'video') { + firstVideo = item; + break; + } + } + + if (!firstVideo) { + return []; + } + + return this.getVideo(firstVideo.url, shouldSplitChapters); } - public async getVideo(url: string, shouldSplitChapters: boolean): Promise { - const videoId = getYouTubeID(url); - if (!videoId) { - throw new Error('Invalid YouTube URL.'); - } - - const result = await this.getVideosByID([videoId]); + async getVideo(url: string, shouldSplitChapters: boolean): Promise { + const result = await this.getVideosByID([String(getYouTubeID(url))]); const video = result.at(0); if (!video) { - throw new Error('Video could not be found or is unavailable.'); + throw new Error('Video could not be found.'); } return this.getMetadataFromVideo({video, shouldSplitChapters}); } - public async getPlaylist(listId: string, shouldSplitChapters: boolean): Promise { - try { - const playlistParams = { - searchParams: { - part: 'id, snippet, contentDetails', - id: listId, - }, - }; - - const {items: playlists} = await this.cache.wrap( - async () => this.executeYouTubeRequest<{items: PlaylistResponse[]}>('playlists', playlistParams), - playlistParams, - { - expiresIn: ONE_MINUTE_IN_SECONDS, - key: this.createCacheKey('youtube-playlist', listId), - }, - ); - - const playlist = playlists.at(0); - if (!playlist) { - throw new Error('Playlist could not be found.'); - } - - // Helper function to fetch a single page of playlist items - const fetchPlaylistPage = async (token?: string) => { - const playlistItemsParams = { - searchParams: { - part: 'id, contentDetails', - playlistId: listId, - maxResults: '50', - pageToken: token, - }, - }; - - return this.cache.wrap( - async () => this.executeYouTubeRequest('playlistItems', playlistItemsParams), - playlistItemsParams, - { - expiresIn: ONE_MINUTE_IN_SECONDS, - key: this.createCacheKey('youtube-playlist-items', `${listId}-${token ?? 'initial'}`), - }, - ); - }; - - // Recursively fetch all playlist pages - const fetchAllPages = async (token?: string): Promise => { - const {items, nextPageToken} = await fetchPlaylistPage(token); - if (!nextPageToken || items.length >= playlist.contentDetails.itemCount) { - return items; - } - - const nextItems = await fetchAllPages(nextPageToken); - return [...items, ...nextItems]; - }; - - const playlistVideos = await fetchAllPages(); - - const videoDetailPromises = playlistVideos.map(async item => - this.getVideosByID([item.contentDetails.videoId]), - ); - - const videoDetailChunks = await Promise.all(videoDetailPromises); - const videoDetails = videoDetailChunks.flat(); - - const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id}; - const songsToReturn: SongMetadata[] = []; - - for (const video of playlistVideos) { - try { - const videoDetail = videoDetails.find(i => i.id === video.contentDetails.videoId); - if (videoDetail) { - songsToReturn.push( - ...this.getMetadataFromVideo({ - video: videoDetail, - queuedPlaylist, - shouldSplitChapters, - }), - ); - } - } catch (error) { - debug(`Skipping unavailable video in playlist: ${video.contentDetails.videoId}`); - } - } - - if (songsToReturn.length === 0) { - throw new Error('No playable videos found in this playlist.'); - } - - return songsToReturn; - } catch (error) { - debug('Playlist processing error:', error); - - if (error instanceof Error) { - throw error; - } - - throw new Error('Failed to process playlist. Please try again.'); - } - } - - private createYouTubeError(message: string, code: YouTubeError['code'], status?: number): YouTubeError { - const error = new Error(message) as YouTubeError; - error.code = code; - error.status = status; - error.retryable = code === 'NETWORK_ERROR' || (status ? status >= 500 : false); - return error; - } - - private createCacheKey(prefix: string, key: string): string { - const fullKey = `${prefix}-${key}`; - if (fullKey.length <= MAX_CACHE_KEY_LENGTH) { - return fullKey; - } - - const hash = crypto.createHash('sha1').update(key).digest('hex'); - return `${prefix}-${key.slice(0, MAX_CACHE_KEY_LENGTH - prefix.length - 41)}-${hash}`; - } - - private async executeYouTubeRequest(endpoint: string, params: any): Promise { - return pRetry( - async () => { - try { - const response = (await this.got(endpoint, params).json()) as T; - - if (!response) { - throw this.createYouTubeError('Empty response from YouTube API', 'NETWORK_ERROR'); - } - - return response; - } catch (error) { - if (error instanceof HTTPError) { - const status = error.response.statusCode; - - switch (status) { - case 403: - throw this.createYouTubeError( - 'YouTube API quota exceeded. Please try again later.', - 'QUOTA_EXCEEDED', - status, - ); - case 429: - throw this.createYouTubeError( - 'YouTube API rate limit reached. Please try again later.', - 'RATE_LIMITED', - status, - ); - case 404: - throw this.createYouTubeError('Resource not found on YouTube.', 'NOT_FOUND', status); - default: - if (status >= 500) { - throw this.createYouTubeError('YouTube API is temporarily unavailable.', 'NETWORK_ERROR', status); - } - - throw this.createYouTubeError('YouTube API request failed.', 'UNKNOWN', status); - } - } - - if (error instanceof RequestError && error.code === 'ETIMEDOUT') { - throw this.createYouTubeError('YouTube API request timed out.', 'NETWORK_ERROR'); - } - - throw error; - } - }, - { - retries: YOUTUBE_MAX_RETRY_COUNT, - minTimeout: YOUTUBE_BASE_RETRY_DELAY_MS, - factor: 2, - randomize: true, - onFailedAttempt: error => { - const youTubeError - = error.message && typeof error.message === 'object' && 'code' in error.message - ? (error.message as YouTubeError) - : null; - - debug( - [ - `YouTube API request failed (attempt ${error.attemptNumber}/${YOUTUBE_MAX_RETRY_COUNT + 1})`, - `Error code: ${youTubeError?.code ?? 'UNKNOWN'}`, - `Status: ${youTubeError?.status ?? 'N/A'}`, - `Message: ${error.message}`, - `Retries left: ${error.retriesLeft}`, - ].join('\n'), - ); - - if (youTubeError && !youTubeError.retryable) { - throw error; - } - }, - }, - ); - } - - private async getVideosByID(videoIDs: string[]): Promise { - if (videoIDs.length === 0) { - return []; - } - - const params = { + async getPlaylist(listId: string, shouldSplitChapters: boolean): Promise { + const playlistParams = { searchParams: { part: 'id, snippet, contentDetails', - id: videoIDs.join(','), + id: listId, }, }; + const {items: playlists} = await this.cache.wrap( + async () => this.got('playlists', playlistParams).json() as Promise<{items: PlaylistResponse[]}>, + playlistParams, + { + expiresIn: ONE_MINUTE_IN_SECONDS, + }, + ); - try { - const {items: videos} = await this.cache.wrap( - async () => this.executeYouTubeRequest<{items: VideoDetailsResponse[]}>('videos', params), - params, + const playlist = playlists.at(0)!; + + if (!playlist) { + throw new Error('Playlist could not be found.'); + } + + const playlistVideos: PlaylistItem[] = []; + const videoDetailsPromises: Array> = []; + const videoDetails: VideoDetailsResponse[] = []; + + let nextToken: string | undefined; + + while (playlistVideos.length < playlist.contentDetails.itemCount) { + const playlistItemsParams = { + searchParams: { + part: 'id, contentDetails', + playlistId: listId, + maxResults: '50', + pageToken: nextToken, + }, + }; + + // eslint-disable-next-line no-await-in-loop + const {items, nextPageToken} = await this.cache.wrap( + async () => this.got('playlistItems', playlistItemsParams).json() as Promise, + playlistItemsParams, { - expiresIn: ONE_HOUR_IN_SECONDS, - key: this.createCacheKey('youtube-videos', videoIDs.join(',')), + expiresIn: ONE_MINUTE_IN_SECONDS, }, ); - return videos; - } catch (error) { - if (error instanceof Error && 'code' in error) { - const youTubeError = error as YouTubeError; - if (youTubeError.code === 'QUOTA_EXCEEDED' || youTubeError.code === 'RATE_LIMITED') { - throw error; - } - } + nextToken = nextPageToken; + playlistVideos.push(...items); - throw new Error('Failed to fetch video information. Please try again.'); + // Start fetching extra details about videos + // PlaylistItem misses some details, eg. if the video is a livestream + videoDetailsPromises.push((async () => { + const videoDetailItems = await this.getVideosByID(items.map(item => item.contentDetails.videoId)); + videoDetails.push(...videoDetailItems); + })()); } + + await Promise.all(videoDetailsPromises); + + const queuedPlaylist = {title: playlist.snippet.title, source: playlist.id}; + + const songsToReturn: SongMetadata[] = []; + + for (const video of playlistVideos) { + try { + songsToReturn.push(...this.getMetadataFromVideo({ + video: videoDetails.find((i: {id: string}) => i.id === video.contentDetails.videoId)!, + queuedPlaylist, + shouldSplitChapters, + })); + } catch (_: unknown) { + // Private and deleted videos are sometimes in playlists, duration of these + // is not returned and they should not be added to the queue. + } + } + + return songsToReturn; } private getMetadataFromVideo({ @@ -375,7 +200,7 @@ export default class { queuedPlaylist, shouldSplitChapters, }: { - video: VideoDetailsResponse; + video: VideoDetailsResponse; // | YoutubePlaylistItem; queuedPlaylist?: QueuedPlaylist; shouldSplitChapters?: boolean; }): SongMetadata[] { @@ -396,22 +221,26 @@ export default class { } const chapters = this.parseChaptersFromDescription(video.snippet.description, base.length); + if (!chapters) { return [base]; } - return Array.from(chapters.entries()).map(([label, {offset, length}]) => ({ - ...base, - offset, - length, - title: `${label} (${base.title})`, - })); + const tracks: SongMetadata[] = []; + + for (const [label, {offset, length}] of chapters) { + tracks.push({ + ...base, + offset, + length, + title: `${label} (${base.title})`, + }); + } + + return tracks; } - private parseChaptersFromDescription( - description: string, - videoDurationSeconds: number, - ) { + private parseChaptersFromDescription(description: string, videoDurationSeconds: number) { const map = new Map(); let foundFirstTimestamp = false; @@ -423,7 +252,6 @@ export default class { } if (!foundFirstTimestamp) { - // We expect the first timestamp to match something like "0:00" or "00:00" if (/0{1,2}:00/.test(timestamps[0][0])) { foundFirstTimestamp = true; } else { @@ -441,14 +269,34 @@ export default class { for (const [i, {name, offset}] of foundTimestamps.entries()) { map.set(name, { offset, - length: - i === foundTimestamps.length - 1 - ? videoDurationSeconds - offset - : foundTimestamps[i + 1].offset - offset, + length: i === foundTimestamps.length - 1 + ? videoDurationSeconds - offset + : foundTimestamps[i + 1].offset - offset, }); } - return map.size > 0 ? map : null; + if (!map.size) { + return null; + } + + return map; + } + + private async getVideosByID(videoIDs: string[]): Promise { + const p = { + searchParams: { + part: 'id, snippet, contentDetails', + id: videoIDs.join(','), + }, + }; + + const {items: videos} = await this.cache.wrap( + async () => this.got('videos', p).json() as Promise<{items: VideoDetailsResponse[]}>, + p, + { + expiresIn: ONE_HOUR_IN_SECONDS, + }, + ); + return videos; } } - From 5425b54e90aa9519c56f37b7b7a03b6b31f49617 Mon Sep 17 00:00:00 2001 From: Michael <13820335+shiftybitshiftr@users.noreply.github.com> Date: Mon, 27 Jan 2025 17:30:19 -0800 Subject: [PATCH 8/8] Revert "Update CHANGELOG.md with YT changes" This reverts commit ebb5803653b7f43977e0094fda8b8319224ead87. --- CHANGELOG.md | 1 - 1 file changed, 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7d66f07..ff49ad6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] - Remove Spotify requirement - Dependency update -- Improve YouTube API error handling ## [2.10.0] - 2024-11-04 - New `/config set-reduce-vol-when-voice` command to automatically turn down the volume when people are speaking in the channel