|
1 | | -import {inject, injectable} from 'inversify'; |
| 1 | +import {inject, injectable, optional} from 'inversify'; |
2 | 2 | import * as spotifyURI from 'spotify-uri'; |
3 | 3 | import {SongMetadata, QueuedPlaylist, MediaSource} from './player.js'; |
4 | 4 | import {TYPES} from '../types.js'; |
5 | 5 | import ffmpeg from 'fluent-ffmpeg'; |
6 | 6 | import YoutubeAPI from './youtube-api.js'; |
7 | 7 | import SpotifyAPI, {SpotifyTrack} from './spotify-api.js'; |
| 8 | +import {URL} from 'node:url'; |
8 | 9 |
|
9 | 10 | @injectable() |
10 | 11 | export default class { |
11 | 12 | private readonly youtubeAPI: YoutubeAPI; |
12 | | - private readonly spotifyAPI: SpotifyAPI; |
| 13 | + private readonly spotifyAPI?: SpotifyAPI; |
13 | 14 |
|
14 | | - constructor(@inject(TYPES.Services.YoutubeAPI) youtubeAPI: YoutubeAPI, @inject(TYPES.Services.SpotifyAPI) spotifyAPI: SpotifyAPI) { |
| 15 | + constructor(@inject(TYPES.Services.YoutubeAPI) youtubeAPI: YoutubeAPI, @inject(TYPES.Services.SpotifyAPI) @optional() spotifyAPI?: SpotifyAPI) { |
15 | 16 | this.youtubeAPI = youtubeAPI; |
16 | 17 | this.spotifyAPI = spotifyAPI; |
17 | 18 | } |
18 | 19 |
|
19 | | - async youtubeVideoSearch(query: string, shouldSplitChapters: boolean): Promise<SongMetadata[]> { |
| 20 | + async getSongs(query: string, playlistLimit: number, shouldSplitChapters: boolean): Promise<[SongMetadata[], string]> { |
| 21 | + const newSongs: SongMetadata[] = []; |
| 22 | + let extraMsg = ''; |
| 23 | + |
| 24 | + // Test if it's a complete URL |
| 25 | + try { |
| 26 | + const url = new URL(query); |
| 27 | + |
| 28 | + const YOUTUBE_HOSTS = [ |
| 29 | + 'www.youtube.com', |
| 30 | + 'youtu.be', |
| 31 | + 'youtube.com', |
| 32 | + 'music.youtube.com', |
| 33 | + 'www.music.youtube.com', |
| 34 | + ]; |
| 35 | + |
| 36 | + if (YOUTUBE_HOSTS.includes(url.host)) { |
| 37 | + // YouTube source |
| 38 | + if (url.searchParams.get('list')) { |
| 39 | + // YouTube playlist |
| 40 | + newSongs.push(...await this.youtubePlaylist(url.searchParams.get('list')!, shouldSplitChapters)); |
| 41 | + } else { |
| 42 | + const songs = await this.youtubeVideo(url.href, shouldSplitChapters); |
| 43 | + |
| 44 | + if (songs) { |
| 45 | + newSongs.push(...songs); |
| 46 | + } else { |
| 47 | + throw new Error('that doesn\'t exist'); |
| 48 | + } |
| 49 | + } |
| 50 | + } else if (url.protocol === 'spotify:' || url.host === 'open.spotify.com') { |
| 51 | + if (this.spotifyAPI === undefined) { |
| 52 | + throw new Error('Spotify is not enabled!'); |
| 53 | + } |
| 54 | + |
| 55 | + const [convertedSongs, nSongsNotFound, totalSongs] = await this.spotifySource(query, playlistLimit, shouldSplitChapters); |
| 56 | + |
| 57 | + if (totalSongs > playlistLimit) { |
| 58 | + extraMsg = `a random sample of ${playlistLimit} songs was taken`; |
| 59 | + } |
| 60 | + |
| 61 | + if (totalSongs > playlistLimit && nSongsNotFound !== 0) { |
| 62 | + extraMsg += ' and '; |
| 63 | + } |
| 64 | + |
| 65 | + if (nSongsNotFound !== 0) { |
| 66 | + if (nSongsNotFound === 1) { |
| 67 | + extraMsg += '1 song was not found'; |
| 68 | + } else { |
| 69 | + extraMsg += `${nSongsNotFound.toString()} songs were not found`; |
| 70 | + } |
| 71 | + } |
| 72 | + |
| 73 | + newSongs.push(...convertedSongs); |
| 74 | + } else { |
| 75 | + const song = await this.httpLiveStream(query); |
| 76 | + |
| 77 | + if (song) { |
| 78 | + newSongs.push(song); |
| 79 | + } else { |
| 80 | + throw new Error('that doesn\'t exist'); |
| 81 | + } |
| 82 | + } |
| 83 | + } catch (err: any) { |
| 84 | + if (err instanceof Error && err.message === 'Spotify is not enabled!') { |
| 85 | + throw err; |
| 86 | + } |
| 87 | + |
| 88 | + // Not a URL, must search YouTube |
| 89 | + const songs = await this.youtubeVideoSearch(query, shouldSplitChapters); |
| 90 | + |
| 91 | + if (songs) { |
| 92 | + newSongs.push(...songs); |
| 93 | + } else { |
| 94 | + throw new Error('that doesn\'t exist'); |
| 95 | + } |
| 96 | + } |
| 97 | + |
| 98 | + return [newSongs, extraMsg]; |
| 99 | + } |
| 100 | + |
| 101 | + private async youtubeVideoSearch(query: string, shouldSplitChapters: boolean): Promise<SongMetadata[]> { |
20 | 102 | return this.youtubeAPI.search(query, shouldSplitChapters); |
21 | 103 | } |
22 | 104 |
|
23 | | - async youtubeVideo(url: string, shouldSplitChapters: boolean): Promise<SongMetadata[]> { |
| 105 | + private async youtubeVideo(url: string, shouldSplitChapters: boolean): Promise<SongMetadata[]> { |
24 | 106 | return this.youtubeAPI.getVideo(url, shouldSplitChapters); |
25 | 107 | } |
26 | 108 |
|
27 | | - async youtubePlaylist(listId: string, shouldSplitChapters: boolean): Promise<SongMetadata[]> { |
| 109 | + private async youtubePlaylist(listId: string, shouldSplitChapters: boolean): Promise<SongMetadata[]> { |
28 | 110 | return this.youtubeAPI.getPlaylist(listId, shouldSplitChapters); |
29 | 111 | } |
30 | 112 |
|
31 | | - async spotifySource(url: string, playlistLimit: number, shouldSplitChapters: boolean): Promise<[SongMetadata[], number, number]> { |
| 113 | + private async spotifySource(url: string, playlistLimit: number, shouldSplitChapters: boolean): Promise<[SongMetadata[], number, number]> { |
| 114 | + if (this.spotifyAPI === undefined) { |
| 115 | + return [[], 0, 0]; |
| 116 | + } |
| 117 | + |
32 | 118 | const parsed = spotifyURI.parse(url); |
33 | 119 |
|
34 | 120 | switch (parsed.type) { |
@@ -58,7 +144,7 @@ export default class { |
58 | 144 | } |
59 | 145 | } |
60 | 146 |
|
61 | | - async httpLiveStream(url: string): Promise<SongMetadata> { |
| 147 | + private async httpLiveStream(url: string): Promise<SongMetadata> { |
62 | 148 | return new Promise((resolve, reject) => { |
63 | 149 | ffmpeg(url).ffprobe((err, _) => { |
64 | 150 | if (err) { |
|
0 commit comments