mirror of
https://github.com/jeffvli/feishin.git
synced 2026-05-10 04:30:25 +02:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b95f47a91 | |||
| 2267e9bc9d | |||
| 089311c673 | |||
| 773f349b66 | |||
| 3980c8ea97 | |||
| 257a5ceef0 | |||
| fb022891fe | |||
| 5d9906b8f2 | |||
| 6f7cb468b2 | |||
| 076693e969 | |||
| 781d8055b5 | |||
| 960bb5c660 | |||
| 42bb2bf66f | |||
| f03d88cd8c | |||
| 58f6535ba6 | |||
| 9a59ce3613 |
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
|||||||
+1
-1
@@ -2,7 +2,7 @@
|
|||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"productName": "Feishin",
|
"productName": "Feishin",
|
||||||
"description": "Feishin music server",
|
"description": "Feishin music server",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
|
"build": "concurrently \"npm run build:main\" \"npm run build:renderer\" \"npm run build:remote\"",
|
||||||
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
"build:main": "cross-env NODE_ENV=production TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.main.prod.ts",
|
||||||
|
|||||||
Generated
+10
-9
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -1318,9 +1318,10 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/xml2js": {
|
"node_modules/xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||||
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"sax": ">=0.6.0",
|
"sax": ">=0.6.0",
|
||||||
"xmlbuilder": "~11.0.0"
|
"xmlbuilder": "~11.0.0"
|
||||||
@@ -1584,7 +1585,7 @@
|
|||||||
"jsbi": "^2.0.5",
|
"jsbi": "^2.0.5",
|
||||||
"long": "^4.0.0",
|
"long": "^4.0.0",
|
||||||
"safe-buffer": "^5.1.1",
|
"safe-buffer": "^5.1.1",
|
||||||
"xml2js": "^0.4.17"
|
"xml2js": "0.5.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"debug": {
|
"debug": {
|
||||||
@@ -2317,9 +2318,9 @@
|
|||||||
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
"integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="
|
||||||
},
|
},
|
||||||
"xml2js": {
|
"xml2js": {
|
||||||
"version": "0.4.23",
|
"version": "0.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.5.0.tgz",
|
||||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
"integrity": "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"sax": ">=0.6.0",
|
"sax": ">=0.6.0",
|
||||||
"xmlbuilder": "~11.0.0"
|
"xmlbuilder": "~11.0.0"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "feishin",
|
"name": "feishin",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "./dist/main/main.js",
|
"main": "./dist/main/main.js",
|
||||||
"author": {
|
"author": {
|
||||||
@@ -20,5 +20,11 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"electron": "36.1.0"
|
"electron": "36.1.0"
|
||||||
},
|
},
|
||||||
|
"resolutions": {
|
||||||
|
"xml2js": "0.5.0"
|
||||||
|
},
|
||||||
|
"overrides": {
|
||||||
|
"xml2js": "0.5.0"
|
||||||
|
},
|
||||||
"license": "GPL-3.0"
|
"license": "GPL-3.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -747,8 +747,8 @@
|
|||||||
"folderWithCount_few": "{{count}} složky",
|
"folderWithCount_few": "{{count}} složky",
|
||||||
"folderWithCount_other": "{{count}} složek",
|
"folderWithCount_other": "{{count}} složek",
|
||||||
"albumArtist_one": "umělec alba",
|
"albumArtist_one": "umělec alba",
|
||||||
"albumArtist_few": "umělci alba",
|
"albumArtist_few": "umělci alb",
|
||||||
"albumArtist_other": "umělců alba",
|
"albumArtist_other": "umělci alb",
|
||||||
"track_one": "skladba",
|
"track_one": "skladba",
|
||||||
"track_few": "skladby",
|
"track_few": "skladby",
|
||||||
"track_other": "skladby",
|
"track_other": "skladby",
|
||||||
|
|||||||
@@ -494,6 +494,9 @@ const createWindow = async (first = true) => {
|
|||||||
|
|
||||||
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
app.commandLine.appendSwitch('disable-features', 'HardwareMediaKeyHandling,MediaSessionService');
|
||||||
|
|
||||||
|
// https://github.com/electron/electron/issues/46538#issuecomment-2808806722
|
||||||
|
app.commandLine.appendSwitch('gtk-version', '3');
|
||||||
|
|
||||||
// Must duplicate with the one in renderer process settings.store.ts
|
// Must duplicate with the one in renderer process settings.store.ts
|
||||||
enum BindingActions {
|
enum BindingActions {
|
||||||
GLOBAL_SEARCH = 'globalSearch',
|
GLOBAL_SEARCH = 'globalSearch',
|
||||||
|
|||||||
@@ -695,58 +695,98 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
|
const yearsFilter = yearsGroup.length ? formatCommaDelimitedString(yearsGroup) : undefined;
|
||||||
const albumIdsFilter = query.albumIds
|
|
||||||
? formatCommaDelimitedString(query.albumIds)
|
|
||||||
: undefined;
|
|
||||||
const artistIdsFilter = query.artistIds
|
const artistIdsFilter = query.artistIds
|
||||||
? formatCommaDelimitedString(query.artistIds)
|
? formatCommaDelimitedString(query.artistIds)
|
||||||
: query.albumArtistIds
|
: query.albumArtistIds
|
||||||
? formatCommaDelimitedString(query.albumArtistIds)
|
? formatCommaDelimitedString(query.albumArtistIds)
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const res = await jfApiClient(apiClientProps).getSongList({
|
let items: z.infer<typeof jfType._response.song>[] = [];
|
||||||
params: {
|
let totalRecordCount = 0;
|
||||||
userId: apiClientProps.server?.userId,
|
const batchSize = 50;
|
||||||
},
|
|
||||||
query: {
|
|
||||||
AlbumIds: albumIdsFilter,
|
|
||||||
ArtistIds: artistIdsFilter,
|
|
||||||
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
|
||||||
GenreIds: query.genreIds?.join(','),
|
|
||||||
IncludeItemTypes: 'Audio',
|
|
||||||
IsFavorite: query.favorite,
|
|
||||||
Limit: query.limit,
|
|
||||||
ParentId: query.musicFolderId,
|
|
||||||
Recursive: true,
|
|
||||||
SearchTerm: query.searchTerm,
|
|
||||||
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
|
||||||
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
|
||||||
StartIndex: query.startIndex,
|
|
||||||
...query._custom?.jellyfin,
|
|
||||||
Years: yearsFilter,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.status !== 200) {
|
// Handle albumIds fetches in batches to prevent HTTP 414 errors
|
||||||
throw new Error('Failed to get song list');
|
if (query.albumIds && query.albumIds.length > batchSize) {
|
||||||
}
|
const albumIdBatches = chunk(query.albumIds, batchSize);
|
||||||
|
|
||||||
let items: z.infer<typeof jfType._response.song>[];
|
for (const batch of albumIdBatches) {
|
||||||
|
const albumIdsFilter = formatCommaDelimitedString(batch);
|
||||||
|
|
||||||
// Jellyfin Bodge because of code from https://github.com/jellyfin/jellyfin/blob/c566ccb63bf61f9c36743ddb2108a57c65a2519b/Emby.Server.Implementations/Data/SqliteItemRepository.cs#L3622
|
const res = await jfApiClient(apiClientProps).getSongList({
|
||||||
// If the Album ID filter is passed, Jellyfin will search for
|
params: {
|
||||||
// 1. the matching album id
|
userId: apiClientProps.server?.userId,
|
||||||
// 2. An album with the name of the album.
|
},
|
||||||
// It is this second condition causing issues,
|
query: {
|
||||||
if (query.albumIds) {
|
AlbumIds: albumIdsFilter,
|
||||||
const albumIdSet = new Set(query.albumIds);
|
ArtistIds: artistIdsFilter,
|
||||||
items = res.body.Items.filter((item) => albumIdSet.has(item.AlbumId!));
|
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||||
|
GenreIds: query.genreIds?.join(','),
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
IsFavorite: query.favorite,
|
||||||
|
Limit: query.limit,
|
||||||
|
ParentId: query.musicFolderId,
|
||||||
|
Recursive: true,
|
||||||
|
SearchTerm: query.searchTerm,
|
||||||
|
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
||||||
|
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||||
|
StartIndex: query.startIndex,
|
||||||
|
...query._custom?.jellyfin,
|
||||||
|
Years: yearsFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
if (items.length < res.body.Items.length) {
|
if (res.status !== 200) {
|
||||||
res.body.TotalRecordCount -= res.body.Items.length - items.length;
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
items = [...items, ...res.body.Items];
|
||||||
|
totalRecordCount += res.body.Items.length;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
items = res.body.Items;
|
const albumIdsFilter = query.albumIds
|
||||||
|
? formatCommaDelimitedString(query.albumIds)
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const res = await jfApiClient(apiClientProps).getSongList({
|
||||||
|
params: {
|
||||||
|
userId: apiClientProps.server?.userId,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
AlbumIds: albumIdsFilter,
|
||||||
|
ArtistIds: artistIdsFilter,
|
||||||
|
Fields: 'Genres, DateCreated, MediaSources, ParentId',
|
||||||
|
GenreIds: query.genreIds?.join(','),
|
||||||
|
IncludeItemTypes: 'Audio',
|
||||||
|
IsFavorite: query.favorite,
|
||||||
|
Limit: query.limit,
|
||||||
|
ParentId: query.musicFolderId,
|
||||||
|
Recursive: true,
|
||||||
|
SearchTerm: query.searchTerm,
|
||||||
|
SortBy: songListSortMap.jellyfin[query.sortBy] || 'Album,SortName',
|
||||||
|
SortOrder: sortOrderMap.jellyfin[query.sortOrder],
|
||||||
|
StartIndex: query.startIndex,
|
||||||
|
...query._custom?.jellyfin,
|
||||||
|
Years: yearsFilter,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('Failed to get song list');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Jellyfin Bodge because of code from https://github.com/jellyfin/jellyfin/blob/c566ccb63bf61f9c36743ddb2108a57c65a2519b/Emby.Server.Implementations/Data/SqliteItemRepository.cs#L3622
|
||||||
|
// If the Album ID filter is passed, Jellyfin will search for
|
||||||
|
// 1. the matching album id
|
||||||
|
// 2. An album with the name of the album.
|
||||||
|
// It is this second condition causing issues,
|
||||||
|
if (query.albumIds) {
|
||||||
|
const albumIdSet = new Set(query.albumIds);
|
||||||
|
items = res.body.Items.filter((item) => albumIdSet.has(item.AlbumId!));
|
||||||
|
totalRecordCount = items.length;
|
||||||
|
} else {
|
||||||
|
items = res.body.Items;
|
||||||
|
totalRecordCount = res.body.TotalRecordCount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -754,7 +794,7 @@ export const JellyfinController: ControllerEndpoint = {
|
|||||||
jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
|
jfNormalize.song(item, apiClientProps.server, '', query.imageSize),
|
||||||
),
|
),
|
||||||
startIndex: query.startIndex,
|
startIndex: query.startIndex,
|
||||||
totalRecordCount: res.body.TotalRecordCount,
|
totalRecordCount,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
getSongListCount: async ({ apiClientProps, query }) =>
|
getSongListCount: async ({ apiClientProps, query }) =>
|
||||||
|
|||||||
@@ -284,7 +284,7 @@ const normalizeAlbumArtist = (
|
|||||||
) || [];
|
) || [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: null,
|
albumCount: item.AlbumCount ?? null,
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: item.Overview || null,
|
biography: item.Overview || null,
|
||||||
duration: item.RunTimeTicks / 10000,
|
duration: item.RunTimeTicks / 10000,
|
||||||
@@ -308,7 +308,7 @@ const normalizeAlbumArtist = (
|
|||||||
serverId: server?.id || '',
|
serverId: server?.id || '',
|
||||||
serverType: ServerType.JELLYFIN,
|
serverType: ServerType.JELLYFIN,
|
||||||
similarArtists,
|
similarArtists,
|
||||||
songCount: null,
|
songCount: item.SongCount ?? null,
|
||||||
userFavorite: item.UserData?.IsFavorite || false,
|
userFavorite: item.UserData?.IsFavorite || false,
|
||||||
userRating: null,
|
userRating: null,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -431,6 +431,7 @@ const providerIds = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
|
AlbumCount: z.number().optional(),
|
||||||
BackdropImageTags: z.array(z.string()),
|
BackdropImageTags: z.array(z.string()),
|
||||||
ChannelId: z.null(),
|
ChannelId: z.null(),
|
||||||
DateCreated: z.string(),
|
DateCreated: z.string(),
|
||||||
@@ -446,6 +447,7 @@ const albumArtist = z.object({
|
|||||||
ProviderIds: providerIds.optional(),
|
ProviderIds: providerIds.optional(),
|
||||||
RunTimeTicks: z.number(),
|
RunTimeTicks: z.number(),
|
||||||
ServerId: z.string(),
|
ServerId: z.string(),
|
||||||
|
SongCount: z.number().optional(),
|
||||||
Type: z.string(),
|
Type: z.string(),
|
||||||
UserData: userData.optional(),
|
UserData: userData.optional(),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -278,11 +278,25 @@ const normalizeAlbumArtist = (
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let albumCount: number;
|
||||||
|
let songCount: number;
|
||||||
|
|
||||||
|
if (item.stats) {
|
||||||
|
albumCount = Math.max(
|
||||||
|
item.stats.albumartist?.albumCount ?? 0,
|
||||||
|
item.stats.artist?.albumCount ?? 0,
|
||||||
|
);
|
||||||
|
songCount = Math.max(
|
||||||
|
item.stats.albumartist?.songCount ?? 0,
|
||||||
|
item.stats.artist?.songCount ?? 0,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
albumCount = item.albumCount;
|
||||||
|
songCount = item.songCount;
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumCount: Math.max(
|
albumCount,
|
||||||
item.stats?.albumartist?.albumCount || item.albumCount,
|
|
||||||
item.stats?.artist?.albumCount || 0,
|
|
||||||
),
|
|
||||||
backgroundImageUrl: null,
|
backgroundImageUrl: null,
|
||||||
biography: item.biography || null,
|
biography: item.biography || null,
|
||||||
duration: null,
|
duration: null,
|
||||||
@@ -307,7 +321,7 @@ const normalizeAlbumArtist = (
|
|||||||
imageUrl: artist?.artistImageUrl || null,
|
imageUrl: artist?.artistImageUrl || null,
|
||||||
name: artist.name,
|
name: artist.name,
|
||||||
})) || null,
|
})) || null,
|
||||||
songCount: item.stats?.albumartist?.songCount || item.songCount,
|
songCount,
|
||||||
userFavorite: item.starred,
|
userFavorite: item.starred,
|
||||||
userRating: item.rating,
|
userRating: item.rating,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -190,7 +190,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
...ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||||
albums: artist.album.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)),
|
||||||
similarArtists:
|
similarArtists:
|
||||||
artistInfo?.similarArtist?.map((artist) =>
|
artistInfo?.similarArtist?.map((artist) =>
|
||||||
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
ssNormalize.albumArtist(artist, apiClientProps.server, 300),
|
||||||
@@ -305,7 +305,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return artist.body.artist.album;
|
return artist.body.artist.album ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -935,7 +935,9 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.albumIds || query.artistIds) {
|
const artistIds = query.albumArtistIds || query.artistIds;
|
||||||
|
|
||||||
|
if (query.albumIds || artistIds) {
|
||||||
if (query.albumIds) {
|
if (query.albumIds) {
|
||||||
for (const albumId of query.albumIds) {
|
for (const albumId of query.albumIds) {
|
||||||
fromAlbumPromises.push(
|
fromAlbumPromises.push(
|
||||||
@@ -948,8 +950,8 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.artistIds) {
|
if (artistIds) {
|
||||||
for (const artistId of query.artistIds) {
|
for (const artistId of artistIds) {
|
||||||
artistDetailPromises.push(
|
artistDetailPromises.push(
|
||||||
ssApiClient(apiClientProps).getArtist({
|
ssApiClient(apiClientProps).getArtist({
|
||||||
query: {
|
query: {
|
||||||
@@ -966,7 +968,7 @@ export const SubsonicController: ControllerEndpoint = {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return artist.body.artist.album;
|
return artist.body.artist.album ?? [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const albumIds = albums.map((album) => album.id);
|
const albumIds = albums.map((album) => album.id);
|
||||||
|
|||||||
@@ -156,7 +156,7 @@ const albumListParameters = z.object({
|
|||||||
const albumList = z.array(album.omit({ song: true }));
|
const albumList = z.array(album.omit({ song: true }));
|
||||||
|
|
||||||
const albumArtist = z.object({
|
const albumArtist = z.object({
|
||||||
album: z.array(album),
|
album: z.array(album).optional(),
|
||||||
albumCount: z.string(),
|
albumCount: z.string(),
|
||||||
artistImageUrl: z.string().optional(),
|
artistImageUrl: z.string().optional(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
|
|||||||
@@ -207,11 +207,14 @@ export const FeatureCarousel = ({ data }: FeatureCarouselProps) => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
))}
|
))}
|
||||||
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
<Badge size="lg">{currentItem?.releaseYear}</Badge>
|
||||||
<Badge size="lg">
|
{currentItem?.songCount !== null &&
|
||||||
{t('entity.trackWithCount', {
|
currentItem?.songCount !== undefined && (
|
||||||
count: currentItem?.songCount || 0,
|
<Badge size="lg">
|
||||||
})}
|
{t('entity.trackWithCount', {
|
||||||
</Badge>
|
count: currentItem?.songCount || 0,
|
||||||
|
})}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</Group>
|
</Group>
|
||||||
<Group position="apart">
|
<Group position="apart">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -388,8 +388,8 @@ export const useVirtualTable = <TFilter extends BaseQuery<any>>({
|
|||||||
break;
|
break;
|
||||||
case LibraryItem.ARTIST:
|
case LibraryItem.ARTIST:
|
||||||
navigate(
|
navigate(
|
||||||
generatePath(AppRoute.LIBRARY_ALBUM_ARTISTS_DETAIL, {
|
generatePath(AppRoute.LIBRARY_ARTISTS_DETAIL, {
|
||||||
albumArtistId: e.data.id,
|
artistId: e.data.id,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ const AlbumListRoute = () => {
|
|||||||
|
|
||||||
const artist = searchParams.get('artistName');
|
const artist = searchParams.get('artistName');
|
||||||
const title = artist
|
const title = artist
|
||||||
? t('page.albumList.artistAlbums', { artist })
|
? t('page.albumList.artistAlbums', { artist, postProcess: 'sentenceCase' })
|
||||||
: genreId
|
: genreId
|
||||||
? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) })
|
? t('page.albumList.genreAlbums', { genre: titleCase(genreTitle) })
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|||||||
@@ -297,7 +297,7 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
handlePlayQueueAdd?.({
|
handlePlayQueueAdd?.({
|
||||||
byItemType: {
|
byItemType: {
|
||||||
id: [routeId],
|
id: [routeId],
|
||||||
type: albumArtistId ? LibraryItem.ALBUM : LibraryItem.ALBUM_ARTIST,
|
type: albumArtistId ? LibraryItem.ALBUM_ARTIST : LibraryItem.ARTIST,
|
||||||
},
|
},
|
||||||
playType: playType || playButtonBehavior,
|
playType: playType || playButtonBehavior,
|
||||||
});
|
});
|
||||||
@@ -340,9 +340,15 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const albumCount = detailQuery?.data?.albumCount;
|
||||||
|
const artistContextItems =
|
||||||
|
(albumCount ?? 1) > 0
|
||||||
|
? ARTIST_CONTEXT_MENU_ITEMS
|
||||||
|
: ARTIST_CONTEXT_MENU_ITEMS.filter((item) => !item.id.toLowerCase().includes('play'));
|
||||||
|
|
||||||
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
const handleGeneralContextMenu = useHandleGeneralContextMenu(
|
||||||
LibraryItem.ALBUM_ARTIST,
|
LibraryItem.ALBUM_ARTIST,
|
||||||
ARTIST_CONTEXT_MENU_ITEMS,
|
artistContextItems,
|
||||||
);
|
);
|
||||||
|
|
||||||
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
const topSongs = topSongsQuery?.data?.items?.slice(0, 10);
|
||||||
@@ -369,7 +375,10 @@ export const AlbumArtistDetailContent = ({ background }: AlbumArtistDetailConten
|
|||||||
<LibraryBackgroundOverlay $backgroundColor={background} />
|
<LibraryBackgroundOverlay $backgroundColor={background} />
|
||||||
<DetailContainer>
|
<DetailContainer>
|
||||||
<Group spacing="md">
|
<Group spacing="md">
|
||||||
<PlayButton onClick={() => handlePlay(playButtonBehavior)} />
|
<PlayButton
|
||||||
|
disabled={albumCount === 0}
|
||||||
|
onClick={() => handlePlay(playButtonBehavior)}
|
||||||
|
/>
|
||||||
<Group spacing="xs">
|
<Group spacing="xs">
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
|||||||
@@ -28,25 +28,29 @@ export const AlbumArtistDetailHeader = forwardRef(
|
|||||||
serverId: server?.id,
|
serverId: server?.id,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const albumCount = detailQuery?.data?.albumCount;
|
||||||
|
const songCount = detailQuery?.data?.songCount;
|
||||||
|
const duration = detailQuery?.data?.duration;
|
||||||
|
const durationEnabled = duration !== null && duration !== undefined;
|
||||||
|
|
||||||
const metadataItems = [
|
const metadataItems = [
|
||||||
{
|
{
|
||||||
enabled: detailQuery?.data?.albumCount,
|
enabled: albumCount !== null && albumCount !== undefined,
|
||||||
id: 'albumCount',
|
id: 'albumCount',
|
||||||
secondary: false,
|
secondary: false,
|
||||||
value: t('entity.albumWithCount', { count: detailQuery?.data?.albumCount || 0 }),
|
value: t('entity.albumWithCount', { count: albumCount || 0 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: detailQuery?.data?.songCount,
|
enabled: songCount !== null && songCount !== undefined,
|
||||||
id: 'songCount',
|
id: 'songCount',
|
||||||
secondary: false,
|
secondary: false,
|
||||||
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
value: t('entity.trackWithCount', { count: songCount || 0 }),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enabled: detailQuery.data?.duration,
|
enabled: durationEnabled,
|
||||||
id: 'duration',
|
id: 'duration',
|
||||||
secondary: true,
|
secondary: true,
|
||||||
value:
|
value: durationEnabled && formatDurationString(duration),
|
||||||
detailQuery?.data?.duration && formatDurationString(detailQuery.data.duration),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { useCallback, useState, Fragment, useRef } from 'react';
|
import { useCallback, useState, Fragment, useRef } from 'react';
|
||||||
import { ActionIcon, Group, Kbd, ScrollArea } from '@mantine/core';
|
import { ActionIcon, Group, Kbd, ScrollArea } from '@mantine/core';
|
||||||
import { useDisclosure, useDebouncedValue } from '@mantine/hooks';
|
import { useDisclosure, useDebouncedValue } from '@mantine/hooks';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
import { RiSearchLine, RiCloseFill } from 'react-icons/ri';
|
import { RiSearchLine, RiCloseFill } from 'react-icons/ri';
|
||||||
import { generatePath, useNavigate } from 'react-router';
|
import { generatePath, useNavigate } from 'react-router';
|
||||||
import styled from 'styled-components';
|
import styled from 'styled-components';
|
||||||
@@ -37,6 +38,7 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
const activePage = pages[pages.length - 1];
|
const activePage = pages[pages.length - 1];
|
||||||
const isHome = activePage === CommandPalettePages.HOME;
|
const isHome = activePage === CommandPalettePages.HOME;
|
||||||
const searchInputRef = useRef<HTMLInputElement>(null);
|
const searchInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
const { t } = useTranslation();
|
||||||
|
|
||||||
const popPage = useCallback(() => {
|
const popPage = useCallback(() => {
|
||||||
setPages((pages) => {
|
setPages((pages) => {
|
||||||
@@ -187,13 +189,17 @@ export const CommandPalette = ({ modalProps }: CommandPaletteProps) => {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<LibraryCommandItem
|
<LibraryCommandItem
|
||||||
|
disabled={artist?.albumCount === 0}
|
||||||
handlePlayQueueAdd={handlePlayQueueAdd}
|
handlePlayQueueAdd={handlePlayQueueAdd}
|
||||||
id={artist.id}
|
id={artist.id}
|
||||||
imageUrl={artist.imageUrl}
|
imageUrl={artist.imageUrl}
|
||||||
itemType={LibraryItem.ALBUM_ARTIST}
|
itemType={LibraryItem.ALBUM_ARTIST}
|
||||||
subtitle={
|
subtitle={
|
||||||
(artist?.albumCount || 0) > 0
|
artist?.albumCount !== undefined &&
|
||||||
? `${artist.albumCount} albums`
|
artist?.albumCount !== null
|
||||||
|
? t('entity.albumWithCount', {
|
||||||
|
count: artist.albumCount,
|
||||||
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
title={artist.name}
|
title={artist.name}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ const StyledImage = styled.img`
|
|||||||
const ActionsContainer = styled(Flex)``;
|
const ActionsContainer = styled(Flex)``;
|
||||||
|
|
||||||
interface LibraryCommandItemProps {
|
interface LibraryCommandItemProps {
|
||||||
|
disabled?: boolean;
|
||||||
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
handlePlayQueueAdd?: (options: PlayQueueAddOptions) => void;
|
||||||
id: string;
|
id: string;
|
||||||
imageUrl: string | null;
|
imageUrl: string | null;
|
||||||
@@ -62,6 +63,7 @@ interface LibraryCommandItemProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const LibraryCommandItem = ({
|
export const LibraryCommandItem = ({
|
||||||
|
disabled,
|
||||||
id,
|
id,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
subtitle,
|
subtitle,
|
||||||
@@ -154,6 +156,7 @@ export const LibraryCommandItem = ({
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
disabled={disabled}
|
||||||
size="md"
|
size="md"
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.play', { postProcess: 'sentenceCase' }),
|
label: t('player.play', { postProcess: 'sentenceCase' }),
|
||||||
@@ -166,6 +169,7 @@ export const LibraryCommandItem = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
disabled={disabled}
|
||||||
size="md"
|
size="md"
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.addLast', { postProcess: 'sentenceCase' }),
|
label: t('player.addLast', { postProcess: 'sentenceCase' }),
|
||||||
@@ -179,6 +183,7 @@ export const LibraryCommandItem = ({
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
compact
|
compact
|
||||||
|
disabled={disabled}
|
||||||
size="md"
|
size="md"
|
||||||
tooltip={{
|
tooltip={{
|
||||||
label: t('player.addNext', { postProcess: 'sentenceCase' }),
|
label: t('player.addNext', { postProcess: 'sentenceCase' }),
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ const MotionButton = styled(UnstyledButton)`
|
|||||||
fill: var(--btn-filled-fg);
|
fill: var(--btn-filled-fg);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover:not([disabled]) {
|
||||||
background: var(--btn-filled-bg);
|
background: var(--btn-filled-bg);
|
||||||
transform: scale(1.1);
|
transform: scale(1.1);
|
||||||
|
|
||||||
@@ -28,6 +28,10 @@ const MotionButton = styled(UnstyledButton)`
|
|||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
transition: background-color 0.2s ease-in-out;
|
transition: background-color 0.2s ease-in-out;
|
||||||
transition: transform 0.2s ease-in-out;
|
transition: transform 0.2s ease-in-out;
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -734,6 +734,22 @@ export const useSettingsStore = create<SettingsSlice>()(
|
|||||||
),
|
),
|
||||||
{
|
{
|
||||||
merge: mergeOverridingColumns,
|
merge: mergeOverridingColumns,
|
||||||
|
migrate(persistedState, version) {
|
||||||
|
if (version === 8) {
|
||||||
|
const state = persistedState as SettingsSlice;
|
||||||
|
state.general.sidebarItems = state.general.sidebarItems.filter(
|
||||||
|
(item) => item.id !== 'Folders',
|
||||||
|
);
|
||||||
|
state.general.sidebarItems.push({
|
||||||
|
disabled: false,
|
||||||
|
id: 'Artists-all',
|
||||||
|
label: i18n.t('page.sidebar.artists'),
|
||||||
|
route: AppRoute.LIBRARY_ARTISTS,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return persistedState;
|
||||||
|
},
|
||||||
name: 'store_settings',
|
name: 'store_settings',
|
||||||
version: 9,
|
version: 9,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user