mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-07 20:40:15 +02:00
add support for full playlist re-order (#1327)
This commit is contained in:
@@ -628,6 +628,20 @@ export const controller: GeneralController = {
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
replacePlaylist(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
if (!server) {
|
||||
throw new Error(
|
||||
`${i18n.t('error.apiRouteError', { postProcess: 'sentenceCase' })}: replacePlaylist`,
|
||||
);
|
||||
}
|
||||
|
||||
return apiController(
|
||||
'replacePlaylist',
|
||||
server.type,
|
||||
)?.({ ...args, apiClientProps: { ...args.apiClientProps, server } });
|
||||
},
|
||||
scrobble(args) {
|
||||
const server = getServerById(args.apiClientProps.serverId);
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { set } from 'idb-keyval';
|
||||
import chunk from 'lodash/chunk';
|
||||
import filter from 'lodash/filter';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
@@ -1162,6 +1163,113 @@ export const JellyfinController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
if (!apiClientProps.server?.userId) {
|
||||
throw new Error('No userId found');
|
||||
}
|
||||
|
||||
// 1. Fetch existing songs from the playlist
|
||||
const existingSongsRes = await jfApiClient(apiClientProps).getPlaylistSongList({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, UserData, ParentId, People, Tags',
|
||||
IncludeItemTypes: 'Audio',
|
||||
UserId: apiClientProps.server?.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSongsRes.status !== 200) {
|
||||
throw new Error('Failed to fetch existing playlist songs');
|
||||
}
|
||||
|
||||
const existingSongs = existingSongsRes.body.Items.map((item) =>
|
||||
jfNormalize.song(item, apiClientProps.server),
|
||||
);
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
const playlistDetailRes = await jfApiClient(apiClientProps).getPlaylistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
userId: apiClientProps.server?.userId,
|
||||
},
|
||||
query: {
|
||||
Fields: 'Genres, DateCreated, MediaSources, ChildCount, ParentId',
|
||||
Ids: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (playlistDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get playlist detail');
|
||||
}
|
||||
|
||||
const playlist = jfNormalize.playlist(playlistDetailRes.body, apiClientProps.server);
|
||||
|
||||
// 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
|
||||
const backup = {
|
||||
id: query.id,
|
||||
name: playlist.name,
|
||||
songIds: existingSongs.map((song) => song.id),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store backup in IndexedDB using idb-keyval
|
||||
const backupKey = `playlist-backup-${query.id}`;
|
||||
await set(backupKey, backup);
|
||||
|
||||
// 4. Remove all songs from the playlist
|
||||
if (existingSongs.length > 0) {
|
||||
const existingPlaylistItemIds = existingSongs
|
||||
.map((song) => song.playlistItemId)
|
||||
.filter((id): id is string => id !== undefined && id !== null);
|
||||
|
||||
if (existingPlaylistItemIds.length > 0) {
|
||||
const chunks = chunk(existingPlaylistItemIds, MAX_ITEMS_PER_PLAYLIST_ADD);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const removeRes = await jfApiClient(apiClientProps).removeFromPlaylist({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
EntryIds: chunk.join(','),
|
||||
},
|
||||
});
|
||||
|
||||
if (removeRes.status !== 204) {
|
||||
throw new Error('Failed to remove songs from playlist');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add the new song ids to the playlist
|
||||
if (body.songId.length > 0) {
|
||||
const chunks = chunk(body.songId, MAX_ITEMS_PER_PLAYLIST_ADD);
|
||||
|
||||
for (const chunk of chunks) {
|
||||
const addRes = await jfApiClient(apiClientProps).addToPlaylist({
|
||||
body: null,
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
Ids: chunk.join(','),
|
||||
UserId: apiClientProps.server?.userId,
|
||||
},
|
||||
});
|
||||
|
||||
if (addRes.status !== 204) {
|
||||
throw new Error('Failed to add songs to playlist');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
scrobble: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { set } from 'idb-keyval';
|
||||
|
||||
import { ndApiClient } from '/@/renderer/api/navidrome/navidrome-api';
|
||||
import { ssApiClient } from '/@/renderer/api/subsonic/subsonic-api';
|
||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||
@@ -782,6 +784,95 @@ export const NavidromeController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
// 1. Fetch existing songs from the playlist without any sorts
|
||||
const existingSongsRes = await ndApiClient(apiClientProps as any).getPlaylistSongList({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
_end: -1,
|
||||
_order: 'ASC',
|
||||
_start: 0,
|
||||
...excludeMissing(apiClientProps.server),
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSongsRes.status !== 200) {
|
||||
throw new Error('Failed to fetch existing playlist songs');
|
||||
}
|
||||
|
||||
const existingSongs = existingSongsRes.body.data.map((item) =>
|
||||
ndNormalize.song(item, apiClientProps.server),
|
||||
);
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
const playlistDetailRes = await ndApiClient(apiClientProps).getPlaylistDetail({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (playlistDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get playlist detail');
|
||||
}
|
||||
|
||||
const playlist = ndNormalize.playlist(playlistDetailRes.body.data, apiClientProps.server);
|
||||
|
||||
// 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
|
||||
const backup = {
|
||||
id: query.id,
|
||||
name: playlist.name,
|
||||
songIds: existingSongs.map((song) => song.id),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store backup in IndexedDB using idb-keyval
|
||||
const backupKey = `playlist-backup-${query.id}`;
|
||||
await set(backupKey, backup);
|
||||
|
||||
// 4. Remove all songs from the playlist
|
||||
if (existingSongs.length > 0) {
|
||||
const existingPlaylistItemIds = existingSongs
|
||||
.map((song) => song.playlistItemId)
|
||||
.filter((id): id is string => id !== undefined && id !== null);
|
||||
|
||||
if (existingPlaylistItemIds.length > 0) {
|
||||
const removeRes = await ndApiClient(apiClientProps).removeFromPlaylist({
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
query: {
|
||||
id: existingPlaylistItemIds,
|
||||
},
|
||||
});
|
||||
|
||||
if (removeRes.status !== 200) {
|
||||
throw new Error('Failed to remove songs from playlist');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add the new song ids to the playlist
|
||||
if (body.songId.length > 0) {
|
||||
const addRes = await ndApiClient(apiClientProps).addToPlaylist({
|
||||
body: {
|
||||
ids: body.songId,
|
||||
},
|
||||
params: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (addRes.status !== 200) {
|
||||
throw new Error('Failed to add songs to playlist');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
scrobble: SubsonicController.scrobble,
|
||||
search: SubsonicController.search,
|
||||
setRating: SubsonicController.setRating,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { ServerInferResponses } from '@ts-rest/core';
|
||||
|
||||
import dayjs from 'dayjs';
|
||||
import { set } from 'idb-keyval';
|
||||
import filter from 'lodash/filter';
|
||||
import orderBy from 'lodash/orderBy';
|
||||
import md5 from 'md5';
|
||||
@@ -1479,6 +1480,87 @@ export const SubsonicController: InternalControllerEndpoint = {
|
||||
|
||||
return null;
|
||||
},
|
||||
replacePlaylist: async (args) => {
|
||||
const { apiClientProps, body, query } = args;
|
||||
|
||||
// 1. Fetch existing songs from the playlist
|
||||
const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (existingSongsRes.status !== 200) {
|
||||
throw new Error('Failed to fetch existing playlist songs');
|
||||
}
|
||||
|
||||
const existingSongs =
|
||||
existingSongsRes.body.playlist.entry?.map((song) =>
|
||||
ssNormalize.song(song, apiClientProps.server),
|
||||
) || [];
|
||||
|
||||
// 2. Get playlist detail to get the name
|
||||
const playlistDetailRes = await ssApiClient(apiClientProps).getPlaylist({
|
||||
query: {
|
||||
id: query.id,
|
||||
},
|
||||
});
|
||||
|
||||
if (playlistDetailRes.status !== 200) {
|
||||
throw new Error('Failed to get playlist detail');
|
||||
}
|
||||
|
||||
const playlist = ssNormalize.playlist(
|
||||
playlistDetailRes.body.playlist,
|
||||
apiClientProps.server,
|
||||
);
|
||||
|
||||
// 3. Make a backup of the playlist ids and their order, along with the id of the playlist and name
|
||||
const backup = {
|
||||
id: query.id,
|
||||
name: playlist.name,
|
||||
songIds: existingSongs.map((song) => song.id),
|
||||
timestamp: Date.now(),
|
||||
};
|
||||
|
||||
// Store backup in IndexedDB using idb-keyval
|
||||
const backupKey = `playlist-backup-${query.id}`;
|
||||
await set(backupKey, backup);
|
||||
|
||||
// 4. Remove all songs from the playlist (Subsonic uses indices, not IDs)
|
||||
if (existingSongs.length > 0) {
|
||||
// Get indices of all songs (0-based)
|
||||
// Remove in reverse order to avoid index shifting issues
|
||||
const songIndices = existingSongs.map((_, index) => index).reverse();
|
||||
|
||||
const removeRes = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
query: {
|
||||
playlistId: query.id,
|
||||
songIndexToRemove: songIndices.map((index) => index.toString()),
|
||||
},
|
||||
});
|
||||
|
||||
if (removeRes.status !== 200) {
|
||||
throw new Error('Failed to remove songs from playlist');
|
||||
}
|
||||
}
|
||||
|
||||
// 5. Add the new song ids to the playlist
|
||||
if (body.songId.length > 0) {
|
||||
const addRes = await ssApiClient(apiClientProps).updatePlaylist({
|
||||
query: {
|
||||
playlistId: query.id,
|
||||
songIdToAdd: body.songId,
|
||||
},
|
||||
});
|
||||
|
||||
if (addRes.status !== 200) {
|
||||
throw new Error('Failed to add songs to playlist');
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
scrobble: async (args) => {
|
||||
const { apiClientProps, query } = args;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user