add initial playlist reimplementation

This commit is contained in:
jeffvli
2025-11-27 14:04:33 -08:00
parent ac5de29c71
commit 092a9c3f19
18 changed files with 878 additions and 747 deletions
+284 -1
View File
@@ -1,10 +1,24 @@
import { AxiosHeaders } from 'axios';
import isElectron from 'is-electron';
import orderBy from 'lodash/orderBy';
import reverse from 'lodash/reverse';
import shuffle from 'lodash/shuffle';
import semverCoerce from 'semver/functions/coerce';
import semverGte from 'semver/functions/gte';
import { z } from 'zod';
import { ServerListItem } from '/@/shared/types/domain-types';
import {
Album,
AlbumArtist,
AlbumArtistListSort,
AlbumListSort,
ArtistListSort,
LibraryItem,
ServerListItem,
Song,
SongListSort,
SortOrder,
} from '/@/shared/types/domain-types';
import { ServerFeature } from '/@/shared/types/features-types';
// Since ts-rest client returns a strict response type, we need to add the headers to the body object
@@ -113,3 +127,272 @@ export const getClientType = (): string => {
};
export const SEPARATOR_STRING = ' · ';
export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => {
let results = songs;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case SongListSort.ALBUM:
results = orderBy(
results,
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, 'asc', 'asc'],
);
break;
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ARTIST:
results = orderBy(
results,
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.BPM:
results = orderBy(results, ['bpm'], [order]);
break;
case SongListSort.CHANNELS:
results = orderBy(results, ['channels'], [order]);
break;
case SongListSort.COMMENT:
results = orderBy(results, ['comment'], [order]);
break;
case SongListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case SongListSort.FAVORITED:
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.GENRE:
results = orderBy(
results,
[
(v) => v.genres?.[0]?.name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ID:
if (order === 'desc') {
results = reverse(results as any);
}
break;
case SongListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case SongListSort.RANDOM:
results = shuffle(results);
break;
case SongListSort.RATING:
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case SongListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case SongListSort.RELEASE_DATE:
results = orderBy(results, ['releaseDate'], [order]);
break;
case SongListSort.YEAR:
results = orderBy(
results,
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
[order, 'asc', 'asc', 'asc'],
);
break;
default:
break;
}
return results;
};
export const sortSongsByFetchedOrder = (
songs: Song[],
fetchedIds: string[],
itemType: LibraryItem,
): Song[] => {
// Group songs by the fetched ID they belong to
const songsByFetchedId = new Map<string, Song[]>();
for (const song of songs) {
let matchedId: string | undefined;
switch (itemType) {
case LibraryItem.ALBUM:
matchedId = fetchedIds.find((id) => song.albumId === id);
break;
case LibraryItem.ALBUM_ARTIST:
matchedId = fetchedIds.find((id) =>
song.albumArtists.some((artist) => artist.id === id),
);
break;
case LibraryItem.ARTIST:
matchedId = fetchedIds.find((id) =>
song.artists.some((artist) => artist.id === id),
);
break;
case LibraryItem.GENRE:
matchedId = fetchedIds.find((id) => song.genres.some((genre) => genre.id === id));
break;
case LibraryItem.PLAYLIST:
// For playlists, we might need to track which playlist each song came from
// This is a simplified approach - you may need to adjust based on your data structure
matchedId = fetchedIds.find((id) => song.playlistItemId === id);
break;
default:
break;
}
if (matchedId) {
if (!songsByFetchedId.has(matchedId)) {
songsByFetchedId.set(matchedId, []);
}
songsByFetchedId.get(matchedId)!.push(song);
}
}
// Sort each group by discNumber and trackNumber
for (const [fetchedId, groupSongs] of songsByFetchedId.entries()) {
const sortedGroup = orderBy(groupSongs, ['discNumber', 'trackNumber'], ['asc', 'asc']);
songsByFetchedId.set(fetchedId, sortedGroup);
}
// Combine groups in the order of fetchedIds
const result: Song[] = [];
for (const fetchedId of fetchedIds) {
const groupSongs = songsByFetchedId.get(fetchedId);
if (groupSongs) {
result.push(...groupSongs);
}
}
// Add any songs that didn't match any fetched ID at the end
const matchedIds = new Set(result.map((s) => s.id));
const unmatchedSongs = songs.filter((s) => !matchedIds.has(s.id));
if (unmatchedSongs.length > 0) {
const sortedUnmatched = orderBy(
unmatchedSongs,
['discNumber', 'trackNumber'],
['asc', 'asc'],
);
result.push(...sortedUnmatched);
}
return result;
};
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort | ArtistListSort,
sortOrder: SortOrder,
) => {
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
let results = artists;
switch (sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
break;
case AlbumArtistListSort.FAVORITED:
results = orderBy(artists, ['starred'], [order]);
break;
case AlbumArtistListSort.NAME:
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumArtistListSort.RATING:
results = orderBy(artists, ['userRating'], [order]);
break;
default:
break;
}
return results;
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
let results = albums;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case AlbumListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.name.toLowerCase()],
[order, 'asc'],
);
break;
case AlbumListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case AlbumListSort.FAVORITED:
results = orderBy(results, ['starred'], [order]);
break;
case AlbumListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case AlbumListSort.RANDOM:
results = shuffle(results);
break;
case AlbumListSort.RATING:
results = orderBy(results, ['userRating'], [order]);
break;
case AlbumListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case AlbumListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]);
break;
case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]);
break;
default:
break;
}
return results;
};
-274
View File
@@ -1,7 +1,3 @@
import orderBy from 'lodash/orderBy';
import reverse from 'lodash/reverse';
import shuffle from 'lodash/shuffle';
import {
JFAlbumArtistListSort,
JFAlbumListSort,
@@ -1434,273 +1430,3 @@ type BaseEndpointArgsWithServer = {
signal?: AbortSignal;
};
};
export const sortAlbumList = (albums: Album[], sortBy: AlbumListSort, sortOrder: SortOrder) => {
let results = albums;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case AlbumListSort.ALBUM_ARTIST:
results = orderBy(
results,
['albumArtist', (v) => v.name.toLowerCase()],
[order, 'asc'],
);
break;
case AlbumListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case AlbumListSort.FAVORITED:
results = orderBy(results, ['starred'], [order]);
break;
case AlbumListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case AlbumListSort.RANDOM:
results = shuffle(results);
break;
case AlbumListSort.RATING:
results = orderBy(results, ['userRating'], [order]);
break;
case AlbumListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case AlbumListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case AlbumListSort.SONG_COUNT:
results = orderBy(results, ['songCount'], [order]);
break;
case AlbumListSort.YEAR:
results = orderBy(results, ['releaseYear'], [order]);
break;
default:
break;
}
return results;
};
export const sortSongList = (songs: Song[], sortBy: SongListSort, sortOrder: SortOrder) => {
let results = songs;
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
switch (sortBy) {
case SongListSort.ALBUM:
results = orderBy(
results,
[(v) => v.album?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, 'asc', 'asc'],
);
break;
case SongListSort.ALBUM_ARTIST:
results = orderBy(
results,
[(v) => v.albumArtists[0]?.name.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ARTIST:
results = orderBy(
results,
[(v) => v.artistName?.toLowerCase(), 'discNumber', 'trackNumber'],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.BPM:
results = orderBy(results, ['bpm'], [order]);
break;
case SongListSort.CHANNELS:
results = orderBy(results, ['channels'], [order]);
break;
case SongListSort.COMMENT:
results = orderBy(results, ['comment'], [order]);
break;
case SongListSort.DURATION:
results = orderBy(results, ['duration'], [order]);
break;
case SongListSort.FAVORITED:
results = orderBy(results, ['userFavorite', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.GENRE:
results = orderBy(
results,
[
(v) => v.genres?.[0]?.name.toLowerCase(),
(v) => v.album?.toLowerCase(),
'discNumber',
'trackNumber',
],
[order, order, 'asc', 'asc'],
);
break;
case SongListSort.ID:
if (order === 'desc') {
results = reverse(results as any);
}
break;
case SongListSort.NAME:
results = orderBy(results, [(v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.PLAY_COUNT:
results = orderBy(results, ['playCount'], [order]);
break;
case SongListSort.RANDOM:
results = shuffle(results);
break;
case SongListSort.RATING:
results = orderBy(results, ['userRating', (v) => v.name.toLowerCase()], [order]);
break;
case SongListSort.RECENTLY_ADDED:
results = orderBy(results, ['createdAt'], [order]);
break;
case SongListSort.RECENTLY_PLAYED:
results = orderBy(results, ['lastPlayedAt'], [order]);
break;
case SongListSort.RELEASE_DATE:
results = orderBy(results, ['releaseDate'], [order]);
break;
case SongListSort.YEAR:
results = orderBy(
results,
['releaseYear', (v) => v.album?.toLowerCase(), 'discNumber', 'track'],
[order, 'asc', 'asc', 'asc'],
);
break;
default:
break;
}
return results;
};
export const sortSongsByFetchedOrder = (
songs: Song[],
fetchedIds: string[],
itemType: LibraryItem,
): Song[] => {
// Group songs by the fetched ID they belong to
const songsByFetchedId = new Map<string, Song[]>();
for (const song of songs) {
let matchedId: string | undefined;
switch (itemType) {
case LibraryItem.ALBUM:
matchedId = fetchedIds.find((id) => song.albumId === id);
break;
case LibraryItem.ALBUM_ARTIST:
matchedId = fetchedIds.find((id) =>
song.albumArtists.some((artist) => artist.id === id),
);
break;
case LibraryItem.ARTIST:
matchedId = fetchedIds.find((id) =>
song.artists.some((artist) => artist.id === id),
);
break;
case LibraryItem.GENRE:
matchedId = fetchedIds.find((id) => song.genres.some((genre) => genre.id === id));
break;
case LibraryItem.PLAYLIST:
// For playlists, we might need to track which playlist each song came from
// This is a simplified approach - you may need to adjust based on your data structure
matchedId = fetchedIds.find((id) => song.playlistItemId === id);
break;
default:
break;
}
if (matchedId) {
if (!songsByFetchedId.has(matchedId)) {
songsByFetchedId.set(matchedId, []);
}
songsByFetchedId.get(matchedId)!.push(song);
}
}
// Sort each group by discNumber and trackNumber
for (const [fetchedId, groupSongs] of songsByFetchedId.entries()) {
const sortedGroup = orderBy(groupSongs, ['discNumber', 'trackNumber'], ['asc', 'asc']);
songsByFetchedId.set(fetchedId, sortedGroup);
}
// Combine groups in the order of fetchedIds
const result: Song[] = [];
for (const fetchedId of fetchedIds) {
const groupSongs = songsByFetchedId.get(fetchedId);
if (groupSongs) {
result.push(...groupSongs);
}
}
// Add any songs that didn't match any fetched ID at the end
const matchedIds = new Set(result.map((s) => s.id));
const unmatchedSongs = songs.filter((s) => !matchedIds.has(s.id));
if (unmatchedSongs.length > 0) {
const sortedUnmatched = orderBy(
unmatchedSongs,
['discNumber', 'trackNumber'],
['asc', 'asc'],
);
result.push(...sortedUnmatched);
}
return result;
};
export const sortAlbumArtistList = (
artists: AlbumArtist[],
sortBy: AlbumArtistListSort | ArtistListSort,
sortOrder: SortOrder,
) => {
const order = sortOrder === SortOrder.ASC ? 'asc' : 'desc';
let results = artists;
switch (sortBy) {
case AlbumArtistListSort.ALBUM_COUNT:
results = orderBy(artists, ['albumCount', (v) => v.name.toLowerCase()], [order, 'asc']);
break;
case AlbumArtistListSort.FAVORITED:
results = orderBy(artists, ['starred'], [order]);
break;
case AlbumArtistListSort.NAME:
results = orderBy(artists, [(v) => v.name.toLowerCase()], [order]);
break;
case AlbumArtistListSort.RATING:
results = orderBy(artists, ['userRating'], [order]);
break;
default:
break;
}
return results;
};
+1
View File
@@ -21,6 +21,7 @@ export const DragTargetMap = {
[LibraryItem.ARTIST]: DragTarget.ARTIST,
[LibraryItem.GENRE]: DragTarget.GENRE,
[LibraryItem.PLAYLIST]: DragTarget.PLAYLIST,
[LibraryItem.PLAYLIST_SONG]: DragTarget.SONG,
[LibraryItem.QUEUE_SONG]: DragTarget.QUEUE_SONG,
[LibraryItem.SONG]: DragTarget.SONG,
};