From 2398fad986e9a9dc8edb94642395e2203aae3780 Mon Sep 17 00:00:00 2001 From: Jimbo <17926797+Kapppa@users.noreply.github.com> Date: Sun, 25 May 2025 11:18:02 +0100 Subject: [PATCH 1/2] feat: Support SoundCloud modified: .env.example modified: package.json modified: src/commands/play.ts modified: src/inversify.config.ts modified: src/managers/player.ts modified: src/services/config.ts modified: src/services/get-songs.ts modified: src/services/player.ts new file: src/services/soundcloud-api.ts modified: src/services/third-party.ts modified: src/types.ts modified: src/utils/build-embed.ts modified: src/utils/get-youtube-and-spotify-suggestions-for.ts modified: yarn.lock --- .env.example | 4 + package.json | 1 + src/commands/play.ts | 6 +- src/inversify.config.ts | 5 +- src/managers/player.ts | 7 +- src/services/config.ts | 2 + src/services/get-songs.ts | 27 ++- src/services/player.ts | 14 +- src/services/soundcloud-api.ts | 155 ++++++++++++++++++ src/services/third-party.ts | 4 + src/types.ts | 1 + src/utils/build-embed.ts | 5 + ...get-youtube-and-spotify-suggestions-for.ts | 5 +- yarn.lock | 39 +++-- 14 files changed, 247 insertions(+), 28 deletions(-) create mode 100644 src/services/soundcloud-api.ts diff --git a/.env.example b/.env.example index cb4fc193a..753f073ef 100644 --- a/.env.example +++ b/.env.example @@ -4,6 +4,10 @@ YOUTUBE_API_KEY= SPOTIFY_CLIENT_ID= SPOTIFY_CLIENT_SECRET= +# See https://www.npmjs.com/package/soundcloud.ts for instructions to get this. +SOUNDCLOUD_CLIENT_ID= +SOUNDCLOUD_OAUTH_TOKEN= + ############ # Optional # ############ diff --git a/package.json b/package.json index deaa63718..4c5b7f761 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "postinstall-postinstall": "^2.1.0", "read-pkg": "7.1.0", "reflect-metadata": "^0.2.2", + "soundcloud.ts": "^0.6.5", "sponsorblock-api": "^0.2.4", "spotify-uri": "^3.0.2", "spotify-web-api-node": "^5.0.2", diff --git a/src/commands/play.ts b/src/commands/play.ts index c87ccd5a4..0ca9da1e9 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -6,7 +6,7 @@ import Spotify from 'spotify-web-api-node'; import Command from './index.js'; import {TYPES} from '../types.js'; import ThirdParty from '../services/third-party.js'; -import getYouTubeAndSpotifySuggestionsFor from '../utils/get-youtube-and-spotify-suggestions-for.js'; +import getSearchSuggestions from '../utils/get-youtube-and-spotify-suggestions-for.js'; import KeyValueCacheProvider from '../services/key-value-cache.js'; import {ONE_HOUR_IN_SECONDS} from '../utils/constants.js'; import AddQueryToQueue from '../services/add-query-to-queue.js'; @@ -28,7 +28,7 @@ export default class implements Command { const queryDescription = thirdParty === undefined ? 'YouTube URL or search query' - : 'YouTube URL, Spotify URL, or search query'; + : 'YouTube URL, Spotify URL, SoundCloud URL, or search query'; this.slashCommand = new SlashCommandBuilder() .setName('play') @@ -82,7 +82,7 @@ export default class implements Command { } catch {} const suggestions = await this.cache.wrap( - getYouTubeAndSpotifySuggestionsFor, + getSearchSuggestions, query, this.spotify, 10, diff --git a/src/inversify.config.ts b/src/inversify.config.ts index 8e621cbce..6da4bf23c 100644 --- a/src/inversify.config.ts +++ b/src/inversify.config.ts @@ -13,6 +13,7 @@ import AddQueryToQueue from './services/add-query-to-queue.js'; import GetSongs from './services/get-songs.js'; import YoutubeAPI from './services/youtube-api.js'; import SpotifyAPI from './services/spotify-api.js'; +import SoundCloudAPI from './services/soundcloud-api.js'; // Commands import Command from './commands/index.js'; @@ -42,6 +43,7 @@ import ThirdParty from './services/third-party.js'; import FileCacheProvider from './services/file-cache.js'; import KeyValueCacheProvider from './services/key-value-cache.js'; + const container = new Container(); // Intents @@ -64,13 +66,14 @@ container.bind(TYPES.Config).toConstantValue(new ConfigProvider()); container.bind(TYPES.Services.GetSongs).to(GetSongs).inSingletonScope(); container.bind(TYPES.Services.AddQueryToQueue).to(AddQueryToQueue).inSingletonScope(); container.bind(TYPES.Services.YoutubeAPI).to(YoutubeAPI).inSingletonScope(); +container.bind(TYPES.Services.SoundCloudAPI).to(SoundCloudAPI).inSingletonScope(); // SOUNDCLOUD // Only instanciate spotify dependencies if the Spotify client ID and secret are set const config = container.get(TYPES.Config); if (config.SPOTIFY_CLIENT_ID !== '' && config.SPOTIFY_CLIENT_SECRET !== '') { container.bind(TYPES.Services.SpotifyAPI).to(SpotifyAPI).inSingletonScope(); - container.bind(TYPES.ThirdParty).to(ThirdParty); } +container.bind(TYPES.ThirdParty).to(ThirdParty); // Commands [ diff --git a/src/managers/player.ts b/src/managers/player.ts index 420cf4889..61cccfba8 100644 --- a/src/managers/player.ts +++ b/src/managers/player.ts @@ -2,22 +2,25 @@ import {inject, injectable} from 'inversify'; import {TYPES} from '../types.js'; import Player from '../services/player.js'; import FileCacheProvider from '../services/file-cache.js'; +import ThirdParty from '../services/third-party.js'; @injectable() export default class { private readonly guildPlayers: Map; private readonly fileCache: FileCacheProvider; + private readonly thirdparty: ThirdParty; - constructor(@inject(TYPES.FileCache) fileCache: FileCacheProvider) { + constructor(@inject(TYPES.FileCache) fileCache: FileCacheProvider, @inject(TYPES.ThirdParty) thirdparty: ThirdParty) { this.guildPlayers = new Map(); this.fileCache = fileCache; + this.thirdparty = thirdparty; } get(guildId: string): Player { let player = this.guildPlayers.get(guildId); if (!player) { - player = new Player(this.fileCache, guildId); + player = new Player(this.fileCache, guildId, this.thirdparty.soundcloud); this.guildPlayers.set(guildId, player); } diff --git a/src/services/config.ts b/src/services/config.ts index 3941a3c74..b2c57dbdc 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -39,6 +39,8 @@ export default class Config { readonly YOUTUBE_API_KEY!: string; readonly SPOTIFY_CLIENT_ID!: string; readonly SPOTIFY_CLIENT_SECRET!: string; + readonly SOUNDCLOUD_CLIENT_ID!: string; + readonly SOUNDCLOUD_OAUTH_TOKEN!: string; readonly REGISTER_COMMANDS_ON_BOT!: boolean; readonly DATA_DIR!: string; readonly CACHE_DIR!: string; diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts index c48d87dc7..124f30adb 100644 --- a/src/services/get-songs.ts +++ b/src/services/get-songs.ts @@ -4,16 +4,19 @@ import {SongMetadata, QueuedPlaylist, MediaSource} from './player.js'; import {TYPES} from '../types.js'; import ffmpeg from 'fluent-ffmpeg'; import YoutubeAPI from './youtube-api.js'; +import SoundCloudAPI from './soundcloud-api.js'; import SpotifyAPI, {SpotifyTrack} from './spotify-api.js'; import {URL} from 'node:url'; @injectable() export default class { private readonly youtubeAPI: YoutubeAPI; + private readonly soundcloudApi: SoundCloudAPI; private readonly spotifyAPI?: SpotifyAPI; - constructor(@inject(TYPES.Services.YoutubeAPI) youtubeAPI: YoutubeAPI, @inject(TYPES.Services.SpotifyAPI) @optional() spotifyAPI?: SpotifyAPI) { + constructor(@inject(TYPES.Services.YoutubeAPI) youtubeAPI: YoutubeAPI, @inject(TYPES.Services.SoundCloudAPI) @optional() soundcloudApi: SoundCloudAPI, @inject(TYPES.Services.SpotifyAPI) @optional() spotifyAPI?: SpotifyAPI) { this.youtubeAPI = youtubeAPI; + this.soundcloudApi = soundcloudApi; this.spotifyAPI = spotifyAPI; } @@ -47,6 +50,14 @@ export default class { throw new Error('that doesn\'t exist'); } } + } else if (url.host === 'soundcloud.com') { + if (url.pathname.includes("/sets/")) { + const songs = await this.soundcloudPlaylist(url.href); + newSongs.push(...songs) + } else { + const songs = await this.soundcloudUrl(url.href); + newSongs.push(...songs) + } } else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') { if (this.spotifyAPI === undefined) { throw new Error('Spotify is not enabled!'); @@ -85,6 +96,8 @@ export default class { throw err; } + console.log(err) + // Not a URL, must search YouTube const songs = await this.youtubeVideoSearch(query, shouldSplitChapters); @@ -110,6 +123,18 @@ export default class { return this.youtubeAPI.getPlaylist(listId, shouldSplitChapters); } + private async soundcloudSearch(query: string): Promise { + return this.soundcloudApi.search(query); + } + + private async soundcloudUrl(url: string): Promise { + return this.soundcloudApi.get(url); + } + + private async soundcloudPlaylist(url: string): Promise { + return this.soundcloudApi.getPlaylist(url); + } + private async spotifySource(url: string, playlistLimit: number, shouldSplitChapters: boolean): Promise<[SongMetadata[], number, number]> { if (this.spotifyAPI === undefined) { return [[], 0, 0]; diff --git a/src/services/player.ts b/src/services/player.ts index 8a3dd4e0d..cc600ac92 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -21,10 +21,12 @@ import debug from '../utils/debug.js'; import {getGuildSettings} from '../utils/get-guild-settings.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; import {Setting} from '@prisma/client'; +import {Soundcloud} from 'soundcloud.ts'; export enum MediaSource { Youtube, HLS, + SoundCloud } export interface QueuedPlaylist { @@ -81,13 +83,15 @@ export default class { private positionInSeconds = 0; private readonly fileCache: FileCacheProvider; + private readonly soundcloud: Soundcloud; private disconnectTimer: NodeJS.Timeout | null = null; private readonly channelToSpeakingUsers: Map> = new Map(); - constructor(fileCache: FileCacheProvider, guildId: string) { + constructor(fileCache: FileCacheProvider, guildId: string, soundcloud: Soundcloud) { this.fileCache = fileCache; this.guildId = guildId; + this.soundcloud = soundcloud; } async connect(channel: VoiceChannel): Promise { @@ -505,6 +509,12 @@ export default class { return this.createReadStream({url: song.url, cacheKey: song.url}); } + if (song.source === MediaSource.SoundCloud) { + const stream = await this.soundcloud.util.streamTrack(song.url); + console.log(song.url) + return this.createReadStream({url: stream, cacheKey: song.url, cache: song.length < 30*60}) + } + let ffmpegInput: string | null; const ffmpegInputOptions: string[] = []; let shouldCacheVideo = false; @@ -665,7 +675,7 @@ export default class { } } - private async createReadStream(options: {url: string; cacheKey: string; ffmpegInputOptions?: string[]; cache?: boolean; volumeAdjustment?: string}): Promise { + private async createReadStream(options: {url: string | ReadableStream; cacheKey: string; ffmpegInputOptions?: string[]; cache?: boolean; volumeAdjustment?: string}): Promise { return new Promise((resolve, reject) => { const capacitor = new WriteStream(); diff --git a/src/services/soundcloud-api.ts b/src/services/soundcloud-api.ts new file mode 100644 index 000000000..0c6661e22 --- /dev/null +++ b/src/services/soundcloud-api.ts @@ -0,0 +1,155 @@ +import {inject, injectable} from 'inversify'; +import PQueue from 'p-queue'; +import Soundcloud, {SoundcloudTrack, SoundcloudTrackSearch} from 'soundcloud.ts'; +import {SongMetadata, QueuedPlaylist, MediaSource} from './player.js'; +import {TYPES} from '../types.js'; +import KeyValueCacheProvider from './key-value-cache.js'; +import {ONE_HOUR_IN_SECONDS, ONE_MINUTE_IN_SECONDS} from '../utils/constants.js'; +import ThirdParty from './third-party.js'; + +@injectable() +export default class { + private readonly cache: KeyValueCacheProvider; + private readonly soundcloud: Soundcloud; + private readonly scsrQueue: PQueue; + + constructor(@inject(TYPES.KeyValueCache) cache: KeyValueCacheProvider, @inject(TYPES.ThirdParty) thirdParty: ThirdParty) { + this.soundcloud = thirdParty.soundcloud; + this.cache = cache; + this.scsrQueue = new PQueue({concurrency: 4}); + } + + async search(query: string): Promise { + const items = await this.scsrQueue.add(async () => this.cache.wrap( + async () => this.soundcloud.tracks.search({q: query}), + { + limit: 10, + }, + { + expiresIn: ONE_HOUR_IN_SECONDS, + }, + )) as SoundcloudTrackSearch; + + const track = items.collection.at(0); + + if (!track) { + throw new Error('Track could not be found.'); + } + + return this.getMetadataFromVideo({track}); + } + + async get(query: string): Promise { + const track = await this.scsrQueue.add(async () => this.cache.wrap( + async () => this.soundcloud.tracks.get(query), + // {q: query}, + { + limit: 10, + }, + { + expiresIn: ONE_HOUR_IN_SECONDS, + }, + )); + + if (!track) { + throw new Error('Track could not be found.'); + } + + return this.getMetadataFromVideo({track}); + } + + async getPlaylist(listId: string): Promise { + const playlist = await this.cache.wrap( + async () => this.soundcloud.playlists.get(listId), + { + key: listId, + expiresIn: ONE_MINUTE_IN_SECONDS, + }, + ); + + if (!playlist) { + throw new Error('Playlist could not be found.'); + } + + const queuedPlaylist = {title: playlist.title, source: playlist.id.toString()}; + + const songsToReturn: SongMetadata[] = []; + + for (const track of playlist.tracks) { + try { + songsToReturn.push(...this.getMetadataFromVideo({ + track, + queuedPlaylist, + })); + } 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; + } + + // not fully supported yet. + async getArtist(userName: string): Promise { + const tracks = await this.cache.wrap( + async () => this.soundcloud.users.tracks(userName), + { + key: userName + 'tracks', + expiresIn: ONE_MINUTE_IN_SECONDS, + }, + ); + + const user = await this.cache.wrap( + async () => this.soundcloud.users.get(userName), + { + key: userName + 'user', + expiresIn: ONE_MINUTE_IN_SECONDS, + }, + ); + + if (!tracks) { + throw new Error('Playlist could not be found.'); + } + + const queuedPlaylist = {title: user.username, source: user.id.toString()}; + + const songsToReturn: SongMetadata[] = []; + + for (const track of tracks) { + try { + songsToReturn.push(...this.getMetadataFromVideo({ + track, + queuedPlaylist, + })); + } 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({ + track, + queuedPlaylist, + }: { + track: SoundcloudTrack; + queuedPlaylist?: QueuedPlaylist; + }): SongMetadata[] { + const base: SongMetadata = { + source: MediaSource.SoundCloud, + title: track.title, + artist: track.user.username, + length: track.duration / 1000, + offset: 0, + url: track.permalink_url, + playlist: queuedPlaylist ?? null, + isLive: false, + thumbnailUrl: track.artwork_url, + }; + + return [base]; + } +} \ No newline at end of file diff --git a/src/services/third-party.ts b/src/services/third-party.ts index da20f3546..41eb2d935 100644 --- a/src/services/third-party.ts +++ b/src/services/third-party.ts @@ -1,5 +1,6 @@ import {inject, injectable} from 'inversify'; import SpotifyWebApi from 'spotify-web-api-node'; +import {Soundcloud} from 'soundcloud.ts'; import pRetry from 'p-retry'; import {TYPES} from '../types.js'; import Config from './config.js'; @@ -7,6 +8,7 @@ import Config from './config.js'; @injectable() export default class ThirdParty { readonly spotify: SpotifyWebApi; + readonly soundcloud: Soundcloud; private spotifyTokenTimerId?: NodeJS.Timeout; @@ -16,6 +18,8 @@ export default class ThirdParty { clientSecret: config.SPOTIFY_CLIENT_SECRET, }); + this.soundcloud = new Soundcloud(config.SOUNDCLOUD_CLIENT_ID, config.SOUNDCLOUD_OAUTH_TOKEN) + void this.refreshSpotifyToken(); } diff --git a/src/types.ts b/src/types.ts index 3b64ea1d6..b07d5172f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -14,5 +14,6 @@ export const TYPES = { GetSongs: Symbol('GetSongs'), YoutubeAPI: Symbol('YoutubeAPI'), SpotifyAPI: Symbol('SpotifyAPI'), + SoundCloudAPI: Symbol('SoundCloudAPI'), }, }; diff --git a/src/utils/build-embed.ts b/src/utils/build-embed.ts index 23db0b9cc..214d4cb6a 100644 --- a/src/utils/build-embed.ts +++ b/src/utils/build-embed.ts @@ -19,6 +19,11 @@ const getSongTitle = ({title, url, offset, source}: QueuedSong, shouldTruncate = const cleanSongTitle = title.replace(/\[.*\]/, '').trim(); const songTitle = shouldTruncate ? truncate(cleanSongTitle, getMaxSongTitleLength(cleanSongTitle)) : cleanSongTitle; + + if (source === MediaSource.SoundCloud) { + return `[${songTitle}](${url})`; + } + const youtubeId = url.length === 11 ? url : getYouTubeID(url) ?? ''; return `[${songTitle}](https://www.youtube.com/watch?v=${youtubeId}${offset === 0 ? '' : '&t=' + String(offset)})`; diff --git a/src/utils/get-youtube-and-spotify-suggestions-for.ts b/src/utils/get-youtube-and-spotify-suggestions-for.ts index bd6f1c87b..a9a87f37a 100644 --- a/src/utils/get-youtube-and-spotify-suggestions-for.ts +++ b/src/utils/get-youtube-and-spotify-suggestions-for.ts @@ -14,7 +14,8 @@ const filterDuplicates = (items: T[]) => { return results; }; -const getYouTubeAndSpotifySuggestionsFor = async (query: string, spotify?: SpotifyWebApi, limit = 10): Promise => { +// TODO: add support for SoundCloud +const getSearchSuggestions = async (query: string, spotify?: SpotifyWebApi, limit = 10): Promise => { // Only search Spotify if enabled const spotifySuggestionPromise = spotify === undefined ? undefined @@ -74,4 +75,4 @@ const getYouTubeAndSpotifySuggestionsFor = async (query: string, spotify?: Spoti return suggestions; }; -export default getYouTubeAndSpotifySuggestionsFor; +export default getSearchSuggestions; diff --git a/yarn.lock b/yarn.lock index b113aa279..5b643454f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -162,18 +162,18 @@ tslib "^2.5.0" ws "^8.13.0" -"@distube/ytdl-core@^4.16.6": - version "4.16.6" - resolved "https://registry.yarnpkg.com/@distube/ytdl-core/-/ytdl-core-4.16.6.tgz#d433b0c8a8863c199f0f8be68b236e29479be6fe" - integrity sha512-+K88E2FRk/QPSC3l/U/qNq2LO0hPmFnDmK8CHK1Pzwh530aSacTLBhuZPgtoJAwNNncaTkmskP6lXoaw2eZjQw== +"@distube/ytdl-core@^4.16.10": + version "4.16.10" + resolved "https://registry.yarnpkg.com/@distube/ytdl-core/-/ytdl-core-4.16.10.tgz#42a3b2716cff13ca894b8ef4c322ab167992a004" + integrity sha512-KFKZtNlynOO0PYxelUF5h2bKyAU1d8fe6aZmo+gxWt7H2MQbd0bUeyV4y9iWhI57nukjkSXXQGB625CfhrVdGQ== dependencies: - http-cookie-agent "^6.0.8" + http-cookie-agent "^7.0.1" https-proxy-agent "^7.0.6" m3u8stream "^0.8.6" miniget "^4.2.3" sax "^1.4.1" - tough-cookie "^5.1.0" - undici "^7.3.0" + tough-cookie "^5.1.2" + undici "^7.8.0" "@distube/ytsr@^2.0.4": version "2.0.4" @@ -2835,10 +2835,10 @@ http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.1: resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz#abe02fcb2985460bf0323be664436ec3476a6d5a" integrity sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ== -http-cookie-agent@^6.0.8: - version "6.0.8" - resolved "https://registry.npmjs.org/http-cookie-agent/-/http-cookie-agent-6.0.8.tgz#f2635638f4172c7de0c482396ea7313e9731a62b" - integrity sha512-qnYh3yLSr2jBsTYkw11elq+T361uKAJaZ2dR4cfYZChw1dt9uL5t3zSUwehoqqVb4oldk1BpkXKm2oat8zV+oA== +http-cookie-agent@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/http-cookie-agent/-/http-cookie-agent-7.0.1.tgz#1e1e1569d2f833f905ab368d222ba0bdba51376b" + integrity sha512-lZHFZUdPTw64PdksQac5xbUd4NWjUbyDYnvR//2sbLpcC4UqEUW0x/6O+rDntVzJzJ07QvhtL5XZSC+c5EK+IQ== dependencies: agent-base "^7.1.3" @@ -4893,6 +4893,11 @@ socks@^2.3.3: ip-address "^9.0.5" smart-buffer "^4.2.0" +soundcloud.ts@^0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/soundcloud.ts/-/soundcloud.ts-0.6.5.tgz#3a54997eeb04fd85a36e2dac092995260f9f2645" + integrity sha512-X5juSiNpFHgmkytlULueAdJSTkY1KjcotOWiT++3IbihDVEjduMe/fs/PDx4X3uXt6tPmUyWjove510ZtXAWqA== + source-map-support@^0.5.21: version "0.5.21" resolved "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" @@ -5224,9 +5229,9 @@ token-types@^5.0.0-alpha.2, token-types@^5.0.1: "@tokenizer/token" "^0.3.0" ieee754 "^1.2.1" -tough-cookie@^5.1.0: +tough-cookie@^5.1.2: version "5.1.2" - resolved "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-5.1.2.tgz#66d774b4a1d9e12dc75089725af3ac75ec31bed7" integrity sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A== dependencies: tldts "^6.1.32" @@ -5407,10 +5412,10 @@ undici@^6.18.2: resolved "https://registry.npmjs.org/undici/-/undici-6.21.2.tgz#49c5884e8f9039c65a89ee9018ef3c8e2f1f4928" integrity sha512-uROZWze0R0itiAKVPsYhFov9LxrPMHLMEQFszeI2gCN6bnIIZ8twzBCJcN2LJrBBLfrP0t1FW0g+JmKVl8Vk1g== -undici@^7.3.0: - version "7.6.0" - resolved "https://registry.npmjs.org/undici/-/undici-7.6.0.tgz#a57f69b6710027b7ef7fed3654a6d05e788115e0" - integrity sha512-gaFsbThjrDGvAaD670r81RZro/s6H2PVZF640Qn0p5kZK+/rim7/mmyfp2W7VB5vOMaFM8vuFBJUaMlaZTYHlA== +undici@^7.8.0: + version "7.10.0" + resolved "https://registry.yarnpkg.com/undici/-/undici-7.10.0.tgz#8ae17a976acc6593b13c9ff3342840bea9b24670" + integrity sha512-u5otvFBOBZvmdjWLVW+5DAc9Nkq8f24g0O9oY7qw2JVIF1VocIFoyz9JFkuVOS2j41AufeO0xnlweJ2RLT8nGw== unique-string@^2.0.0: version "2.0.0" From 181c9a3a5a232a2b3882d9fcae9438ece125d894 Mon Sep 17 00:00:00 2001 From: Jimbo <17926797+Kapppa@users.noreply.github.com> Date: Fri, 30 May 2025 09:01:36 +0100 Subject: [PATCH 2/2] fix link error --- src/services/get-songs.ts | 2 -- src/services/player.ts | 5 ++--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/services/get-songs.ts b/src/services/get-songs.ts index 124f30adb..ebabe4c6d 100644 --- a/src/services/get-songs.ts +++ b/src/services/get-songs.ts @@ -96,8 +96,6 @@ export default class { throw err; } - console.log(err) - // Not a URL, must search YouTube const songs = await this.youtubeVideoSearch(query, shouldSplitChapters); diff --git a/src/services/player.ts b/src/services/player.ts index cc600ac92..d3d74755f 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -510,8 +510,7 @@ export default class { } if (song.source === MediaSource.SoundCloud) { - const stream = await this.soundcloud.util.streamTrack(song.url); - console.log(song.url) + const stream: string = await this.soundcloud.util.streamLink(song.url); return this.createReadStream({url: stream, cacheKey: song.url, cache: song.length < 30*60}) } @@ -675,7 +674,7 @@ export default class { } } - private async createReadStream(options: {url: string | ReadableStream; cacheKey: string; ffmpegInputOptions?: string[]; cache?: boolean; volumeAdjustment?: string}): Promise { + private async createReadStream(options: {url: string; cacheKey: string; ffmpegInputOptions?: string[]; cache?: boolean; volumeAdjustment?: string}): Promise { return new Promise((resolve, reject) => { const capacitor = new WriteStream();