From b97590dd36d75bf68a5fa1eb456907b5960f173b Mon Sep 17 00:00:00 2001 From: simojenki Date: Mon, 17 Feb 2025 05:47:19 +0000 Subject: [PATCH] more --- src/music_library.ts | 23 ++++- src/subsonic.ts | 141 ++++++++++++++++++++++----- src/subsonic_music_library.ts | 114 +++------------------- tests/builders.ts | 21 ++-- tests/subsonic_music_library.test.ts | 46 +++------ 5 files changed, 181 insertions(+), 164 deletions(-) diff --git a/src/music_library.ts b/src/music_library.ts index 554ffae..36a86e1 100644 --- a/src/music_library.ts +++ b/src/music_library.ts @@ -60,7 +60,7 @@ export type Encoding = { mimeType: string } -export type Track = { +export type TrackSummary = { id: string; name: string; encoding: Encoding, @@ -68,9 +68,12 @@ export type Track = { number: number | undefined; genre: Genre | undefined; coverArt: BUrn | undefined; - album: AlbumSummary; artist: ArtistSummary; rating: Rating; +} + +export type Track = TrackSummary & { + album: AlbumSummary; }; export type RadioStation = { @@ -129,6 +132,18 @@ export const albumToAlbumSummary = (it: Album): AlbumSummary => ({ coverArt: it.coverArt }); +export const trackToTrackSummary = (it: Track): TrackSummary => ({ + id: it.id, + name: it.name, + encoding: it.encoding, + duration: it.duration, + number: it.number, + genre: it.genre, + coverArt: it.coverArt, + artist: it.artist, + rating: it.rating +}); + export const playlistToPlaylistSummary = (it: Playlist): PlaylistSummary => ({ id: it.id, name: it.name, @@ -199,8 +214,8 @@ export interface MusicLibrary { deletePlaylist(id: string): Promise addToPlaylist(playlistId: string, trackId: string): Promise removeFromPlaylist(playlistId: string, indicies: number[]): Promise - similarSongs(id: string): Promise; - topSongs(artistId: string): Promise; + similarSongs(id: string): Promise; + topSongs(artistId: string): Promise; radioStation(id: string): Promise radioStations(): Promise } diff --git a/src/subsonic.ts b/src/subsonic.ts index aaa9fbc..9783aee 100644 --- a/src/subsonic.ts +++ b/src/subsonic.ts @@ -12,9 +12,9 @@ import { Track, CoverArt, AlbumQueryType, - PlaylistSummary, Encoding, albumToAlbumSummary, + TrackSummary, } from "./music_library"; import sharp from "sharp"; import _ from "underscore"; @@ -170,24 +170,34 @@ export type GetAlbumResponse = { }; }; -type playlist = { - id: string; - name: string; - coverArt: string | undefined; -}; - export type GetPlaylistResponse = { // todo: isnt the type here a composite? playlistSummary && { entry: song[]; } playlist: { id: string; name: string; - coverArt: string | undefined; entry: song[]; + + // todo: this is an ND specific field? + coverArt: string | undefined; }; }; export type GetPlaylistsResponse = { - playlists: { playlist: playlist[] }; + playlists: { + playlist: { + id: string; + name: string; + //owner: string, + //public: boolean, + //created: string, + //changed: string, + //songCount: int, + //duration: int, + + // todo: this is an ND specific field. + coverArt: string | undefined; + }[] + }; }; export type GetSimilarSongsResponse = { @@ -280,11 +290,10 @@ export const artistImageURN = ( } }; -export const asTrack = ( - album: AlbumSummary, +export const asTrackSummary = ( song: song, customPlayers: CustomPlayers -): Track => ({ +): TrackSummary => ({ id: song.id, name: song.title, encoding: pipe( @@ -300,7 +309,6 @@ export const asTrack = ( number: song.track || 0, genre: maybeAsGenre(song.genre), coverArt: coverArtURN(song.coverArt), - album: album, artist: { id: song.artistId, name: song.artist ? song.artist : "?", @@ -317,6 +325,15 @@ export const asTrack = ( }, }); +export const asTrack = ( + album: AlbumSummary, + song: song, + customPlayers: CustomPlayers +): Track => ({ + ...asTrackSummary(song, customPlayers), + album: album, +}); + export const asAlbumSummary = (album: album): AlbumSummary => ({ id: album.id, name: album.name, @@ -327,13 +344,6 @@ export const asAlbumSummary = (album: album): AlbumSummary => ({ coverArt: coverArtURN(album.coverArt), }); -// coverArtURN -export const asPlayListSummary = (playlist: playlist): PlaylistSummary => ({ - id: playlist.id, - name: playlist.name, - coverArt: coverArtURN(playlist.coverArt), -}); - export const asGenre = (genreName: string) => ({ id: b64Encode(genreName), name: genreName, @@ -611,7 +621,6 @@ export class Subsonic { tracks: y }; }); - getArtist = ( credentials: Credentials, @@ -764,5 +773,93 @@ export class Subsonic { "accept-ranges": stream.headers["accept-ranges"], }, stream: stream.data, - })) + })); + + playlists = (credentials: Credentials) => + this.getJSON(credentials, "/rest/getPlaylists") + .then(({ playlists }) => (playlists.playlist || []).map( it => ({ + id: it.id, + name: it.name, + coverArt: coverArtURN(it.coverArt), + })) + ); + + playlist = (credentials: Credentials, id: string) => + this.getJSON(credentials, "/rest/getPlaylist", { + id, + }) + .then(({ playlist }) => { + let trackNumber = 1; + return { + id: playlist.id, + name: playlist.name, + coverArt: coverArtURN(playlist.coverArt), + entries: (playlist.entry || []).map((entry) => ({ + ...asTrack( + { + id: entry.albumId!, + name: entry.album!, + year: entry.year, + genre: maybeAsGenre(entry.genre), + artistName: entry.artist, + artistId: entry.artistId, + coverArt: coverArtURN(entry.coverArt), + }, + entry, + this.customPlayers + ), + number: trackNumber++, + })), + }; + }); + + createPlayList = (credentials: Credentials, name: string) => + this.getJSON(credentials, "/rest/createPlaylist", { + name, + }) + .then(({ playlist }) => ({ + id: playlist.id, + name: playlist.name, + coverArt: coverArtURN(playlist.coverArt), + })); + + deletePlayList = (credentials: Credentials, id: string) => + this.getJSON(credentials, "/rest/deletePlaylist", { + id, + }) + .then(it => it.status == "ok"); + + updatePlaylist = ( + credentials: Credentials, + playlistId: string, + changes : Partial<{ songIdToAdd: string | undefined, songIndexToRemove: number[] | undefined }> = {} + ) => + this.getJSON(credentials, "/rest/updatePlaylist", { + playlistId, + ...changes + }) + .then(it => it.status == "ok"); + + getSimilarSongs2 = (credentials: Credentials, id: string) => + this.getJSON( + credentials, + "/rest/getSimilarSongs2", + //todo: remove this hard coded 50? + { id, count: 50 } + ) + .then((it) => + (it.similarSongs2.song || []).map(it => asTrackSummary(it, this.customPlayers)) + ); + + getTopSongs = (credentials: Credentials, artist: string) => + this.getJSON( + credentials, + "/rest/getTopSongs", + //todo: remove this hard coded 50? + { artist, count: 50 } + ) + .then((it) => + (it.topSongs.song || []).map(it => asTrackSummary(it, this.customPlayers)) + ); + } diff --git a/src/subsonic_music_library.ts b/src/subsonic_music_library.ts index d22cf40..ae59d30 100644 --- a/src/subsonic_music_library.ts +++ b/src/subsonic_music_library.ts @@ -15,24 +15,15 @@ import { Artist, AuthFailure, AuthSuccess, - albumToAlbumSummary, } from "./music_library"; import { Subsonic, CustomPlayers, - asTrack, PingResponse, NO_CUSTOM_PLAYERS, asToken, parseToken, artistImageURN, - GetPlaylistsResponse, - GetPlaylistResponse, - asPlayListSummary, - coverArtURN, - maybeAsGenre, - GetSimilarSongsResponse, - GetTopSongsResponse, GetInternetRadioStationsResponse, asYear, isValidImage @@ -292,112 +283,31 @@ export class SubsonicMusicLibrary implements MusicLibrary { ); playlists = async () => - this.subsonic - .getJSON(this.credentials, "/rest/getPlaylists") - .then(({ playlists }) => - (playlists.playlist || []).map(asPlayListSummary) - ); + this.subsonic.playlists(this.credentials); playlist = async (id: string) => - this.subsonic - .getJSON(this.credentials, "/rest/getPlaylist", { - id, - }) - .then(({ playlist }) => { - let trackNumber = 1; - return { - id: playlist.id, - name: playlist.name, - coverArt: coverArtURN(playlist.coverArt), - entries: (playlist.entry || []).map((entry) => ({ - ...asTrack( - { - id: entry.albumId!, - name: entry.album!, - year: entry.year, - genre: maybeAsGenre(entry.genre), - artistName: entry.artist, - artistId: entry.artistId, - coverArt: coverArtURN(entry.coverArt), - }, - entry, - this.customPlayers - ), - number: trackNumber++, - })), - }; - }); + this.subsonic.playlist(this.credentials, id); createPlaylist = async (name: string) => - this.subsonic - .getJSON(this.credentials, "/rest/createPlaylist", { - name, - }) - .then(({ playlist }) => ({ - id: playlist.id, - name: playlist.name, - coverArt: coverArtURN(playlist.coverArt), - })); + this.subsonic.createPlayList(this.credentials, name); deletePlaylist = async (id: string) => - this.subsonic - .getJSON(this.credentials, "/rest/deletePlaylist", { - id, - }) - .then((_) => true); + this.subsonic.deletePlayList(this.credentials, id); addToPlaylist = async (playlistId: string, trackId: string) => - this.subsonic - .getJSON(this.credentials, "/rest/updatePlaylist", { - playlistId, - songIdToAdd: trackId, - }) - .then((_) => true); + this.subsonic.updatePlaylist(this.credentials, playlistId, { songIdToAdd: trackId }); removeFromPlaylist = async (playlistId: string, indicies: number[]) => - this.subsonic - .getJSON(this.credentials, "/rest/updatePlaylist", { - playlistId, - songIndexToRemove: indicies, - }) - .then((_) => true); + this.subsonic.updatePlaylist(this.credentials, playlistId, { songIndexToRemove: indicies }); - similarSongs = async (id: string) => - this.subsonic - .getJSON( - this.credentials, - "/rest/getSimilarSongs2", - { id, count: 50 } - ) - .then((it) => it.similarSongs2.song || []) - .then((songs) => - Promise.all( - songs.map((song) => - this.subsonic - .getAlbum(this.credentials, song.albumId!) - .then((album) => asTrack(albumToAlbumSummary(album), song, this.customPlayers)) - ) - ) - ); + similarSongs = async (id: string) => + this.subsonic.getSimilarSongs2(this.credentials, id) topSongs = async (artistId: string) => - this.subsonic.getArtist(this.credentials, artistId).then(({ name }) => - this.subsonic - .getJSON(this.credentials, "/rest/getTopSongs", { - artist: name, - count: 50, - }) - .then((it) => it.topSongs.song || []) - .then((songs) => - Promise.all( - songs.map((song) => - this.subsonic - .getAlbum(this.credentials, song.albumId!) - .then((album) => asTrack(albumToAlbumSummary(album), song, this.customPlayers)) - ) - ) - ) - ); + this.subsonic.getArtist(this.credentials, artistId) + .then(({ name }) => + this.subsonic.getTopSongs(this.credentials, name) + ); radioStations = async () => this.subsonic diff --git a/tests/builders.ts b/tests/builders.ts index 1aa7d9f..8c6650a 100644 --- a/tests/builders.ts +++ b/tests/builders.ts @@ -13,7 +13,8 @@ import { SimilarArtist, AlbumSummary, RadioStation, - ArtistSummary + ArtistSummary, + TrackSummary } from "../src/music_library"; import { b64Encode } from "../src/b64"; @@ -178,12 +179,11 @@ export const SAMPLE_GENRES = [ ]; export const randomGenre = () => SAMPLE_GENRES[randomInt(SAMPLE_GENRES.length)]; -export function aTrack(fields: Partial = {}): Track { +export function aTrackSummary(fields: Partial = {}): TrackSummary { const id = uuid(); const artist = fields.artist || anArtistSummary(); const genre = fields.genre || randomGenre(); const rating = { love: false, stars: Math.floor(Math.random() * 5) }; - const album = fields.album || anAlbumSummary({ artistId: artist.id, artistName: artist.name, genre }) return { id, name: `Track ${id}`, @@ -195,12 +195,21 @@ export function aTrack(fields: Partial = {}): Track { number: randomInt(100), genre, artist, - album, coverArt: { system: "subsonic", resource: `art:${uuid()}`}, rating, ...fields, }; -} +}; + +export function aTrack(fields: Partial = {}): Track { + const summary = aTrackSummary(fields); + const album = fields.album || anAlbumSummary({ artistId: summary.artist.id, artistName: summary.artist.name, genre: summary.genre }) + return { + ...summary, + album, + ...fields + }; +}; export function anAlbumSummary(fields: Partial = {}): AlbumSummary { const id = uuid(); @@ -213,7 +222,7 @@ export function anAlbumSummary(fields: Partial = {}): AlbumSummary artistId: `Artist ${uuid()}`, artistName: `Artist ${randomstring.generate()}`, ...fields - } + }; }; export function anAlbum(fields: Partial = {}): Album { diff --git a/tests/subsonic_music_library.test.ts b/tests/subsonic_music_library.test.ts index af4ebba..1881706 100644 --- a/tests/subsonic_music_library.test.ts +++ b/tests/subsonic_music_library.test.ts @@ -19,7 +19,6 @@ import { CustomPlayers, PingResponse, images, - } from "../src/subsonic"; import { @@ -42,6 +41,7 @@ import { AuthFailure, RadioStation, AlbumSummary, + trackToTrackSummary, } from "../src/music_library"; import { aGenre, @@ -4216,21 +4216,18 @@ describe("SubsonicMusicLibrary", () => { const track1 = aTrack({ id: "track1", artist: artistToArtistSummary(artist1), - album: albumToAlbumSummary(album1), + album: album1, genre: pop, }); mockGET .mockImplementationOnce(() => Promise.resolve(ok(getSimilarSongsJson([track1]))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album1))) ); const result = await subsonic.similarSongs(id); - expect(result).toEqual([track1]); + expect(result).toEqual([trackToTrackSummary(track1)]); expect(mockGET).toHaveBeenCalledWith( url.append({ pathname: "/rest/getSimilarSongs2" }).href(), @@ -4288,20 +4285,15 @@ describe("SubsonicMusicLibrary", () => { mockGET .mockImplementationOnce(() => Promise.resolve(ok(getSimilarSongsJson([track1, track2, track3]))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album1))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album2))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album1))) ); const result = await subsonic.similarSongs(id); - expect(result).toEqual([track1, track2, track3]); + expect(result).toEqual([ + trackToTrackSummary(track1), + trackToTrackSummary(track2), + trackToTrackSummary(track3), + ]); expect(mockGET).toHaveBeenCalledWith( url.append({ pathname: "/rest/getSimilarSongs2" }).href(), @@ -4390,14 +4382,13 @@ describe("SubsonicMusicLibrary", () => { ) .mockImplementationOnce(() => Promise.resolve(ok(getTopSongsJson([track1]))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album1))) ); const result = await subsonic.topSongs(artistId); - expect(result).toEqual([track1]); + expect(result).toEqual([ + trackToTrackSummary(track1) + ]); expect(mockGET).toHaveBeenCalledWith( url.append({ pathname: "/rest/getTopSongs" }).href(), @@ -4452,20 +4443,15 @@ describe("SubsonicMusicLibrary", () => { ) .mockImplementationOnce(() => Promise.resolve(ok(getTopSongsJson([track1, track2, track3]))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album1))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album2))) - ) - .mockImplementationOnce(() => - Promise.resolve(ok(getAlbumJson(album1))) ); const result = await subsonic.topSongs(artistId); - expect(result).toEqual([track1, track2, track3]); + expect(result).toEqual([ + trackToTrackSummary(track1), + trackToTrackSummary(track2), + trackToTrackSummary(track3), + ]); expect(mockGET).toHaveBeenCalledWith( url.append({ pathname: "/rest/getTopSongs" }).href(),