import { nextTick } from 'vue' import { useRouter } from 'vue-router' import { defineStore } from 'pinia' import type { Track, AlbumArtist, Album } from '../types' import { shuffled } from '../utils' import { SubsonicApi } from '../subsonicApi' import { useMainStore } from '../store/main' import { usePlayerStore } from '../store/player' export const useRadioStore = defineStore('radio', { actions: { // -------- HELPERS -------- takeUpToX(acc: Track[], next: Track[]): Track[] { if (acc.length >= 100) return acc const remaining = 100 - acc.length return acc.concat(next.slice(0, remaining)) }, pickRandom(array: T[], count: number): T[] { const copy = [...array] const result: T[] = [] while (result.length < count && copy.length) { const i = Math.floor(Math.random() * copy.length) const pick = copy.splice(i, 1)[0] if (!!pick) result.push(pick) } return result }, // -------- CORE -------- async playRandomOrTracks(opts: { params?: Parameters[0]; tracks?: Track[] }) { const mainStore = useMainStore(), playerStore = usePlayerStore(), router = useRouter() let shouldRoute = false mainStore.showLoader() try { let tracks: Track[] = [] if (opts.params) { tracks = await this.subsonicApi.getRandomTracks(opts.params) } else if (opts.tracks) { tracks = opts.tracks } if (!tracks?.length) return const randomized = shuffled(tracks) await playerStore.playNow(randomized) shouldRoute = true } finally { mainStore.hideLoader() if (shouldRoute) router.push({ name: 'queue' }) } }, async continueFromTrack(track: Track) { if (!track) return // Prefer explicit genre if present let genreName: string | undefined = (track as any).genre if (!genreName && track.albumId) { try { const album = await this.subsonicApi.getAlbumDetails(track.albumId) genreName = album.genres?.[0]?.name } catch (err) { console.warn('Radio: failed to resolve album genre', err) } } if (!genreName) { console.warn('Radio: no genre found, cannot continue') return } await this.playRandomOrTracks({ params: { genre: genreName, size: 100 } }) }, // -------- RADIOS -------- async shuffleGenre(genreId: string) { await this.playRandomOrTracks({ params: { genre: genreId, size: 100 } }) }, async shuffleMood(genreName: string) { await this.playRandomOrTracks({ params: { genre: genreName, size: 100 } }) }, async luckyRadio() { await this.playRandomOrTracks({ params: { size: 100 } }) }, async shuffleRecentlyPlayed() { const mainStore = useMainStore() mainStore.showLoader() try { const albums: Album[] = this.pickRandom(await this.subsonicApi.getAlbums('recently-played', 50), 10) let tracks: Track[] = [] for (const album of albums) { const fullAlbum = await this.subsonicApi.getAlbumDetails(album.id) if (fullAlbum.tracks?.length) { tracks = this.takeUpToX(tracks, fullAlbum.tracks) if (tracks.length >= 100) break } } if (tracks.length) { await this.playRandomOrTracks({ tracks }) } } finally { mainStore.hideLoader() } }, async shuffleRecentlyAdded() { const mainStore = useMainStore() mainStore.showLoader() try { const albums: Album[] = this.pickRandom(await this.subsonicApi.getAlbums('recently-added', 50), 10) let tracks: Track[] = [] for (const album of albums) { const fullAlbum = await this.subsonicApi.getAlbumDetails(album.id) if (fullAlbum.tracks?.length) { tracks = this.takeUpToX(tracks, fullAlbum.tracks) if (tracks.length >= 100) break } } if (tracks.length) { await this.playRandomOrTracks({ tracks }) } } finally { mainStore.hideLoader() } }, async shuffleMostPlayed() { const mainStore = useMainStore() mainStore.showLoader() try { const albums: Album[] = this.pickRandom(await this.subsonicApi.getAlbums('most-played', 50), 10) let tracks: Track[] = [] for (const album of albums) { const fullAlbum = await this.subsonicApi.getAlbumDetails(album.id) if (fullAlbum.tracks?.length) { tracks = this.takeUpToX(tracks, fullAlbum.tracks) if (tracks.length >= 100) break } } if (tracks.length) { await this.playRandomOrTracks({ tracks }) } } finally { mainStore.hideLoader() } }, async shuffleFavouriteAlbums() { const mainStore = useMainStore() mainStore.showLoader() try { const favourites = await this.subsonicApi.getFavourites() const favouriteAlbums = this.pickRandom(favourites.albums, 10) if (!favouriteAlbums.length) return let tracks: Track[] = [] for (const album of favouriteAlbums as { id: string }[]) { const fullAlbum = await this.subsonicApi.getAlbumDetails(album.id) if (fullAlbum.tracks?.length) { tracks = this.takeUpToX(tracks, fullAlbum.tracks) if (tracks.length >= 100) break } } if (tracks.length) { await this.playRandomOrTracks({ tracks }) } } finally { mainStore.hideLoader() } }, async shuffleFavouriteArtists() { const mainStore = useMainStore() mainStore.showLoader() try { const favourites = await this.subsonicApi.getFavourites() const randomArtists = this.pickRandom(favourites.artists, 10) if (!randomArtists.length) return let tracks: Track[] = [] for (const artist of randomArtists as { id: string }[]) { let artistTracks: Track[] = [] for await (const batch of this.subsonicApi.getTracksByArtist(artist.id)) { artistTracks = artistTracks.concat(batch) if (artistTracks.length >= 20) break } tracks = tracks.concat(artistTracks.slice(0, 20)) if (tracks.length >= 100) break } if (tracks.length) { await this.playRandomOrTracks({ tracks }) } } finally { mainStore.hideLoader() } }, async shufflePlaylists() { const mainStore = useMainStore() mainStore.showLoader() try { const playlists = (await this.subsonicApi.getPlaylists()).slice(0, 5) let allTracks: Track[] = [] for (const p of playlists) { const full = await this.subsonicApi.getPlaylist(p.id) if (full.tracks?.length) { allTracks = this.takeUpToX(allTracks, full.tracks) if (allTracks.length >= 100) break } } if (allTracks.length) { await this.playRandomOrTracks({ tracks: allTracks }) } } finally { mainStore.hideLoader() } }, async shuffleArtist(artistId: string) { const mainStore = useMainStore() mainStore.showLoader() try { let tracks: Track[] = [] for await (const batch of this.subsonicApi.getTracksByArtist(artistId)) { tracks = this.takeUpToX(tracks, batch) if (tracks.length >= 100) break } await this.playRandomOrTracks({ tracks }) } finally { mainStore.hideLoader() } }, async radioArtist(artistId: string) { await this.playRandomOrTracks({ tracks: await this.subsonicApi.getSimilarTracksByArtist(artistId, 50) }) }, async radioAlbum(tracks: Track[], artists: AlbumArtist[]) { if (!tracks?.length || !artists?.length || !artists[0]?.id) return const playerStore = usePlayerStore() playerStore.setShuffle(false) const similarTracks = await this.subsonicApi.getSimilarTracksByArtist(artists[0].id, 50) if (similarTracks?.length) await this.playRandomOrTracks({ tracks: similarTracks }) }, async shuffleAlbum(tracks: Track[]) { if (!tracks?.length) return await this.playRandomOrTracks({ tracks: tracks.slice(0, 100) }) } } })