From 9f2490228cce0ca7c8a268b8d5f70eb385a500fa Mon Sep 17 00:00:00 2001 From: AlexInCube Date: Mon, 12 Aug 2024 11:06:18 +0300 Subject: [PATCH] 3.6.0-dev-5 Soundcloud now works correctly again, but @distube/soundcloud code copied to bot repository with bug fix. package.json now contains only fixed versions of packages. --- package.json | 65 ++++---- pnpm-lock.yaml | 93 +++++------ src/audioplayer/LoadPlugins.ts | 2 +- src/audioplayer/plugins/soundcloud.ts | 224 ++++++++++++++++++++++++++ 4 files changed, 306 insertions(+), 78 deletions(-) create mode 100644 src/audioplayer/plugins/soundcloud.ts diff --git a/package.json b/package.json index d2203b6..3d0676a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "aicbot", - "version": "3.6.0-dev", + "version": "3.6.0", "description": "Discord Bot for playing music", "main": "build/main.js", "scripts": { @@ -21,46 +21,47 @@ "node": ">=20" }, "dependencies": { - "@discordjs/rest": "^2.3.0", - "@discordjs/voice": "^0.17.0", - "@distube/direct-link": "^1.0.1", - "@distube/file": "^1.0.1", - "@distube/soundcloud": "^2.0.3", - "@distube/spotify": "^2.0.2", - "@distube/youtube": "^1.0.4", - "@distube/yt-dlp": "^2.0.1", - "@distube/ytdl-core": "^4.14.4", - "@distube/ytsr": "^2.0.4", - "@eslint/js": "^9.9.0", + "@discordjs/rest": "2.3.0", + "@discordjs/voice": "0.17.0", + "@distube/direct-link": "1.0.1", + "@distube/file": "1.0.1", + "@distube/soundcloud": "2.0.3", + "@distube/spotify": "2.0.2", + "@distube/youtube": "1.0.4", + "@distube/yt-dlp": "2.0.1", + "@distube/ytdl-core": "4.14.4", + "@distube/ytsr": "2.0.4", "cross-env": "7.0.3", - "discord.js": "^14.15.3", - "distube": "^5.0.2", - "distube-apple-music": "^0.1.0", - "distube-yandex-music-plugin": "^1.0.5", - "dotenv": "^16.4.5", - "genius-lyrics": "^4.4.7", - "i18next": "^23.12.2", - "i18next-fs-backend": "^2.3.2", - "mongoose": "^8.5.2", - "node-cron": "^3.0.3", - "node-os-utils": "^1.3.7", - "opusscript": "^0.1.1", - "prism-media": "^1.3.5", + "discord.js": "14.15.3", + "distube": "5.0.2", + "distube-apple-music": "0.1.0", + "distube-yandex-music-plugin": "1.0.5", + "dotenv": "16.4.5", + "genius-lyrics": "4.4.7", + "i18next": "23.12.2", + "i18next-fs-backend": "2.3.2", + "mongoose": "8.5.2", + "node-cron": "3.0.3", + "node-os-utils": "1.3.7", + "opusscript": "0.1.1", + "prism-media": "1.3.5", "puppeteer": "22.13.1", - "puppeteer-extra": "^3.3.6", - "puppeteer-extra-plugin-stealth": "^2.11.2", - "sodium-native": "^4.1.1", - "typescript-eslint": "^7.18.0", - "uuid": "^10.0.0", - "zod": "^3.23.8" + "puppeteer-extra": "3.3.6", + "puppeteer-extra-plugin-stealth": "2.11.2", + "sodium-native": "4.1.1", + "soundcloud.ts": "0.5.5", + "uuid": "10.0.0", + "zod": "3.23.8" }, "devDependencies": { - "@types/node": "^20.14.15", + "@types/node": "^22.2.0", "@types/node-cron": "^3.0.11", "@types/node-os-utils": "^1.3.4", "@types/uuid": "^10.0.0", "@typescript-eslint/eslint-plugin": "^8.0.1", "@typescript-eslint/parser": "^8.0.1", + "@eslint/js": "^9.9.0", + "typescript-eslint": "7.18.0", "eslint": "^9.9.0", "eslint-config-prettier": "^9.1.0", "eslint-plugin-prettier": "^5.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 97660cc..0188dae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,105 +9,105 @@ importers: .: dependencies: '@discordjs/rest': - specifier: ^2.3.0 + specifier: 2.3.0 version: 2.3.0 '@discordjs/voice': - specifier: ^0.17.0 + specifier: 0.17.0 version: 0.17.0(opusscript@0.1.1) '@distube/direct-link': - specifier: ^1.0.1 + specifier: 1.0.1 version: 1.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/file': - specifier: ^1.0.1 + specifier: 1.0.1 version: 1.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/soundcloud': - specifier: ^2.0.3 + specifier: 2.0.3 version: 2.0.3(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/spotify': - specifier: ^2.0.2 + specifier: 2.0.2 version: 2.0.2(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/youtube': - specifier: ^1.0.4 + specifier: 1.0.4 version: 1.0.4(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/yt-dlp': - specifier: ^2.0.1 + specifier: 2.0.1 version: 2.0.1(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) '@distube/ytdl-core': - specifier: ^4.14.4 + specifier: 4.14.4 version: 4.14.4 '@distube/ytsr': - specifier: ^2.0.4 + specifier: 2.0.4 version: 2.0.4 - '@eslint/js': - specifier: ^9.9.0 - version: 9.9.0 cross-env: specifier: 7.0.3 version: 7.0.3 discord.js: - specifier: ^14.15.3 + specifier: 14.15.3 version: 14.15.3 distube: - specifier: ^5.0.2 + specifier: 5.0.2 version: 5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3) distube-apple-music: - specifier: ^0.1.0 + specifier: 0.1.0 version: 0.1.0(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) distube-yandex-music-plugin: - specifier: ^1.0.5 + specifier: 1.0.5 version: 1.0.5(distube@5.0.2(@discordjs/voice@0.17.0(opusscript@0.1.1))(discord.js@14.15.3)) dotenv: - specifier: ^16.4.5 + specifier: 16.4.5 version: 16.4.5 genius-lyrics: - specifier: ^4.4.7 + specifier: 4.4.7 version: 4.4.7 i18next: - specifier: ^23.12.2 + specifier: 23.12.2 version: 23.12.2 i18next-fs-backend: - specifier: ^2.3.2 + specifier: 2.3.2 version: 2.3.2 mongoose: - specifier: ^8.5.2 + specifier: 8.5.2 version: 8.5.2(socks@2.8.3) node-cron: - specifier: ^3.0.3 + specifier: 3.0.3 version: 3.0.3 node-os-utils: - specifier: ^1.3.7 + specifier: 1.3.7 version: 1.3.7 opusscript: - specifier: ^0.1.1 + specifier: 0.1.1 version: 0.1.1 prism-media: - specifier: ^1.3.5 + specifier: 1.3.5 version: 1.3.5(opusscript@0.1.1) puppeteer: specifier: 22.13.1 version: 22.13.1(typescript@5.5.4) puppeteer-extra: - specifier: ^3.3.6 + specifier: 3.3.6 version: 3.3.6(@types/puppeteer@7.0.4(typescript@5.5.4))(puppeteer-core@22.13.1)(puppeteer@22.13.1(typescript@5.5.4)) puppeteer-extra-plugin-stealth: - specifier: ^2.11.2 + specifier: 2.11.2 version: 2.11.2(puppeteer-extra@3.3.6(@types/puppeteer@7.0.4(typescript@5.5.4))(puppeteer-core@22.13.1)(puppeteer@22.13.1(typescript@5.5.4))) sodium-native: - specifier: ^4.1.1 + specifier: 4.1.1 version: 4.1.1 - typescript-eslint: - specifier: ^7.18.0 - version: 7.18.0(eslint@9.9.0)(typescript@5.5.4) + soundcloud.ts: + specifier: 0.5.5 + version: 0.5.5 uuid: - specifier: ^10.0.0 + specifier: 10.0.0 version: 10.0.0 zod: - specifier: ^3.23.8 + specifier: 3.23.8 version: 3.23.8 devDependencies: + '@eslint/js': + specifier: ^9.9.0 + version: 9.9.0 '@types/node': - specifier: ^20.14.15 - version: 20.14.15 + specifier: ^22.2.0 + version: 22.2.0 '@types/node-cron': specifier: ^3.0.11 version: 3.0.11 @@ -141,6 +141,9 @@ importers: typescript: specifier: ^5.5.4 version: 5.5.4 + typescript-eslint: + specifier: 7.18.0 + version: 7.18.0(eslint@9.9.0)(typescript@5.5.4) packages: @@ -323,8 +326,8 @@ packages: '@types/node-os-utils@1.3.4': resolution: {integrity: sha512-BCUYrbdoO4FUbx6MB9atLNFnkxdliFaxdiTJMIPPiecXIApc5zf4NIqV5G1jWv/ReZvtYyHLs40RkBjHX+vykA==} - '@types/node@20.14.15': - resolution: {integrity: sha512-Fz1xDMCF/B00/tYSVMlmK7hVeLh7jE5f3B7X1/hmV0MJBwE27KlS7EvD/Yp+z1lm8mVhwV5w+n8jOZG8AfTlKw==} + '@types/node@22.2.0': + resolution: {integrity: sha512-bm6EG6/pCpkxDf/0gDNDdtDILMOHgaQBVOJGdwsqClnxA3xL6jtMv76rLBc006RVMWbmaf0xbmom4Z/5o2nRkQ==} '@types/puppeteer@7.0.4': resolution: {integrity: sha512-ja78vquZc8y+GM2al07GZqWDKQskQXygCDiu0e3uO0DMRKqE0MjrFBFmTulfPYzLB6WnL7Kl2tFPy0WXSpPomg==} @@ -1743,8 +1746,8 @@ packages: unbzip2-stream@1.4.3: resolution: {integrity: sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==} - undici-types@5.26.5: - resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + undici-types@6.13.0: + resolution: {integrity: sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==} undici@5.28.4: resolution: {integrity: sha512-72RFADWFqKmUb2hmmvNODKL3p9hcB6Gt2DOQMis1SEBaV6a4MH8soBvzg+95CYhCKPFedut2JY9bMfrDl9D23g==} @@ -2093,9 +2096,9 @@ snapshots: '@types/node-os-utils@1.3.4': {} - '@types/node@20.14.15': + '@types/node@22.2.0': dependencies: - undici-types: 5.26.5 + undici-types: 6.13.0 '@types/puppeteer@7.0.4(typescript@5.5.4)': dependencies: @@ -2117,11 +2120,11 @@ snapshots: '@types/ws@8.5.12': dependencies: - '@types/node': 20.14.15 + '@types/node': 22.2.0 '@types/yauzl@2.10.3': dependencies: - '@types/node': 20.14.15 + '@types/node': 22.2.0 optional: true '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.9.0)(typescript@5.5.4))(eslint@9.9.0)(typescript@5.5.4)': @@ -3577,7 +3580,7 @@ snapshots: buffer: 5.7.1 through: 2.3.8 - undici-types@5.26.5: {} + undici-types@6.13.0: {} undici@5.28.4: dependencies: diff --git a/src/audioplayer/LoadPlugins.ts b/src/audioplayer/LoadPlugins.ts index 9689d49..0177c6b 100644 --- a/src/audioplayer/LoadPlugins.ts +++ b/src/audioplayer/LoadPlugins.ts @@ -8,7 +8,7 @@ import { DirectLinkPlugin } from '@distube/direct-link'; import { FilePlugin } from '@distube/file'; import { AppleMusicPlugin } from 'distube-apple-music'; import { YandexMusicPlugin } from 'distube-yandex-music-plugin'; -import { SoundCloudPlugin } from '@distube/soundcloud'; +import { SoundCloudPlugin } from './plugins/soundcloud.js'; import { getYoutubeCookie } from '../CookiesAutomation.js'; import Cron from 'node-cron'; diff --git a/src/audioplayer/plugins/soundcloud.ts b/src/audioplayer/plugins/soundcloud.ts new file mode 100644 index 0000000..8b3f783 --- /dev/null +++ b/src/audioplayer/plugins/soundcloud.ts @@ -0,0 +1,224 @@ +import { Soundcloud } from 'soundcloud.ts'; +import { DisTubeError, ExtractorPlugin, Playlist, Song, checkInvalidKey } from 'distube'; +import type { ResolveOptions } from 'distube'; +import type { SoundcloudPlaylistV2, SoundcloudTrackV2 } from 'soundcloud.ts'; + +type Falsy = undefined | null | false | 0 | ''; +const isTruthy = (x: T | Falsy): x is T => Boolean(x); +export enum SearchType { + Track = 'track', + Playlist = 'playlist' +} + +export interface SoundCloudPluginOptions { + clientId?: string; + oauthToken?: string; +} + +export class SoundCloudPlugin extends ExtractorPlugin { + soundcloud: Soundcloud; + constructor(options: SoundCloudPluginOptions = {}) { + super(); + if (typeof options !== 'object' || Array.isArray(options)) { + throw new DisTubeError( + 'INVALID_TYPE', + ['object', 'undefined'], + options, + 'SoundCloudPluginOptions' + ); + } + checkInvalidKey(options, ['clientId', 'oauthToken'], 'SoundCloudPluginOptions'); + if (options.clientId && typeof options.clientId !== 'string') { + throw new DisTubeError('INVALID_TYPE', 'string', options.clientId, 'clientId'); + } + if (options.oauthToken && typeof options.oauthToken !== 'string') { + throw new DisTubeError('INVALID_TYPE', 'string', options.oauthToken, 'oauthToken'); + } + + this.soundcloud = new Soundcloud(options.clientId, options.oauthToken); + } + search( + query: string, + type?: SearchType.Track, + limit?: number, + options?: ResolveOptions + ): Promise[]>; + search( + query: string, + type: SearchType.Playlist, + limit?: number, + options?: ResolveOptions + ): Promise[]>; + search( + query: string, + type?: SearchType, + limit?: number, + options?: ResolveOptions + ): Promise[] | Playlist[]>; + async search( + query: string, + type: SearchType = SearchType.Track, + limit = 10, + options: ResolveOptions = {} + ) { + if (typeof query !== 'string') { + throw new DisTubeError('INVALID_TYPE', 'string', query, 'query'); + } + if (!Object.values(SearchType).includes(type)) { + throw new DisTubeError('INVALID_TYPE', Object.values(SearchType), type, 'type'); + } + if (typeof limit !== 'number' || limit < 1 || !Number.isInteger(limit)) { + throw new DisTubeError('INVALID_TYPE', 'natural number', limit, 'limit'); + } + if (typeof options !== 'object' || Array.isArray(options)) { + throw new DisTubeError('INVALID_TYPE', 'object', options, 'ResolveOptions'); + } + + await this.soundcloud.api.getClientId().catch(() => { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NO_CLIENT_ID', + 'Cannot find SoundCloud client id automatically. Please provide a client id in the constructor.\nGuide: https://github.com/distubejs/soundcloud#documentation' + ); + }); + + switch (type) { + case SearchType.Track: { + const data = await this.soundcloud.tracks.searchV2({ q: query, limit }); + if (!data?.collection?.length) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NO_RESULT', + `Cannot find any "${query}" ${type} on SoundCloud!` + ); + } + return data.collection.map((t: any) => new SoundCloudSong(this, t, options)); + } + case SearchType.Playlist: { + const data = await this.soundcloud.playlists.searchV2({ q: query, limit }); + const playlists = data.collection; + return ( + await Promise.all( + playlists.map( + async (p: any) => + new SoundCloudPlaylist(this, await this.soundcloud.playlists.fetch(p), options) + ) + ) + ).filter(isTruthy); + } + default: + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_UNSUPPORTED_TYPE', + `${type} search is not supported!` + ); + } + } + + validate(url: string) { + return /^https?:\/\/(?:(?:www|m)\.)?soundcloud\.com\/(.*)$/.test(url); + } + + async resolve(url: string, options: ResolveOptions) { + await this.soundcloud.api.getClientId().catch(() => { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NO_CLIENT_ID', + 'Cannot find SoundCloud client id automatically. Please provide a client id in the constructor.\nGuide: https://github.com/distubejs/soundcloud#documentation' + ); + }); + const opt = { ...options, source: 'soundcloud' }; + url = url.replace(/:\/\/(m|www)\./g, '://'); + const data = await this.soundcloud.resolve.getV2(url, true).catch((e: { message: string }) => { + throw new DisTubeError('SOUNDCLOUD_PLUGIN_RESOLVE_ERROR', e.message); + }); + if (!data || !['track', 'playlist'].includes(data.kind)) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_NOT_SUPPORTED', + 'Only public tracks and playlists are supported.' + ); + } + + return data.kind === 'playlist' + ? new SoundCloudPlaylist(this, await this.soundcloud.playlists.fetch(data), opt) + : new SoundCloudSong(this, data, opt); + } + + async getRelatedSongs(song: SoundCloudSong) { + if (!song.url) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_INVALID_SONG', + 'Cannot get related songs from invalid song.' + ); + } + const related = await this.soundcloud.tracks.relatedV2(song.url, 10); + return related + .filter((t: { title: any }) => t.title) + .map((t: any) => new SoundCloudSong(this, t)); + } + + async getStreamURL(song: SoundCloudSong) { + if (!song.url) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_INVALID_SONG', + 'Cannot get stream url from invalid song.' + ); + } + const stream = await this.soundcloud.util.streamLink(song.url); + if (!stream) { + throw new DisTubeError( + 'SOUNDCLOUD_PLUGIN_RATE_LIMITED', + 'Reached SoundCloud rate limits\nSee more: https://developers.soundcloud.com/docs/api/rate-limits#play-requests' + ); + } + return stream; + } + + async searchSong(query: string, options: ResolveOptions) { + const songs = await this.search(query, SearchType.Track, 1, options); + return songs[0]; + } +} + +class SoundCloudSong extends Song { + constructor(plugin: SoundCloudPlugin, info: SoundcloudTrackV2, options: ResolveOptions = {}) { + super( + { + plugin, + source: 'soundcloud', + playFromSource: true, + id: info.id.toString(), + name: info.title, + url: info.permalink_url, + thumbnail: info.artwork_url, + duration: info.duration / 1000, + views: info.playback_count, + uploader: { + name: info.user?.username, + url: info.user?.permalink_url + }, + likes: info.likes_count, + reposts: info.reposts_count + }, + options + ); + } +} + +class SoundCloudPlaylist extends Playlist { + constructor( + plugin: SoundCloudPlugin, + info: SoundcloudPlaylistV2, + options: ResolveOptions = {} + ) { + super( + { + source: 'soundcloud', + id: info.id.toString(), + name: info.title, + url: info.permalink_url, + thumbnail: info.artwork_url ?? undefined, + songs: info.tracks.map((s: any) => new SoundCloudSong(plugin, s, options)) + }, + options + ); + } +} + +export default SoundCloudPlugin;