mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to query for recently added and recently played albums
This commit is contained in:
@@ -94,7 +94,7 @@ export const asResult = <T>([results, total]: [T[], number]) => ({
|
|||||||
|
|
||||||
export type ArtistQuery = Paging;
|
export type ArtistQuery = Paging;
|
||||||
|
|
||||||
export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent';
|
export type AlbumQueryType = 'alphabeticalByArtist' | 'byGenre' | 'random' | 'recent' | 'frequent' | 'newest';
|
||||||
|
|
||||||
export type AlbumQuery = Paging & {
|
export type AlbumQuery = Paging & {
|
||||||
type: AlbumQueryType;
|
type: AlbumQueryType;
|
||||||
|
|||||||
79
src/smapi.ts
79
src/smapi.ts
@@ -8,6 +8,7 @@ import logger from "./logger";
|
|||||||
import { LinkCodes } from "./link_codes";
|
import { LinkCodes } from "./link_codes";
|
||||||
import {
|
import {
|
||||||
Album,
|
Album,
|
||||||
|
AlbumQuery,
|
||||||
AlbumSummary,
|
AlbumSummary,
|
||||||
ArtistSummary,
|
ArtistSummary,
|
||||||
Genre,
|
Genre,
|
||||||
@@ -442,6 +443,19 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
const [type, typeId] = id.split(":");
|
const [type, typeId] = id.split(":");
|
||||||
const paging = { _index: index, _count: count };
|
const paging = { _index: index, _count: count };
|
||||||
logger.debug(`Fetching metadata type=${type}, typeId=${typeId}`);
|
logger.debug(`Fetching metadata type=${type}, typeId=${typeId}`);
|
||||||
|
|
||||||
|
const albums = (q: AlbumQuery): Promise<GetMetadataResponse> =>
|
||||||
|
musicLibrary.albums(q).then((result) => {
|
||||||
|
const accessToken = accessTokens.mint(authToken);
|
||||||
|
return getMetadataResult({
|
||||||
|
mediaCollection: result.results.map((it) =>
|
||||||
|
album(webAddress, accessToken, it)
|
||||||
|
),
|
||||||
|
index: paging._index,
|
||||||
|
total: result.total,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "root":
|
case "root":
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
@@ -450,9 +464,14 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
container({ id: "albums", title: "Albums" }),
|
container({ id: "albums", title: "Albums" }),
|
||||||
container({ id: "genres", title: "Genres" }),
|
container({ id: "genres", title: "Genres" }),
|
||||||
container({ id: "randomAlbums", title: "Random" }),
|
container({ id: "randomAlbums", title: "Random" }),
|
||||||
|
container({ id: "recentlyAdded", title: "Recently Added" }),
|
||||||
|
container({
|
||||||
|
id: "recentlyPlayed",
|
||||||
|
title: "Recently Played",
|
||||||
|
}),
|
||||||
],
|
],
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 4,
|
total: 6,
|
||||||
});
|
});
|
||||||
case "artists":
|
case "artists":
|
||||||
return await musicLibrary.artists(paging).then((result) => {
|
return await musicLibrary.artists(paging).then((result) => {
|
||||||
@@ -465,31 +484,32 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
total: result.total,
|
total: result.total,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
case "albums":
|
case "albums": {
|
||||||
return await musicLibrary
|
return await albums({
|
||||||
.albums({ type: "alphabeticalByArtist", ...paging })
|
type: "alphabeticalByArtist",
|
||||||
.then((result) => {
|
...paging,
|
||||||
const accessToken = accessTokens.mint(authToken);
|
|
||||||
return getMetadataResult({
|
|
||||||
mediaCollection: result.results.map((it) =>
|
|
||||||
album(webAddress, accessToken, it)
|
|
||||||
),
|
|
||||||
index: paging._index,
|
|
||||||
total: result.total,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
}
|
||||||
case "randomAlbums":
|
case "randomAlbums":
|
||||||
return await musicLibrary
|
return await albums({
|
||||||
.albums({ type: "random", ...paging })
|
type: "random",
|
||||||
.then((result) => {
|
...paging,
|
||||||
const accessToken = accessTokens.mint(authToken);
|
|
||||||
return getMetadataResult({
|
|
||||||
mediaCollection: result.results.map((it) =>
|
|
||||||
album(webAddress, accessToken, it)
|
|
||||||
),
|
|
||||||
index: paging._index,
|
|
||||||
total: result.total,
|
|
||||||
});
|
});
|
||||||
|
case "genre":
|
||||||
|
return await albums({
|
||||||
|
type: "byGenre",
|
||||||
|
genre: typeId,
|
||||||
|
...paging,
|
||||||
|
});
|
||||||
|
case "recentlyAdded":
|
||||||
|
return await albums({
|
||||||
|
type: "newest",
|
||||||
|
...paging,
|
||||||
|
});
|
||||||
|
case "recentlyPlayed":
|
||||||
|
return await albums({
|
||||||
|
type: "frequent",
|
||||||
|
...paging,
|
||||||
});
|
});
|
||||||
case "genres":
|
case "genres":
|
||||||
return await musicLibrary
|
return await musicLibrary
|
||||||
@@ -546,19 +566,6 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
total,
|
total,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
case "genre":
|
|
||||||
return await musicLibrary
|
|
||||||
.albums({ type: "byGenre", genre: typeId, ...paging })
|
|
||||||
.then((result) => {
|
|
||||||
const accessToken = accessTokens.mint(authToken);
|
|
||||||
return getMetadataResult({
|
|
||||||
mediaCollection: result.results.map((it) =>
|
|
||||||
album(webAddress, accessToken, it)
|
|
||||||
),
|
|
||||||
index: paging._index,
|
|
||||||
total: result.total,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
default:
|
default:
|
||||||
throw `Unsupported id:${id}`;
|
throw `Unsupported id:${id}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import {
|
|||||||
t,
|
t,
|
||||||
BROWSER_HEADERS,
|
BROWSER_HEADERS,
|
||||||
DODGY_IMAGE_NAME,
|
DODGY_IMAGE_NAME,
|
||||||
asGenre
|
asGenre,
|
||||||
} from "../src/navidrome";
|
} from "../src/navidrome";
|
||||||
import encryption from "../src/encryption";
|
import encryption from "../src/encryption";
|
||||||
|
|
||||||
@@ -280,7 +280,7 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when there are many", () => {
|
describe("when there are many", () => {
|
||||||
const genres = ["g1", "g2", "g3", "g3"]
|
const genres = ["g1", "g2", "g3", "g3"];
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGET
|
mockGET
|
||||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
@@ -868,7 +868,12 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should pass the filter to navidrome", async () => {
|
it("should pass the filter to navidrome", async () => {
|
||||||
const q: AlbumQuery = { _index: 0, _count: 500, genre: "Pop", type: 'byGenre' };
|
const q: AlbumQuery = {
|
||||||
|
_index: 0,
|
||||||
|
_count: 500,
|
||||||
|
genre: "Pop",
|
||||||
|
type: "byGenre",
|
||||||
|
};
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
.then((it) => it as AuthSuccess)
|
.then((it) => it as AuthSuccess)
|
||||||
@@ -892,6 +897,122 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("by newest", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
ok(
|
||||||
|
albumListXml([
|
||||||
|
[artist, album3],
|
||||||
|
[artist, album2],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass the filter to navidrome", async () => {
|
||||||
|
const q: AlbumQuery = { _index: 0, _count: 500, type: "newest" };
|
||||||
|
const result = await navidrome
|
||||||
|
.generateToken({ username, password })
|
||||||
|
.then((it) => it as AuthSuccess)
|
||||||
|
.then((it) => navidrome.login(it.authToken))
|
||||||
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
results: [album3, album2].map(albumToAlbumSummary),
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
|
||||||
|
params: {
|
||||||
|
type: "newest",
|
||||||
|
size: 500,
|
||||||
|
offset: 0,
|
||||||
|
...authParams,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("by recently played", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(
|
||||||
|
ok(
|
||||||
|
albumListXml([
|
||||||
|
[artist, album3],
|
||||||
|
[artist, album2],
|
||||||
|
])
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass the filter to navidrome", async () => {
|
||||||
|
const q: AlbumQuery = { _index: 0, _count: 500, type: "recent" };
|
||||||
|
const result = await navidrome
|
||||||
|
.generateToken({ username, password })
|
||||||
|
.then((it) => it as AuthSuccess)
|
||||||
|
.then((it) => navidrome.login(it.authToken))
|
||||||
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
results: [album3, album2].map(albumToAlbumSummary),
|
||||||
|
total: 2,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
|
||||||
|
params: {
|
||||||
|
type: "recent",
|
||||||
|
size: 500,
|
||||||
|
offset: 0,
|
||||||
|
...authParams,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("by frequently played", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGET
|
||||||
|
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||||
|
.mockImplementationOnce(() =>
|
||||||
|
Promise.resolve(ok(albumListXml([[artist, album2]])))
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should pass the filter to navidrome", async () => {
|
||||||
|
const q: AlbumQuery = { _index: 0, _count: 500, type: "frequent" };
|
||||||
|
const result = await navidrome
|
||||||
|
.generateToken({ username, password })
|
||||||
|
.then((it) => it as AuthSuccess)
|
||||||
|
.then((it) => navidrome.login(it.authToken))
|
||||||
|
.then((it) => it.albums(q));
|
||||||
|
|
||||||
|
expect(result).toEqual({
|
||||||
|
results: [album2].map(albumToAlbumSummary),
|
||||||
|
total: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbumList`, {
|
||||||
|
params: {
|
||||||
|
type: "frequent",
|
||||||
|
size: 500,
|
||||||
|
offset: 0,
|
||||||
|
...authParams,
|
||||||
|
},
|
||||||
|
headers,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when the artist has only 1 album", () => {
|
describe("when the artist has only 1 album", () => {
|
||||||
@@ -911,7 +1032,11 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the album", async () => {
|
it("should return the album", async () => {
|
||||||
const q: AlbumQuery = { _index: 0, _count: 500, type: 'alphabeticalByArtist' };
|
const q: AlbumQuery = {
|
||||||
|
_index: 0,
|
||||||
|
_count: 500,
|
||||||
|
type: "alphabeticalByArtist",
|
||||||
|
};
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
.then((it) => it as AuthSuccess)
|
.then((it) => it as AuthSuccess)
|
||||||
@@ -952,7 +1077,11 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should return the album", async () => {
|
it("should return the album", async () => {
|
||||||
const q: AlbumQuery = { _index: 0, _count: 500, type: 'alphabeticalByArtist' };
|
const q: AlbumQuery = {
|
||||||
|
_index: 0,
|
||||||
|
_count: 500,
|
||||||
|
type: "alphabeticalByArtist",
|
||||||
|
};
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
.then((it) => it as AuthSuccess)
|
.then((it) => it as AuthSuccess)
|
||||||
@@ -983,11 +1112,19 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
const artist1 = anArtist({
|
const artist1 = anArtist({
|
||||||
name: "abba",
|
name: "abba",
|
||||||
albums: [anAlbum({ genre: genre1 }), anAlbum({ genre: genre2 }), anAlbum({ genre: genre3 })],
|
albums: [
|
||||||
|
anAlbum({ genre: genre1 }),
|
||||||
|
anAlbum({ genre: genre2 }),
|
||||||
|
anAlbum({ genre: genre3 }),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const artist2 = anArtist({
|
const artist2 = anArtist({
|
||||||
name: "babba",
|
name: "babba",
|
||||||
albums: [anAlbum({ genre: genre1 }), anAlbum({ genre: genre2 }), anAlbum({ genre: genre3 })],
|
albums: [
|
||||||
|
anAlbum({ genre: genre1 }),
|
||||||
|
anAlbum({ genre: genre2 }),
|
||||||
|
anAlbum({ genre: genre3 }),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
const artists = [artist1, artist2];
|
const artists = [artist1, artist2];
|
||||||
const albums = artists.flatMap((artist) => artist.albums);
|
const albums = artists.flatMap((artist) => artist.albums);
|
||||||
@@ -1002,7 +1139,11 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
describe("querying for all of them", () => {
|
describe("querying for all of them", () => {
|
||||||
it("should return all of them with corrent paging information", async () => {
|
it("should return all of them with corrent paging information", async () => {
|
||||||
const q : AlbumQuery= { _index: 0, _count: 500, type: 'alphabeticalByArtist' };
|
const q: AlbumQuery = {
|
||||||
|
_index: 0,
|
||||||
|
_count: 500,
|
||||||
|
type: "alphabeticalByArtist",
|
||||||
|
};
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
.then((it) => it as AuthSuccess)
|
.then((it) => it as AuthSuccess)
|
||||||
@@ -1028,7 +1169,11 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
describe("querying for a page of them", () => {
|
describe("querying for a page of them", () => {
|
||||||
it("should return the page with the corrent paging information", async () => {
|
it("should return the page with the corrent paging information", async () => {
|
||||||
const q : AlbumQuery = { _index: 2, _count: 2, type: 'alphabeticalByArtist' };
|
const q: AlbumQuery = {
|
||||||
|
_index: 2,
|
||||||
|
_count: 2,
|
||||||
|
type: "alphabeticalByArtist",
|
||||||
|
};
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
.then((it) => it as AuthSuccess)
|
.then((it) => it as AuthSuccess)
|
||||||
@@ -1080,7 +1225,11 @@ describe("Navidrome", () => {
|
|||||||
|
|
||||||
describe("querying for all of them", () => {
|
describe("querying for all of them", () => {
|
||||||
it("will return only the first 500 with the correct paging information", async () => {
|
it("will return only the first 500 with the correct paging information", async () => {
|
||||||
const q: AlbumQuery = { _index: 0, _count: 1000, type: 'alphabeticalByArtist' };
|
const q: AlbumQuery = {
|
||||||
|
_index: 0,
|
||||||
|
_count: 1000,
|
||||||
|
type: "alphabeticalByArtist",
|
||||||
|
};
|
||||||
const result = await navidrome
|
const result = await navidrome
|
||||||
.generateToken({ username, password })
|
.generateToken({ username, password })
|
||||||
.then((it) => it as AuthSuccess)
|
.then((it) => it as AuthSuccess)
|
||||||
@@ -1152,8 +1301,8 @@ describe("Navidrome", () => {
|
|||||||
describe("getting tracks", () => {
|
describe("getting tracks", () => {
|
||||||
describe("for an album", () => {
|
describe("for an album", () => {
|
||||||
describe("when the album has multiple tracks", () => {
|
describe("when the album has multiple tracks", () => {
|
||||||
const hipHop = asGenre("Hip-Hop")
|
const hipHop = asGenre("Hip-Hop");
|
||||||
const tripHop = asGenre("Trip-Hop")
|
const tripHop = asGenre("Trip-Hop");
|
||||||
|
|
||||||
const album = anAlbum({ id: "album1", name: "Burnin", genre: hipHop });
|
const album = anAlbum({ id: "album1", name: "Burnin", genre: hipHop });
|
||||||
const albumSummary = albumToAlbumSummary(album);
|
const albumSummary = albumToAlbumSummary(album);
|
||||||
@@ -1171,8 +1320,16 @@ describe("Navidrome", () => {
|
|||||||
const tracks = [
|
const tracks = [
|
||||||
aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }),
|
aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }),
|
||||||
aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }),
|
aTrack({ artist: artistSummary, album: albumSummary, genre: hipHop }),
|
||||||
aTrack({ artist: artistSummary, album: albumSummary, genre: tripHop }),
|
aTrack({
|
||||||
aTrack({ artist: artistSummary, album: albumSummary, genre: tripHop }),
|
artist: artistSummary,
|
||||||
|
album: albumSummary,
|
||||||
|
genre: tripHop,
|
||||||
|
}),
|
||||||
|
aTrack({
|
||||||
|
artist: artistSummary,
|
||||||
|
album: albumSummary,
|
||||||
|
genre: tripHop,
|
||||||
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -1203,9 +1360,13 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when the album has only 1 track", () => {
|
describe("when the album has only 1 track", () => {
|
||||||
const flipFlop = asGenre("Flip-Flop")
|
const flipFlop = asGenre("Flip-Flop");
|
||||||
|
|
||||||
const album = anAlbum({ id: "album1", name: "Burnin", genre: flipFlop });
|
const album = anAlbum({
|
||||||
|
id: "album1",
|
||||||
|
name: "Burnin",
|
||||||
|
genre: flipFlop,
|
||||||
|
});
|
||||||
const albumSummary = albumToAlbumSummary(album);
|
const albumSummary = albumToAlbumSummary(album);
|
||||||
|
|
||||||
const artist = anArtist({
|
const artist = anArtist({
|
||||||
@@ -1218,7 +1379,13 @@ describe("Navidrome", () => {
|
|||||||
image: NO_IMAGES,
|
image: NO_IMAGES,
|
||||||
};
|
};
|
||||||
|
|
||||||
const tracks = [aTrack({ artist: artistSummary, album: albumSummary, genre: flipFlop })];
|
const tracks = [
|
||||||
|
aTrack({
|
||||||
|
artist: artistSummary,
|
||||||
|
album: albumSummary,
|
||||||
|
genre: flipFlop,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGET
|
mockGET
|
||||||
@@ -1287,7 +1454,7 @@ describe("Navidrome", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("a single track", () => {
|
describe("a single track", () => {
|
||||||
const pop = asGenre("Pop")
|
const pop = asGenre("Pop");
|
||||||
|
|
||||||
const album = anAlbum({ id: "album1", name: "Burnin", genre: pop });
|
const album = anAlbum({ id: "album1", name: "Burnin", genre: pop });
|
||||||
const albumSummary = albumToAlbumSummary(album);
|
const albumSummary = albumToAlbumSummary(album);
|
||||||
@@ -1302,7 +1469,11 @@ describe("Navidrome", () => {
|
|||||||
image: NO_IMAGES,
|
image: NO_IMAGES,
|
||||||
};
|
};
|
||||||
|
|
||||||
const track = aTrack({ artist: artistSummary, album: albumSummary, genre: pop });
|
const track = aTrack({
|
||||||
|
artist: artistSummary,
|
||||||
|
album: albumSummary,
|
||||||
|
genre: pop,
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
mockGET
|
mockGET
|
||||||
|
|||||||
@@ -557,9 +557,11 @@ describe("api", () => {
|
|||||||
{ itemType: "container", id: "albums", title: "Albums" },
|
{ itemType: "container", id: "albums", title: "Albums" },
|
||||||
{ itemType: "container", id: "genres", title: "Genres" },
|
{ itemType: "container", id: "genres", title: "Genres" },
|
||||||
{ itemType: "container", id: "randomAlbums", title: "Random" },
|
{ itemType: "container", id: "randomAlbums", title: "Random" },
|
||||||
|
{ itemType: "container", id: "recentlyAdded", title: "Recently Added" },
|
||||||
|
{ itemType: "container", id: "recentlyPlayed", title: "Recently Played" },
|
||||||
],
|
],
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 4,
|
total: 6,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user