Remove tracks function, replace with just getting album

This commit is contained in:
simojenki
2025-02-15 06:48:23 +00:00
parent 7eeedff040
commit 0602e1f077
8 changed files with 242 additions and 419 deletions

View File

@@ -175,8 +175,7 @@ export interface MusicLibrary {
artists(q: ArtistQuery): Promise<Result<ArtistSummary>>; artists(q: ArtistQuery): Promise<Result<ArtistSummary>>;
artist(id: string): Promise<Artist>; artist(id: string): Promise<Artist>;
albums(q: AlbumQuery): Promise<Result<AlbumSummary>>; albums(q: AlbumQuery): Promise<Result<AlbumSummary>>;
album(id: string): Promise<AlbumSummary>; album(id: string): Promise<Album>;
tracks(albumId: string): Promise<Track[]>;
track(trackId: string): Promise<Track>; track(trackId: string): Promise<Track>;
genres(): Promise<Genre[]>; genres(): Promise<Genre[]>;
years(): Promise<Year[]>; years(): Promise<Year[]>;

View File

@@ -981,7 +981,8 @@ function bindSmapiSoapServiceToExpress(
}); });
case "album": case "album":
return musicLibrary return musicLibrary
.tracks(typeId!) .album(typeId!)
.then(it => it.tracks)
.then(slice2(paging)) .then(slice2(paging))
.then(([page, total]) => { .then(([page, total]) => {
return getMetadataResult({ return getMetadataResult({

View File

@@ -9,6 +9,7 @@ import {
AlbumQuery, AlbumQuery,
ArtistQuery, ArtistQuery,
MusicLibrary, MusicLibrary,
Album,
AlbumSummary, AlbumSummary,
Rating, Rating,
Artist, Artist,
@@ -19,9 +20,7 @@ import {
import { import {
Subsonic, Subsonic,
CustomPlayers, CustomPlayers,
GetAlbumResponse,
asTrack, asTrack,
asAlbumSummary,
PingResponse, PingResponse,
NO_CUSTOM_PLAYERS, NO_CUSTOM_PLAYERS,
asToken, asToken,
@@ -171,25 +170,11 @@ export class SubsonicMusicLibrary implements MusicLibrary {
albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> => albums = async (q: AlbumQuery): Promise<Result<AlbumSummary>> =>
this.subsonic.getAlbumList2(this.credentials, q); this.subsonic.getAlbumList2(this.credentials, q);
// todo: this should probably return an Album album = (id: string): Promise<Album> =>
album = (id: string): Promise<AlbumSummary> => this.subsonic.getAlbum(this.credentials, id);
this.subsonic.getAlbum(this.credentials, id).then(albumToAlbumSummary);
genres = () => this.subsonic.getGenres(this.credentials); genres = () => this.subsonic.getGenres(this.credentials);
// todo: do we even need this if Album has tracks?
tracks = (albumId: string) =>
this.subsonic
.getJSON<GetAlbumResponse>(this.credentials, "/rest/getAlbum", {
id: albumId,
})
.then((it) => it.album)
.then((album) =>
(album.song || []).map((song) =>
asTrack(asAlbumSummary(album), song, this.customPlayers)
)
);
track = (trackId: string) => track = (trackId: string) =>
this.subsonic.getTrack(this.credentials, trackId); this.subsonic.getTrack(this.credentials, trackId);

View File

@@ -167,23 +167,6 @@ describe("InMemoryMusicService", () => {
service.hasTracks(track1, track2, track3, track4); service.hasTracks(track1, track2, track3, track4);
}); });
describe("fetching tracks for an album", () => {
it("should return only tracks on that album", async () => {
expect(await musicLibrary.tracks(artist1Album1.id)).toEqual([
{ ...track1, rating: { love: false, stars: 0 } },
{ ...track2, rating: { love: false, stars: 0 } },
]);
});
});
describe("fetching tracks for an album that doesnt exist", () => {
it("should return empty array", async () => {
expect(await musicLibrary.tracks("non existant album id")).toEqual(
[]
);
});
});
describe("fetching a single track", () => { describe("fetching a single track", () => {
describe("when it exists", () => { describe("when it exists", () => {
it("should return the track", async () => { it("should return the track", async () => {
@@ -424,7 +407,10 @@ describe("InMemoryMusicService", () => {
describe("when it exists", () => { describe("when it exists", () => {
it("should provide an album", async () => { it("should provide an album", async () => {
expect(await musicLibrary.album(artist1_album5.id)).toEqual( expect(await musicLibrary.album(artist1_album5.id)).toEqual(
artist1_album5 {
...artist1_album5,
tracks: []
}
); );
}); });
}); });

View File

@@ -102,7 +102,7 @@ export class InMemoryMusicService implements MusicService {
pipe( pipe(
this.artists.flatMap((it) => it.albums).find((it) => it.id === id), this.artists.flatMap((it) => it.albums).find((it) => it.id === id),
O.fromNullable, O.fromNullable,
O.map((it) => Promise.resolve(it)), O.map((it) => Promise.resolve({ ...it, tracks: [] })),
O.getOrElse(() => Promise.reject(`No album with id '${id}'`)) O.getOrElse(() => Promise.reject(`No album with id '${id}'`))
), ),
genres: () => genres: () =>
@@ -117,12 +117,6 @@ export class InMemoryMusicService implements MusicService {
A.sort(fromCompare<Genre>((x, y) => ordString.compare(x.id, y.id))) A.sort(fromCompare<Genre>((x, y) => ordString.compare(x.id, y.id)))
) )
), ),
tracks: (albumId: string) =>
Promise.resolve(
this.tracks
.filter((it) => it.album.id === albumId)
.map((it) => ({ ...it, rating: { love: false, stars: 0 } }))
),
rate: (_: string, _2: Rating) => Promise.resolve(false), rate: (_: string, _2: Rating) => Promise.resolve(false),
track: (trackId: string) => track: (trackId: string) =>
pipe( pipe(

View File

@@ -41,6 +41,8 @@ import {
PUNK, PUNK,
aPlaylist, aPlaylist,
aRadioStation, aRadioStation,
anArtistSummary,
anAlbumSummary,
} from "./builders"; } from "./builders";
import { InMemoryMusicService } from "./in_memory_music_service"; import { InMemoryMusicService } from "./in_memory_music_service";
import supersoap from "./supersoap"; import supersoap from "./supersoap";
@@ -2356,10 +2358,8 @@ describe("wsdl api", () => {
}); });
describe("asking for an album", () => { describe("asking for an album", () => {
const album = anAlbum(); const album = anAlbumSummary();
const artist = anArtist({ const artist = anArtistSummary();
albums: [album],
});
const track1 = aTrack({ artist, album, number: 1 }); const track1 = aTrack({ artist, album, number: 1 });
const track2 = aTrack({ artist, album, number: 2 }); const track2 = aTrack({ artist, album, number: 2 });
@@ -2370,7 +2370,12 @@ describe("wsdl api", () => {
const tracks = [track1, track2, track3, track4, track5]; const tracks = [track1, track2, track3, track4, track5];
beforeEach(() => { beforeEach(() => {
musicLibrary.tracks.mockResolvedValue(tracks); musicLibrary.album.mockResolvedValue(anAlbum({
...album,
artistName: artist.name,
artistId: artist.id,
tracks
}));
}); });
describe("asking for all for an album", () => { describe("asking for all for an album", () => {
@@ -2394,7 +2399,7 @@ describe("wsdl api", () => {
total: tracks.length, total: tracks.length,
}) })
); );
expect(musicLibrary.tracks).toHaveBeenCalledWith(album.id); expect(musicLibrary.album).toHaveBeenCalledWith(album.id);
}); });
}); });
@@ -2421,7 +2426,7 @@ describe("wsdl api", () => {
total: tracks.length, total: tracks.length,
}) })
); );
expect(musicLibrary.tracks).toHaveBeenCalledWith(album.id); expect(musicLibrary.album).toHaveBeenCalledWith(album.id);
}); });
}); });
}); });

View File

@@ -29,7 +29,8 @@ import {
TranscodingCustomPlayers, TranscodingCustomPlayers,
CustomPlayers, CustomPlayers,
NO_CUSTOM_PLAYERS, NO_CUSTOM_PLAYERS,
Subsonic Subsonic,
asGenre
} from "../src/subsonic"; } from "../src/subsonic";
import { b64Encode } from "../src/b64"; import { b64Encode } from "../src/b64";
@@ -779,54 +780,232 @@ describe("subsonic", () => {
}); });
describe("getting an album", () => { describe("getting an album", () => {
beforeEach(() => { describe("when there are no custom players", () => {
customPlayers.encodingFor.mockReturnValue(O.none); beforeEach(() => {
customPlayers.encodingFor.mockReturnValue(O.none);
});
describe("when the album has some tracks", () => {
const artistId = "artist6677"
const artistName = "Fizzy Wizzy"
const albumSummary = anAlbumSummary({ artistId, artistName })
const artistSumamry = anArtistSummary({ id: artistId, name: artistName })
// todo: fix these ratings
const tracks = [
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
];
const album = anAlbum({
...albumSummary,
tracks,
artistId,
artistName,
});
beforeEach(() => {
mockGET.mockImplementationOnce(() =>
Promise.resolve(ok(getAlbumJson(album)))
);
});
it("should return the album", async () => {
const result = await subsonic.getAlbum(credentials, album.id);
expect(result).toEqual(album);
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
});
});
describe("when the album has no tracks", () => {
const artistId = "artist6677"
const artistName = "Fizzy Wizzy"
const albumSummary = anAlbumSummary({ artistId, artistName })
const album = anAlbum({
...albumSummary,
tracks: [],
artistId,
artistName,
});
beforeEach(() => {
mockGET.mockImplementationOnce(() =>
Promise.resolve(ok(getAlbumJson(album)))
);
});
it("should return the album", async () => {
const result = await subsonic.getAlbum(credentials, album.id);
expect(result).toEqual(album);
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
});
});
}); });
describe("when it exists", () => { describe("when a custom player is configured for the mime type", () => {
const artistId = "artist6677" const hipHop = asGenre("Hip-Hop");
const artistName = "Fizzy Wizzy" const tripHop = asGenre("Trip-Hop");
const albumSummary = anAlbumSummary({ artistId, artistName }) const albumSummary = anAlbumSummary({ id: "album1", name: "Burnin", genre: hipHop });
const artistSumamry = anArtistSummary({ id: artistId, name: artistName })
// todo: fix these ratings const artistSummary = anArtistSummary({
const tracks = [ id: "artist1",
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }), name: "Bob Marley"
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }), });
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
aTrack({ artist: artistSumamry, album: albumSummary, rating: { love: false, stars: 0 } }),
];
const album = anAlbum({ const alac = aTrack({
...albumSummary, artist: artistSummary,
tracks, album: albumSummary,
artistId, encoding: {
artistName, player: "bonob",
}); mimeType: "audio/alac",
},
genre: hipHop,
rating: {
love: true,
stars: 3,
},
});
const m4a = aTrack({
artist: artistSummary,
album: albumSummary,
encoding: {
player: "bonob",
mimeType: "audio/m4a",
},
genre: hipHop,
rating: {
love: false,
stars: 0,
},
});
const mp3 = aTrack({
artist: artistSummary,
album: albumSummary,
encoding: {
player: "bonob",
mimeType: "audio/mp3",
},
genre: tripHop,
rating: {
love: true,
stars: 5,
},
});
beforeEach(() => { const album = anAlbum({
mockGET.mockImplementationOnce(() => ...albumSummary,
Promise.resolve(ok(getAlbumJson(album))) tracks: [alac, m4a, mp3]
); })
});
beforeEach(() => {
customPlayers.encodingFor
.mockReturnValueOnce(
O.of({ player: "bonob+audio/alac", mimeType: "audio/flac" })
)
.mockReturnValueOnce(
O.of({ player: "bonob+audio/m4a", mimeType: "audio/opus" })
)
.mockReturnValueOnce(O.none);
it("should return the album", async () => { mockGET.mockImplementationOnce(() =>
const result = await subsonic.getAlbum(credentials, album.id); Promise.resolve(ok(getAlbumJson(album)))
);
});
expect(result).toEqual(album); it("should return the album with custom players applied", async () => {
const result = await subsonic.getAlbum(credentials, album.id);
expect(axios.get).toHaveBeenCalledWith( expect(result).toEqual({
url.append({ pathname: "/rest/getAlbum" }).href(), ...album,
{ tracks: [
params: asURLSearchParams({ {
...authParamsPlusJson, ...alac,
id: album.id, encoding: {
}), player: "bonob+audio/alac",
headers, mimeType: "audio/flac",
} },
); // todo: this doesnt seem right? why dont the ratings come back?
}); rating: {
love: false,
stars: 0
}
},
{
...m4a,
encoding: {
player: "bonob+audio/m4a",
mimeType: "audio/opus",
},
rating: {
love: false,
stars: 0
}
},
{
...mp3,
encoding: {
player: "bonob",
mimeType: "audio/mp3",
},
rating: {
love: false,
stars: 0
}
},
]
});
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
expect(customPlayers.encodingFor).toHaveBeenCalledTimes(3);
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(1, {
mimeType: "audio/alac",
});
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(2, {
mimeType: "audio/m4a",
});
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(3, {
mimeType: "audio/mp3",
});
});
}); });
}); });
}); });

View File

@@ -55,7 +55,6 @@ import {
ROCK, ROCK,
aRadioStation, aRadioStation,
anAlbumSummary, anAlbumSummary,
anArtistSummary
} from "./builders"; } from "./builders";
import { b64Encode } from "../src/b64"; import { b64Encode } from "../src/b64";
import { BUrn } from "../src/burn"; import { BUrn } from "../src/burn";
@@ -2592,332 +2591,7 @@ describe("SubsonicMusicLibrary", () => {
}); });
}); });
describe("getting tracks", () => { describe("getting tracks", () => {
describe("for an album", () => {
describe("when there are no custom players", () => {
beforeEach(() => {
customPlayers.encodingFor.mockReturnValue(O.none);
});
describe("when the album has multiple tracks, some of which are rated", () => {
const hipHop = asGenre("Hip-Hop");
const tripHop = asGenre("Trip-Hop");
const albumSummary = anAlbumSummary({
id: "album1",
name: "Burnin",
genre: hipHop,
});
const artistSummary = anArtistSummary({
id: "artist1",
name: "Bob Marley"
});
const track1 = aTrack({
artist: artistSummary,
album: albumSummary,
genre: hipHop,
rating: {
love: true,
stars: 3,
},
});
const track2 = aTrack({
artist: artistSummary,
album: albumSummary,
genre: hipHop,
rating: {
love: false,
stars: 0,
},
});
const track3 = aTrack({
artist: artistSummary,
album: albumSummary,
genre: tripHop,
rating: {
love: true,
stars: 5,
},
});
const track4 = aTrack({
artist: artistSummary,
album: albumSummary,
genre: tripHop,
rating: {
love: false,
stars: 1,
},
});
const album = anAlbum({
...albumSummary,
tracks: [track1, track2, track3, track4]
})
beforeEach(() => {
mockGET.mockImplementationOnce(() =>
Promise.resolve(ok(getAlbumJson(album)))
);
});
it("should return the album", async () => {
const result = await subsonic.tracks(album.id);
// todo: not this
const blatRating = (t: Track) => ({
...t,
rating: {
love: false,
stars: 0
}
})
expect(result).toEqual([
blatRating(track1),
blatRating(track2),
blatRating(track3),
blatRating(track4),
]);
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
});
});
describe("when the album has only 1 track", () => {
// todo: why do i care about the genre in here?
const flipFlop = asGenre("Flip-Flop");
const albumSummary = anAlbumSummary({
id: "album1",
name: "Burnin",
genre: flipFlop,
});
const artistSummary = anArtistSummary({
id: "artist1",
name: "Bob Marley"
});
const track = aTrack({
artist: artistSummary,
album: albumSummary,
genre: flipFlop,
});
const album = anAlbum({
...albumSummary,
tracks: [track]
});
beforeEach(() => {
mockGET.mockImplementationOnce(() =>
Promise.resolve(ok(getAlbumJson(album)))
);
});
it("should return the album", async () => {
const result = await subsonic.tracks(album.id);
expect(result).toEqual([{
...track,
// todo: not sure about this
rating: {
love: false,
stars: 0
}
}]);
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
});
});
describe("when the album has no tracks", () => {
const album = anAlbum({
tracks: []
})
beforeEach(() => {
mockGET.mockImplementationOnce(() =>
Promise.resolve(ok(getAlbumJson(album)))
);
});
it("should empty array", async () => {
const result = await subsonic.tracks(album.id);
expect(result).toEqual([]);
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
});
});
});
describe("when a custom player is configured for the mime type", () => {
const hipHop = asGenre("Hip-Hop");
const tripHop = asGenre("Trip-Hop");
const albumSummary = anAlbumSummary({ id: "album1", name: "Burnin", genre: hipHop });
const artistSummary = anArtistSummary({
id: "artist1",
name: "Bob Marley"
});
const alac = aTrack({
artist: artistSummary,
album: albumSummary,
encoding: {
player: "bonob",
mimeType: "audio/alac",
},
genre: hipHop,
rating: {
love: true,
stars: 3,
},
});
const m4a = aTrack({
artist: artistSummary,
album: albumSummary,
encoding: {
player: "bonob",
mimeType: "audio/m4a",
},
genre: hipHop,
rating: {
love: false,
stars: 0,
},
});
const mp3 = aTrack({
artist: artistSummary,
album: albumSummary,
encoding: {
player: "bonob",
mimeType: "audio/mp3",
},
genre: tripHop,
rating: {
love: true,
stars: 5,
},
});
const album = anAlbum({
...albumSummary,
tracks: [alac, m4a, mp3]
})
beforeEach(() => {
customPlayers.encodingFor
.mockReturnValueOnce(
O.of({ player: "bonob+audio/alac", mimeType: "audio/flac" })
)
.mockReturnValueOnce(
O.of({ player: "bonob+audio/m4a", mimeType: "audio/opus" })
)
.mockReturnValueOnce(O.none);
mockGET.mockImplementationOnce(() =>
Promise.resolve(ok(getAlbumJson(album)))
);
});
it("should return the album with custom players applied", async () => {
const result = await subsonic.tracks(album.id);
expect(result).toEqual([
{
...alac,
encoding: {
player: "bonob+audio/alac",
mimeType: "audio/flac",
},
// todo: this doesnt seem right? why dont the ratings come back?
rating: {
love: false,
stars: 0
}
},
{
...m4a,
encoding: {
player: "bonob+audio/m4a",
mimeType: "audio/opus",
},
rating: {
love: false,
stars: 0
}
},
{
...mp3,
encoding: {
player: "bonob",
mimeType: "audio/mp3",
},
rating: {
love: false,
stars: 0
}
},
]);
expect(axios.get).toHaveBeenCalledWith(
url.append({ pathname: "/rest/getAlbum" }).href(),
{
params: asURLSearchParams({
...authParamsPlusJson,
id: album.id,
}),
headers,
}
);
expect(customPlayers.encodingFor).toHaveBeenCalledTimes(3);
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(1, {
mimeType: "audio/alac",
});
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(2, {
mimeType: "audio/m4a",
});
expect(customPlayers.encodingFor).toHaveBeenNthCalledWith(3, {
mimeType: "audio/mp3",
});
});
});
});
describe("a single track", () => { describe("a single track", () => {
const pop = asGenre("Pop"); const pop = asGenre("Pop");