mirror of
https://github.com/antebudimir/feishin.git
synced 2026-01-02 10:53:33 +00:00
prepare bfr changes (#882)
* prepare bfr changes * contributors to subsonic/navidrome * show performer roles * Add BFR smart playlist fields * Fix upload-artifact action to handle v4 --------- Co-authored-by: jeffvli <jeffvictorli@gmail.com>
This commit is contained in:
parent
571aacbaa0
commit
c6d7dc0b32
13 changed files with 378 additions and 88 deletions
69
.github/workflows/publish-pr.yml
vendored
69
.github/workflows/publish-pr.yml
vendored
|
|
@ -11,7 +11,7 @@ jobs:
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [macos-latest]
|
os: [macos-latest, ubuntu-latest, windows-latest]
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout git repo
|
- name: Checkout git repo
|
||||||
|
|
@ -27,34 +27,69 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
npm install --legacy-peer-deps
|
npm install --legacy-peer-deps
|
||||||
|
|
||||||
- name: Build releases
|
- name: Build for Windows
|
||||||
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
uses: nick-invision/retry@v2.8.2
|
uses: nick-invision/retry@v2.8.2
|
||||||
with:
|
with:
|
||||||
timeout_minutes: 30
|
timeout_minutes: 30
|
||||||
max_attempts: 3
|
max_attempts: 3
|
||||||
retry_on: error
|
retry_on: error
|
||||||
command: |
|
command: |
|
||||||
npm run postinstall
|
npm run package:pr:windows
|
||||||
npm run build
|
|
||||||
npm run package:pr
|
|
||||||
on_retry_command: npm cache clean --force
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- name: Build for Linux
|
||||||
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
npm run package:pr:linux
|
||||||
|
|
||||||
|
- name: Build for MacOS
|
||||||
|
if: ${{ matrix.os == 'macos-latest' }}
|
||||||
|
uses: nick-invision/retry@v2.8.2
|
||||||
|
with:
|
||||||
|
timeout_minutes: 30
|
||||||
|
max_attempts: 3
|
||||||
|
retry_on: error
|
||||||
|
command: |
|
||||||
|
npm run package:pr:macos
|
||||||
|
|
||||||
|
- name: Zip Windows Binaries
|
||||||
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
Compress-Archive -Path "release/build/*.exe" -DestinationPath "release/build/windows-binaries.zip" -Force
|
||||||
|
|
||||||
|
- name: Zip Linux Binaries
|
||||||
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
run: |
|
||||||
|
zip -r release/build/linux-binaries.zip release/build/*.{AppImage,deb,rpm}
|
||||||
|
|
||||||
|
- name: Zip MacOS Binaries
|
||||||
|
if: ${{ matrix.os == 'macos-latest' }}
|
||||||
|
run: |
|
||||||
|
zip -r release/build/macos-binaries.zip release/build/*.dmg
|
||||||
|
|
||||||
|
- name: Upload Windows Binaries
|
||||||
|
if: ${{ matrix.os == 'windows-latest' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: windows-binaries
|
name: windows-binaries
|
||||||
path: |
|
path: release/build/windows-binaries.zip
|
||||||
release/build/*.exe
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- name: Upload Linux Binaries
|
||||||
|
if: ${{ matrix.os == 'ubuntu-latest' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: linux-binaries
|
name: linux-binaries
|
||||||
path: |
|
path: release/build/linux-binaries.zip
|
||||||
release/build/*.AppImage
|
|
||||||
release/build/*.deb
|
|
||||||
release/build/*.rpm
|
|
||||||
|
|
||||||
- uses: actions/upload-artifact@v3
|
- name: Upload MacOS Binaries
|
||||||
|
if: ${{ matrix.os == 'macos-latest' }}
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: macos-binaries
|
name: macos-binaries
|
||||||
path: |
|
path: release/build/macos-binaries.zip
|
||||||
release/build/*.dmg
|
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,9 @@
|
||||||
"lint:styles": "npx stylelint **/*.tsx --fix",
|
"lint:styles": "npx stylelint **/*.tsx --fix",
|
||||||
"package": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
"package": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never",
|
||||||
"package:pr": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
"package:pr": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win --mac --linux",
|
||||||
|
"package:pr:macos": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --mac",
|
||||||
|
"package:pr:windows": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --win",
|
||||||
|
"package:pr:linux": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --linux",
|
||||||
"package:dev": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
"package:dev": "node --import tsx ./.erb/scripts/clean.js dist && npm run build && electron-builder build --publish never --dir",
|
||||||
"postinstall": "node --import tsx .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
"postinstall": "node --import tsx .erb/scripts/check-native-dep.js && electron-builder install-app-deps && cross-env NODE_ENV=development TS_NODE_TRANSPILE_ONLY=true webpack --config ./.erb/configs/webpack.config.renderer.dev.dll.ts",
|
||||||
"start": "node --import tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
"start": "node --import tsx ./.erb/scripts/check-port-in-use.js && npm run start:renderer",
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
// Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>"
|
// Should follow a strict naming convention: "<FEATURE GROUP>_<FEATURE NAME>"
|
||||||
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
|
// For example: <FEATURE GROUP>: "Playlists", <FEATURE NAME>: "Smart" = "PLAYLISTS_SMART"
|
||||||
export enum ServerFeature {
|
export enum ServerFeature {
|
||||||
|
BFR = 'bfr',
|
||||||
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
LYRICS_MULTIPLE_STRUCTURED = 'lyricsMultipleStructured',
|
||||||
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
LYRICS_SINGLE_STRUCTURED = 'lyricsSingleStructured',
|
||||||
PLAYLISTS_SMART = 'playlistsSmart',
|
PLAYLISTS_SMART = 'playlistsSmart',
|
||||||
|
|
|
||||||
|
|
@ -176,6 +176,7 @@ const normalizeSong = (
|
||||||
lastPlayedAt: null,
|
lastPlayedAt: null,
|
||||||
lyrics: null,
|
lyrics: null,
|
||||||
name: item.Name,
|
name: item.Name,
|
||||||
|
participants: null,
|
||||||
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
|
path: (item.MediaSources && item.MediaSources[0]?.Path) || null,
|
||||||
peak: null,
|
peak: null,
|
||||||
playCount: (item.UserData && item.UserData.PlayCount) || 0,
|
playCount: (item.UserData && item.UserData.PlayCount) || 0,
|
||||||
|
|
@ -235,6 +236,7 @@ const normalizeAlbum = (
|
||||||
})),
|
})),
|
||||||
id: item.Id,
|
id: item.Id,
|
||||||
imagePlaceholderUrl: null,
|
imagePlaceholderUrl: null,
|
||||||
|
participants: null,
|
||||||
imageUrl: getAlbumCoverArtUrl({
|
imageUrl: getAlbumCoverArtUrl({
|
||||||
baseUrl: server?.url || '',
|
baseUrl: server?.url || '',
|
||||||
item,
|
item,
|
||||||
|
|
|
||||||
|
|
@ -381,39 +381,99 @@ export type NDPlaylistSongList = {
|
||||||
export const NDSongQueryFields = [
|
export const NDSongQueryFields = [
|
||||||
{ label: 'Album', type: 'string', value: 'album' },
|
{ label: 'Album', type: 'string', value: 'album' },
|
||||||
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
|
{ label: 'Album Artist', type: 'string', value: 'albumartist' },
|
||||||
|
{ label: 'Album Artists', type: 'string', value: 'albumartists' },
|
||||||
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
|
{ label: 'Album Comment', type: 'string', value: 'albumcomment' },
|
||||||
{ label: 'Album Type', type: 'string', value: 'albumtype' },
|
{ label: 'Album Type', type: 'string', value: 'albumtype' },
|
||||||
|
{ label: 'Album Version', type: 'string', value: 'albumversion' },
|
||||||
|
{ label: 'Arranger', type: 'string', value: 'arranger' },
|
||||||
{ label: 'Artist', type: 'string', value: 'artist' },
|
{ label: 'Artist', type: 'string', value: 'artist' },
|
||||||
|
{ label: 'Artists', type: 'string', value: 'artists' },
|
||||||
|
{ label: 'Barcode', type: 'string', value: 'barcode' },
|
||||||
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
|
{ label: 'Bitrate', type: 'number', value: 'bitrate' },
|
||||||
{ label: 'BPM', type: 'number', value: 'bpm' },
|
{ label: 'BPM', type: 'number', value: 'bpm' },
|
||||||
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
|
{ label: 'Catalog Number', type: 'string', value: 'catalognumber' },
|
||||||
{ label: 'Channels', type: 'number', value: 'channels' },
|
{ label: 'Channels', type: 'number', value: 'channels' },
|
||||||
{ label: 'Comment', type: 'string', value: 'comment' },
|
{ label: 'Comment', type: 'string', value: 'comment' },
|
||||||
|
{ label: 'Composer', type: 'string', value: 'composer' },
|
||||||
|
{ label: 'Conductor', type: 'string', value: 'conductor' },
|
||||||
|
{ label: 'Copyright', type: 'string', value: 'copyright' },
|
||||||
{ label: 'Date Added', type: 'date', value: 'dateadded' },
|
{ label: 'Date Added', type: 'date', value: 'dateadded' },
|
||||||
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
|
{ label: 'Date Favorited', type: 'date', value: 'dateloved' },
|
||||||
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
|
{ label: 'Date Last Played', type: 'date', value: 'lastplayed' },
|
||||||
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
|
{ label: 'Date Modified', type: 'date', value: 'datemodified' },
|
||||||
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
|
{ label: 'DJ Mixer', type: 'string', value: 'djmixer' },
|
||||||
|
{ label: 'Director', type: 'string', value: 'director' },
|
||||||
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
|
{ label: 'Disc Number', type: 'number', value: 'discnumber' },
|
||||||
|
{ label: 'Disc Subtitle', type: 'string', value: 'discsubtitle' },
|
||||||
|
{ label: 'Disc Total', type: 'number', value: 'disctotal' },
|
||||||
{ label: 'Duration', type: 'number', value: 'duration' },
|
{ label: 'Duration', type: 'number', value: 'duration' },
|
||||||
|
{ label: 'Encoded By', type: 'string', value: 'encodedby' },
|
||||||
|
{ label: 'Encoder Settings', type: 'string', value: 'encodersettings' },
|
||||||
|
{ label: 'Engineer', type: 'string', value: 'engineer' },
|
||||||
|
{ label: 'Explicit Status', type: 'string', value: 'explicitstatus' },
|
||||||
{ label: 'File Path', type: 'string', value: 'filepath' },
|
{ label: 'File Path', type: 'string', value: 'filepath' },
|
||||||
{ label: 'File Type', type: 'string', value: 'filetype' },
|
{ label: 'File Type', type: 'string', value: 'filetype' },
|
||||||
{ label: 'Genre', type: 'string', value: 'genre' },
|
{ label: 'Genre', type: 'string', value: 'genre' },
|
||||||
|
{ label: 'Grouping', type: 'string', value: 'grouping' },
|
||||||
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
|
{ label: 'Has CoverArt', type: 'boolean', value: 'hascoverart' },
|
||||||
{ label: 'Playlist', type: 'playlist', value: 'id' },
|
|
||||||
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
|
{ label: 'Is Compilation', type: 'boolean', value: 'compilation' },
|
||||||
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
|
{ label: 'Is Favorite', type: 'boolean', value: 'loved' },
|
||||||
|
{ label: 'ISRC', type: 'string', value: 'isrc' },
|
||||||
|
{ label: 'Key', type: 'string', value: 'key' },
|
||||||
|
{ label: 'Language', type: 'string', value: 'language' },
|
||||||
|
{ label: 'License', type: 'string', value: 'license' },
|
||||||
|
{ label: 'Lyricist', type: 'string', value: 'lyricist' },
|
||||||
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
|
{ label: 'Lyrics', type: 'string', value: 'lyrics' },
|
||||||
|
{ label: 'Media', type: 'string', value: 'media' },
|
||||||
|
{ label: 'Mixer', type: 'string', value: 'mixer' },
|
||||||
|
{ label: 'Mood', type: 'string', value: 'mood' },
|
||||||
|
{ label: 'Movement', type: 'string', value: 'movement' },
|
||||||
|
{ label: 'Movement Name', type: 'string', value: 'movementname' },
|
||||||
|
{ label: 'Movement Total', type: 'number', value: 'movementtotal' },
|
||||||
|
{ label: 'MusicBrainz Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
|
||||||
|
{ label: 'MusicBrainz Album Artist Id', type: 'string', value: 'musicbrainz_albumartistid' },
|
||||||
|
{ label: 'MusicBrainz Album Id', type: 'string', value: 'musicbrainz_albumid' },
|
||||||
|
{ label: 'MusicBrainz Disc Id', type: 'string', value: 'musicbrainz_discid' },
|
||||||
|
{ label: 'MusicBrainz Recording Id', type: 'string', value: 'musicbrainz_recordingid' },
|
||||||
|
{ label: 'MusicBrainz Release Group Id', type: 'string', value: 'musicbrainz_releasegroupid' },
|
||||||
|
{ label: 'MusicBrainz Track Id', type: 'string', value: 'musicbrainz_trackid' },
|
||||||
|
{ label: 'MusicBrainz Work Id', type: 'string', value: 'musicbrainz_workid' },
|
||||||
{ label: 'Name', type: 'string', value: 'title' },
|
{ label: 'Name', type: 'string', value: 'title' },
|
||||||
|
{ label: 'Original Date', type: 'date', value: 'originaldate' },
|
||||||
|
{ label: 'Performer', type: 'string', value: 'performer' },
|
||||||
{ label: 'Play Count', type: 'number', value: 'playcount' },
|
{ label: 'Play Count', type: 'number', value: 'playcount' },
|
||||||
|
{ label: 'Playlist', type: 'playlist', value: 'id' },
|
||||||
|
{ label: 'Producer', type: 'string', value: 'producer' },
|
||||||
|
{ label: 'R128 Album Gain', type: 'number', value: 'r128_album_gain' },
|
||||||
|
{ label: 'R128 Track Gain', type: 'number', value: 'r128_track_gain' },
|
||||||
{ label: 'Rating', type: 'number', value: 'rating' },
|
{ label: 'Rating', type: 'number', value: 'rating' },
|
||||||
|
{ label: 'Record Label', type: 'string', value: 'recordlabel' },
|
||||||
|
{ label: 'Recording Date', type: 'date', value: 'recordingdate' },
|
||||||
|
{ label: 'Release Country', type: 'string', value: 'releasecountry' },
|
||||||
|
{ label: 'Release Date', type: 'date', value: 'releasedate' },
|
||||||
|
{ label: 'Release Status', type: 'string', value: 'releasestatus' },
|
||||||
|
{ label: 'Release Type', type: 'string', value: 'releasetype' },
|
||||||
|
{ label: 'ReplayGain Album Gain', type: 'number', value: 'replaygain_album_gain' },
|
||||||
|
{ label: 'ReplayGain Album Peak', type: 'number', value: 'replaygain_album_peak' },
|
||||||
|
{ label: 'ReplayGain Track Gain', type: 'number', value: 'replaygain_track_gain' },
|
||||||
|
{ label: 'ReplayGain Track Peak', type: 'number', value: 'replaygain_track_peak' },
|
||||||
|
{ label: 'Remixer', type: 'string', value: 'remixer' },
|
||||||
|
{ label: 'Script', type: 'string', value: 'script' },
|
||||||
{ label: 'Size', type: 'number', value: 'size' },
|
{ label: 'Size', type: 'number', value: 'size' },
|
||||||
{ label: 'Sort Album', type: 'string', value: 'sortalbum' },
|
{ label: 'Sort Album', type: 'string', value: 'albumsort' },
|
||||||
{ label: 'Sort Album Artist', type: 'string', value: 'sortalbumartist' },
|
{ label: 'Sort Album Artist', type: 'string', value: 'albumartistsort' },
|
||||||
{ label: 'Sort Artist', type: 'string', value: 'sortartist' },
|
{ label: 'Sort Album Artists', type: 'string', value: 'albumartistssort' },
|
||||||
{ label: 'Sort Name', type: 'string', value: 'sorttitle' },
|
{ label: 'Sort Artist', type: 'string', value: 'artistsort' },
|
||||||
{ label: 'Track Number', type: 'number', value: 'tracknumber' },
|
{ label: 'Sort Artists', type: 'string', value: 'artistssort' },
|
||||||
|
{ label: 'Sort Composer', type: 'string', value: 'composersort' },
|
||||||
|
{ label: 'Sort Lyricist', type: 'string', value: 'lyricistsort' },
|
||||||
|
{ label: 'Sort Name', type: 'string', value: 'titlesort' },
|
||||||
|
{ label: 'Subtitle', type: 'string', value: 'subtitle' },
|
||||||
|
{ label: 'Track Number', type: 'number', value: 'track' },
|
||||||
|
{ label: 'Track Total', type: 'number', value: 'tracktotal' },
|
||||||
{ label: 'Year', type: 'number', value: 'year' },
|
{ label: 'Year', type: 'number', value: 'year' },
|
||||||
|
{ label: 'Website', type: 'string', value: 'website' },
|
||||||
|
{ label: 'Work', type: 'string', value: 'work' },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const NDSongQueryPlaylistOperators = [
|
export const NDSongQueryPlaylistOperators = [
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
genreListSortMap,
|
genreListSortMap,
|
||||||
Song,
|
Song,
|
||||||
ControllerEndpoint,
|
ControllerEndpoint,
|
||||||
|
ServerListItem,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
|
import { VersionInfo, getFeatures, hasFeature } from '/@/renderer/api/utils';
|
||||||
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
|
import { ServerFeature, ServerFeatures } from '/@/renderer/api/features-types';
|
||||||
|
|
@ -24,10 +25,19 @@ import { ssNormalize } from '/@/renderer/api/subsonic/subsonic-normalize';
|
||||||
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller';
|
||||||
|
|
||||||
const VERSION_INFO: VersionInfo = [
|
const VERSION_INFO: VersionInfo = [
|
||||||
|
['0.55.0', { [ServerFeature.BFR]: [1] }],
|
||||||
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
['0.49.3', { [ServerFeature.SHARING_ALBUM_SONG]: [1] }],
|
||||||
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
['0.48.0', { [ServerFeature.PLAYLISTS_SMART]: [1] }],
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const excludeMissing = (server: ServerListItem | null) => {
|
||||||
|
if (hasFeature(server, ServerFeature.BFR)) {
|
||||||
|
return { missing: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
export const NavidromeController: ControllerEndpoint = {
|
export const NavidromeController: ControllerEndpoint = {
|
||||||
addToPlaylist: async (args) => {
|
addToPlaylist: async (args) => {
|
||||||
const { body, query, apiClientProps } = args;
|
const { body, query, apiClientProps } = args;
|
||||||
|
|
@ -159,6 +169,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
_start: query.startIndex,
|
_start: query.startIndex,
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
|
role: hasFeature(apiClientProps.server, ServerFeature.BFR) ? 'albumartist' : '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -231,6 +242,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
name: query.searchTerm,
|
name: query.searchTerm,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
starred: query.favorite,
|
starred: query.favorite,
|
||||||
|
...excludeMissing(apiClientProps.server),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -367,6 +379,10 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
throw new Error('Failed to ping server');
|
throw new Error('Failed to ping server');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ping.body.serverVersion?.includes('pr-2709')) {
|
||||||
|
ping.body.serverVersion = '0.55.0';
|
||||||
|
}
|
||||||
|
|
||||||
const navidromeFeatures: Record<string, number[]> = getFeatures(
|
const navidromeFeatures: Record<string, number[]> = getFeatures(
|
||||||
VERSION_INFO,
|
VERSION_INFO,
|
||||||
ping.body.serverVersion!,
|
ping.body.serverVersion!,
|
||||||
|
|
@ -390,6 +406,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
}
|
}
|
||||||
|
|
||||||
const features: ServerFeatures = {
|
const features: ServerFeatures = {
|
||||||
|
bfr: !!navidromeFeatures[ServerFeature.BFR],
|
||||||
lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS],
|
lyricsMultipleStructured: !!navidromeFeatures[SubsonicExtensions.SONG_LYRICS],
|
||||||
playlistsSmart: !!navidromeFeatures[ServerFeature.PLAYLISTS_SMART],
|
playlistsSmart: !!navidromeFeatures[ServerFeature.PLAYLISTS_SMART],
|
||||||
publicPlaylist: true,
|
publicPlaylist: true,
|
||||||
|
|
@ -479,6 +496,7 @@ export const NavidromeController: ControllerEndpoint = {
|
||||||
starred: query.favorite,
|
starred: query.favorite,
|
||||||
title: query.searchTerm,
|
title: query.searchTerm,
|
||||||
...query._custom?.navidrome,
|
...query._custom?.navidrome,
|
||||||
|
...excludeMissing(apiClientProps.server),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ import {
|
||||||
Genre,
|
Genre,
|
||||||
ServerListItem,
|
ServerListItem,
|
||||||
ServerType,
|
ServerType,
|
||||||
|
RelatedArtist,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
import z from 'zod';
|
import z from 'zod';
|
||||||
import { ndType } from './navidrome-types';
|
import { ndType } from './navidrome-types';
|
||||||
|
|
@ -54,6 +55,70 @@ const normalizePlayDate = (item: WithDate): string | null => {
|
||||||
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
|
return !item.playDate || item.playDate.includes('0001-') ? null : item.playDate;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getArtists = (
|
||||||
|
item:
|
||||||
|
| z.infer<typeof ndType._response.song>
|
||||||
|
| z.infer<typeof ndType._response.playlistSong>
|
||||||
|
| z.infer<typeof ndType._response.album>,
|
||||||
|
) => {
|
||||||
|
let albumArtists: RelatedArtist[] | undefined;
|
||||||
|
let artists: RelatedArtist[] | undefined;
|
||||||
|
let participants: Record<string, RelatedArtist[]> | null = null;
|
||||||
|
|
||||||
|
if (item.participants) {
|
||||||
|
participants = {};
|
||||||
|
for (const [role, list] of Object.entries(item.participants)) {
|
||||||
|
if (role === 'albumartist' || role === 'artist') {
|
||||||
|
const roleList = list.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
imageUrl: null,
|
||||||
|
name: item.name,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (role === 'albumartist') {
|
||||||
|
albumArtists = roleList;
|
||||||
|
} else {
|
||||||
|
artists = roleList;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const subRoles = new Map<string | undefined, RelatedArtist[]>();
|
||||||
|
|
||||||
|
for (const artist of list) {
|
||||||
|
const item: RelatedArtist = {
|
||||||
|
id: artist.id,
|
||||||
|
imageUrl: null,
|
||||||
|
name: artist.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (subRoles.has(artist.subRole)) {
|
||||||
|
subRoles.get(artist.subRole)!.push(item);
|
||||||
|
} else {
|
||||||
|
subRoles.set(artist.subRole, [item]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [subRole, items] of subRoles.entries()) {
|
||||||
|
if (subRole) {
|
||||||
|
participants[`${role} (${subRole})`] = items;
|
||||||
|
} else {
|
||||||
|
participants[role] = items;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (albumArtists === undefined) {
|
||||||
|
albumArtists = [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (artists === undefined) {
|
||||||
|
artists = [{ id: item.artistId, imageUrl: null, name: item.artist }];
|
||||||
|
}
|
||||||
|
|
||||||
|
return { albumArtists, artists, participants };
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
|
item: z.infer<typeof ndType._response.song> | z.infer<typeof ndType._response.playlistSong>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
|
|
@ -80,10 +145,9 @@ const normalizeSong = (
|
||||||
const imagePlaceholderUrl = null;
|
const imagePlaceholderUrl = null;
|
||||||
return {
|
return {
|
||||||
album: item.album,
|
album: item.album,
|
||||||
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
|
|
||||||
albumId: item.albumId,
|
albumId: item.albumId,
|
||||||
|
...getArtists(item),
|
||||||
artistName: item.artist,
|
artistName: item.artist,
|
||||||
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
|
||||||
bitRate: item.bitRate,
|
bitRate: item.bitRate,
|
||||||
bpm: item.bpm ? item.bpm : null,
|
bpm: item.bpm ? item.bpm : null,
|
||||||
channels: item.channels ? item.channels : null,
|
channels: item.channels ? item.channels : null,
|
||||||
|
|
@ -116,7 +180,7 @@ const normalizeSong = (
|
||||||
item.rgAlbumPeak || item.rgTrackPeak
|
item.rgAlbumPeak || item.rgTrackPeak
|
||||||
? { album: item.rgAlbumPeak, track: item.rgTrackPeak }
|
? { album: item.rgAlbumPeak, track: item.rgTrackPeak }
|
||||||
: null,
|
: null,
|
||||||
playCount: item.playCount,
|
playCount: item.playCount || 0,
|
||||||
playlistItemId,
|
playlistItemId,
|
||||||
releaseDate: (item.releaseDate
|
releaseDate: (item.releaseDate
|
||||||
? new Date(item.releaseDate)
|
? new Date(item.releaseDate)
|
||||||
|
|
@ -155,12 +219,11 @@ const normalizeAlbum = (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtist: item.albumArtist,
|
albumArtist: item.albumArtist,
|
||||||
albumArtists: [{ id: item.albumArtistId, imageUrl: null, name: item.albumArtist }],
|
...getArtists(item),
|
||||||
artists: [{ id: item.artistId, imageUrl: null, name: item.artist }],
|
|
||||||
backdropImageUrl: imageBackdropUrl,
|
backdropImageUrl: imageBackdropUrl,
|
||||||
comment: item.comment || null,
|
comment: item.comment || null,
|
||||||
createdAt: item.createdAt.split('T')[0],
|
createdAt: item.createdAt.split('T')[0],
|
||||||
duration: item.duration * 1000 || null,
|
duration: item.duration !== undefined ? item.duration * 1000 : null,
|
||||||
genres: (item.genres || []).map((genre) => ({
|
genres: (item.genres || []).map((genre) => ({
|
||||||
id: genre.id,
|
id: genre.id,
|
||||||
imageUrl: null,
|
imageUrl: null,
|
||||||
|
|
@ -180,7 +243,7 @@ const normalizeAlbum = (
|
||||||
: item.originalYear
|
: item.originalYear
|
||||||
? new Date(item.originalYear, 0, 1).toISOString()
|
? new Date(item.originalYear, 0, 1).toISOString()
|
||||||
: null,
|
: null,
|
||||||
playCount: item.playCount,
|
playCount: item.playCount || 0,
|
||||||
releaseDate: (item.releaseDate
|
releaseDate: (item.releaseDate
|
||||||
? new Date(item.releaseDate)
|
? new Date(item.releaseDate)
|
||||||
: new Date(item.minYear, 0, 1)
|
: new Date(item.minYear, 0, 1)
|
||||||
|
|
@ -232,7 +295,7 @@ const normalizeAlbumArtist = (
|
||||||
lastPlayedAt: normalizePlayDate(item),
|
lastPlayedAt: normalizePlayDate(item),
|
||||||
mbz: item.mbzArtistId || null,
|
mbz: item.mbzArtistId || null,
|
||||||
name: item.name,
|
name: item.name,
|
||||||
playCount: item.playCount,
|
playCount: item.playCount || 0,
|
||||||
serverId: server?.id || 'unknown',
|
serverId: server?.id || 'unknown',
|
||||||
serverType: ServerType.NAVIDROME,
|
serverType: ServerType.NAVIDROME,
|
||||||
similarArtists:
|
similarArtists:
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ const albumArtist = z.object({
|
||||||
mediumImageUrl: z.string().optional(),
|
mediumImageUrl: z.string().optional(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
orderArtistName: z.string(),
|
orderArtistName: z.string(),
|
||||||
playCount: z.number(),
|
playCount: z.number().optional(),
|
||||||
playDate: z.string().optional(),
|
playDate: z.string().optional(),
|
||||||
rating: z.number(),
|
rating: z.number(),
|
||||||
size: z.number(),
|
size: z.number(),
|
||||||
|
|
@ -98,10 +98,20 @@ const albumArtistList = z.array(albumArtist);
|
||||||
const albumArtistListParameters = paginationParameters.extend({
|
const albumArtistListParameters = paginationParameters.extend({
|
||||||
_sort: z.nativeEnum(NDAlbumArtistListSort).optional(),
|
_sort: z.nativeEnum(NDAlbumArtistListSort).optional(),
|
||||||
genre_id: z.string().optional(),
|
genre_id: z.string().optional(),
|
||||||
|
missing: z.boolean().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
role: z.string().optional(),
|
||||||
starred: z.boolean().optional(),
|
starred: z.boolean().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const participant = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
subRole: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const participants = z.record(z.string(), z.array(participant));
|
||||||
|
|
||||||
const album = z.object({
|
const album = z.object({
|
||||||
albumArtist: z.string(),
|
albumArtist: z.string(),
|
||||||
albumArtistId: z.string(),
|
albumArtistId: z.string(),
|
||||||
|
|
@ -113,7 +123,7 @@ const album = z.object({
|
||||||
coverArtId: z.string().optional(), // Removed after v0.48.0
|
coverArtId: z.string().optional(), // Removed after v0.48.0
|
||||||
coverArtPath: z.string().optional(), // Removed after v0.48.0
|
coverArtPath: z.string().optional(), // Removed after v0.48.0
|
||||||
createdAt: z.string(),
|
createdAt: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number().optional(),
|
||||||
fullText: z.string(),
|
fullText: z.string(),
|
||||||
genre: z.string(),
|
genre: z.string(),
|
||||||
genres: z.array(genre).nullable(),
|
genres: z.array(genre).nullable(),
|
||||||
|
|
@ -127,7 +137,8 @@ const album = z.object({
|
||||||
orderAlbumName: z.string(),
|
orderAlbumName: z.string(),
|
||||||
originalDate: z.string().optional(),
|
originalDate: z.string().optional(),
|
||||||
originalYear: z.number().optional(),
|
originalYear: z.number().optional(),
|
||||||
playCount: z.number(),
|
participants: z.optional(participants),
|
||||||
|
playCount: z.number().optional(),
|
||||||
playDate: z.string().optional(),
|
playDate: z.string().optional(),
|
||||||
rating: z.number().optional(),
|
rating: z.number().optional(),
|
||||||
releaseDate: z.string().optional(),
|
releaseDate: z.string().optional(),
|
||||||
|
|
@ -195,8 +206,9 @@ const song = z.object({
|
||||||
orderAlbumName: z.string(),
|
orderAlbumName: z.string(),
|
||||||
orderArtistName: z.string(),
|
orderArtistName: z.string(),
|
||||||
orderTitle: z.string(),
|
orderTitle: z.string(),
|
||||||
|
participants: z.optional(participants),
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
playCount: z.number(),
|
playCount: z.number().optional(),
|
||||||
playDate: z.string().optional(),
|
playDate: z.string().optional(),
|
||||||
rating: z.number().optional(),
|
rating: z.number().optional(),
|
||||||
releaseDate: z.string().optional(),
|
releaseDate: z.string().optional(),
|
||||||
|
|
@ -211,6 +223,7 @@ const song = z.object({
|
||||||
starred: z.boolean(),
|
starred: z.boolean(),
|
||||||
starredAt: z.string().optional(),
|
starredAt: z.string().optional(),
|
||||||
suffix: z.string(),
|
suffix: z.string(),
|
||||||
|
tags: z.record(z.string(), z.array(z.string())).optional(),
|
||||||
title: z.string(),
|
title: z.string(),
|
||||||
trackNumber: z.number(),
|
trackNumber: z.number(),
|
||||||
updatedAt: z.string(),
|
updatedAt: z.string(),
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@ import {
|
||||||
ServerType,
|
ServerType,
|
||||||
Playlist,
|
Playlist,
|
||||||
Genre,
|
Genre,
|
||||||
|
RelatedArtist,
|
||||||
} from '/@/renderer/api/types';
|
} from '/@/renderer/api/types';
|
||||||
|
|
||||||
const getCoverArtUrl = (args: {
|
const getCoverArtUrl = (args: {
|
||||||
|
|
@ -34,6 +35,92 @@ const getCoverArtUrl = (args: {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getArtists = (
|
||||||
|
item:
|
||||||
|
| z.infer<typeof ssType._response.song>
|
||||||
|
| z.infer<typeof ssType._response.album>
|
||||||
|
| z.infer<typeof ssType._response.albumListEntry>,
|
||||||
|
) => {
|
||||||
|
const albumArtists: RelatedArtist[] = item.albumArtists
|
||||||
|
? item.albumArtists.map((item) => ({
|
||||||
|
id: item.id.toString(),
|
||||||
|
imageUrl: null,
|
||||||
|
name: item.name,
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: item.artistId?.toString() || '',
|
||||||
|
imageUrl: null,
|
||||||
|
name: item.artist || '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const artists: RelatedArtist[] = item.artists
|
||||||
|
? item.artists.map((item) => ({
|
||||||
|
id: item.id.toString(),
|
||||||
|
imageUrl: null,
|
||||||
|
name: item.name,
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: item.artistId?.toString() || '',
|
||||||
|
imageUrl: null,
|
||||||
|
name: item.artist || '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
let participants: Record<string, RelatedArtist[]> | null = null;
|
||||||
|
|
||||||
|
if (item.contributors) {
|
||||||
|
participants = {};
|
||||||
|
|
||||||
|
for (const contributor of item.contributors) {
|
||||||
|
const artist = {
|
||||||
|
id: contributor.artist.id?.toString() || '',
|
||||||
|
imageUrl: null,
|
||||||
|
name: contributor.artist.name || '',
|
||||||
|
};
|
||||||
|
|
||||||
|
const role = contributor.subRole
|
||||||
|
? `${contributor.role} (${contributor.subRole})`
|
||||||
|
: contributor.role;
|
||||||
|
|
||||||
|
if (role in participants) {
|
||||||
|
participants[role].push(artist);
|
||||||
|
} else {
|
||||||
|
participants[role] = [artist];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { albumArtists, artists, participants };
|
||||||
|
};
|
||||||
|
|
||||||
|
const getGenres = (
|
||||||
|
item:
|
||||||
|
| z.infer<typeof ssType._response.song>
|
||||||
|
| z.infer<typeof ssType._response.album>
|
||||||
|
| z.infer<typeof ssType._response.albumListEntry>,
|
||||||
|
): Genre[] => {
|
||||||
|
return item.genres
|
||||||
|
? item.genres.map((genre) => ({
|
||||||
|
id: genre.name,
|
||||||
|
imageUrl: null,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
name: genre.name,
|
||||||
|
}))
|
||||||
|
: item.genre
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
id: item.genre,
|
||||||
|
imageUrl: null,
|
||||||
|
itemType: LibraryItem.GENRE,
|
||||||
|
name: item.genre,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: [];
|
||||||
|
};
|
||||||
|
|
||||||
const normalizeSong = (
|
const normalizeSong = (
|
||||||
item: z.infer<typeof ssType._response.song>,
|
item: z.infer<typeof ssType._response.song>,
|
||||||
server: ServerListItem | null,
|
server: ServerListItem | null,
|
||||||
|
|
@ -51,22 +138,9 @@ const normalizeSong = (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
album: item.album || '',
|
album: item.album || '',
|
||||||
albumArtists: [
|
|
||||||
{
|
|
||||||
id: item.artistId?.toString() || '',
|
|
||||||
imageUrl: null,
|
|
||||||
name: item.artist || '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
albumId: item.albumId?.toString() || '',
|
albumId: item.albumId?.toString() || '',
|
||||||
artistName: item.artist || '',
|
artistName: item.artist || '',
|
||||||
artists: [
|
...getArtists(item),
|
||||||
{
|
|
||||||
id: item.artistId?.toString() || '',
|
|
||||||
imageUrl: null,
|
|
||||||
name: item.artist || '',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
bitRate: item.bitRate || 0,
|
bitRate: item.bitRate || 0,
|
||||||
bpm: item.bpm || null,
|
bpm: item.bpm || null,
|
||||||
channels: null,
|
channels: null,
|
||||||
|
|
@ -84,16 +158,7 @@ const normalizeSong = (
|
||||||
track: item.replayGain.trackGain,
|
track: item.replayGain.trackGain,
|
||||||
}
|
}
|
||||||
: null,
|
: null,
|
||||||
genres: item.genre
|
genres: getGenres(item),
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: item.genre,
|
|
||||||
imageUrl: null,
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
name: item.genre,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
id: item.id.toString(),
|
id: item.id.toString(),
|
||||||
imagePlaceholderUrl: null,
|
imagePlaceholderUrl: null,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
|
@ -176,26 +241,12 @@ const normalizeAlbum = (
|
||||||
|
|
||||||
return {
|
return {
|
||||||
albumArtist: item.artist,
|
albumArtist: item.artist,
|
||||||
albumArtists: item.artistId
|
...getArtists(item),
|
||||||
? [{ id: item.artistId.toString(), imageUrl: null, name: item.artist }]
|
|
||||||
: [],
|
|
||||||
artists: item.artistId
|
|
||||||
? [{ id: item.artistId.toString(), imageUrl: null, name: item.artist }]
|
|
||||||
: [],
|
|
||||||
backdropImageUrl: null,
|
backdropImageUrl: null,
|
||||||
comment: null,
|
comment: null,
|
||||||
createdAt: item.created,
|
createdAt: item.created,
|
||||||
duration: item.duration * 1000,
|
duration: item.duration * 1000,
|
||||||
genres: item.genre
|
genres: getGenres(item),
|
||||||
? [
|
|
||||||
{
|
|
||||||
id: item.genre,
|
|
||||||
imageUrl: null,
|
|
||||||
itemType: LibraryItem.GENRE,
|
|
||||||
name: item.genre,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
: [],
|
|
||||||
id: item.id.toString(),
|
id: item.id.toString(),
|
||||||
imagePlaceholderUrl: null,
|
imagePlaceholderUrl: null,
|
||||||
imageUrl,
|
imageUrl,
|
||||||
|
|
|
||||||
|
|
@ -66,15 +66,29 @@ const genreItem = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const simpleArtist = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const contributor = z.object({
|
||||||
|
artist: simpleArtist,
|
||||||
|
role: z.string(),
|
||||||
|
subRole: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
const song = z.object({
|
const song = z.object({
|
||||||
album: z.string().optional(),
|
album: z.string().optional(),
|
||||||
|
albumArtists: z.array(simpleArtist),
|
||||||
albumId: id.optional(),
|
albumId: id.optional(),
|
||||||
artist: z.string().optional(),
|
artist: z.string().optional(),
|
||||||
artistId: id.optional(),
|
artistId: id.optional(),
|
||||||
|
artists: z.array(simpleArtist),
|
||||||
averageRating: z.number().optional(),
|
averageRating: z.number().optional(),
|
||||||
bitRate: z.number().optional(),
|
bitRate: z.number().optional(),
|
||||||
bpm: z.number().optional(),
|
bpm: z.number().optional(),
|
||||||
contentType: z.string(),
|
contentType: z.string(),
|
||||||
|
contributors: z.array(contributor).optional(),
|
||||||
coverArt: z.string().optional(),
|
coverArt: z.string().optional(),
|
||||||
created: z.string(),
|
created: z.string(),
|
||||||
discNumber: z.number(),
|
discNumber: z.number(),
|
||||||
|
|
@ -101,12 +115,16 @@ const song = z.object({
|
||||||
|
|
||||||
const album = z.object({
|
const album = z.object({
|
||||||
album: z.string(),
|
album: z.string(),
|
||||||
|
albumArtists: z.array(simpleArtist),
|
||||||
artist: z.string(),
|
artist: z.string(),
|
||||||
artistId: id,
|
artistId: id,
|
||||||
|
artists: z.array(simpleArtist),
|
||||||
|
contributors: z.array(contributor).optional(),
|
||||||
coverArt: z.string(),
|
coverArt: z.string(),
|
||||||
created: z.string(),
|
created: z.string(),
|
||||||
duration: z.number(),
|
duration: z.number(),
|
||||||
genre: z.string().optional(),
|
genre: z.string().optional(),
|
||||||
|
genres: z.array(genreItem).optional(),
|
||||||
id,
|
id,
|
||||||
isCompilation: z.boolean().optional(),
|
isCompilation: z.boolean().optional(),
|
||||||
isDir: z.boolean(),
|
isDir: z.boolean(),
|
||||||
|
|
|
||||||
|
|
@ -168,6 +168,7 @@ export type Album = {
|
||||||
mbzId: string | null;
|
mbzId: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
originalDate: string | null;
|
originalDate: string | null;
|
||||||
|
participants: Record<string, RelatedArtist[]> | null;
|
||||||
playCount: number | null;
|
playCount: number | null;
|
||||||
releaseDate: string | null;
|
releaseDate: string | null;
|
||||||
releaseYear: number | null;
|
releaseYear: number | null;
|
||||||
|
|
@ -212,6 +213,7 @@ export type Song = {
|
||||||
lastPlayedAt: string | null;
|
lastPlayedAt: string | null;
|
||||||
lyrics: string | null;
|
lyrics: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
|
participants: Record<string, RelatedArtist[]> | null;
|
||||||
path: string | null;
|
path: string | null;
|
||||||
peak: GainInfo | null;
|
peak: GainInfo | null;
|
||||||
playCount: number;
|
playCount: number;
|
||||||
|
|
|
||||||
|
|
@ -88,10 +88,7 @@ export const AlbumArtistListGridView = ({ itemCount, gridRef }: AlbumArtistListG
|
||||||
server,
|
server,
|
||||||
signal,
|
signal,
|
||||||
},
|
},
|
||||||
query: {
|
query,
|
||||||
limit,
|
|
||||||
...filter,
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
{ cacheTime: 1000 * 60 * 1 },
|
{ cacheTime: 1000 * 60 * 1 },
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,14 @@ import { Group, Table } from '@mantine/core';
|
||||||
import { RiCheckFill, RiCloseFill } from 'react-icons/ri';
|
import { RiCheckFill, RiCloseFill } from 'react-icons/ri';
|
||||||
import { TFunction, useTranslation } from 'react-i18next';
|
import { TFunction, useTranslation } from 'react-i18next';
|
||||||
import { ReactNode } from 'react';
|
import { ReactNode } from 'react';
|
||||||
import { Album, AlbumArtist, AnyLibraryItem, LibraryItem, Song } from '/@/renderer/api/types';
|
import {
|
||||||
|
Album,
|
||||||
|
AlbumArtist,
|
||||||
|
AnyLibraryItem,
|
||||||
|
LibraryItem,
|
||||||
|
RelatedArtist,
|
||||||
|
Song,
|
||||||
|
} from '/@/renderer/api/types';
|
||||||
import { formatDurationString, formatSizeString } from '/@/renderer/utils';
|
import { formatDurationString, formatSizeString } from '/@/renderer/utils';
|
||||||
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
import { replaceURLWithHTMLLinks } from '/@/renderer/utils/linkify';
|
||||||
import { Spoiler, Text } from '/@/renderer/components';
|
import { Spoiler, Text } from '/@/renderer/components';
|
||||||
|
|
@ -46,8 +53,8 @@ const handleRow = <T extends AnyLibraryItem>(t: TFunction, item: T, rule: ItemDe
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const formatArtists = (isAlbumArtist: boolean) => (item: Album | Song) =>
|
const formatArtists = (artists: RelatedArtist[] | undefined | null) =>
|
||||||
(isAlbumArtist ? item.albumArtists : item.artists)?.map((artist, index) => (
|
artists?.map((artist, index) => (
|
||||||
<span key={artist.id || artist.name}>
|
<span key={artist.id || artist.name}>
|
||||||
{index > 0 && <Separator />}
|
{index > 0 && <Separator />}
|
||||||
{artist.id ? (
|
{artist.id ? (
|
||||||
|
|
@ -106,7 +113,7 @@ const BoolField = (key: boolean) =>
|
||||||
|
|
||||||
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
|
const AlbumPropertyMapping: ItemDetailRow<Album>[] = [
|
||||||
{ key: 'name', label: 'common.title' },
|
{ key: 'name', label: 'common.title' },
|
||||||
{ label: 'entity.albumArtist_one', render: formatArtists(true) },
|
{ label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) },
|
||||||
{ label: 'entity.genre_other', render: FormatGenre },
|
{ label: 'entity.genre_other', render: FormatGenre },
|
||||||
{
|
{
|
||||||
label: 'common.duration',
|
label: 'common.duration',
|
||||||
|
|
@ -198,8 +205,8 @@ const AlbumArtistPropertyMapping: ItemDetailRow<AlbumArtist>[] = [
|
||||||
const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
||||||
{ key: 'name', label: 'common.title' },
|
{ key: 'name', label: 'common.title' },
|
||||||
{ key: 'path', label: 'common.path', render: SongPath },
|
{ key: 'path', label: 'common.path', render: SongPath },
|
||||||
{ label: 'entity.albumArtist_one', render: formatArtists(true) },
|
{ label: 'entity.albumArtist_one', render: (item) => formatArtists(item.albumArtists) },
|
||||||
{ key: 'artists', label: 'entity.artist_other', render: formatArtists(false) },
|
{ key: 'artists', label: 'entity.artist_other', render: (item) => formatArtists(item.artists) },
|
||||||
{
|
{
|
||||||
key: 'album',
|
key: 'album',
|
||||||
label: 'entity.album_one',
|
label: 'entity.album_one',
|
||||||
|
|
@ -270,22 +277,42 @@ const SongPropertyMapping: ItemDetailRow<Song>[] = [
|
||||||
{ label: 'filter.comment', render: formatComment },
|
{ label: 'filter.comment', render: formatComment },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const handleParticipants = (item: Album | Song) => {
|
||||||
|
if (item.participants) {
|
||||||
|
return Object.entries(item.participants).map(([role, participants]) => {
|
||||||
|
return (
|
||||||
|
<tr key={role}>
|
||||||
|
<td>
|
||||||
|
{role.slice(0, 1).toLocaleUpperCase()}
|
||||||
|
{role.slice(1)}
|
||||||
|
</td>
|
||||||
|
<td>{formatArtists(participants)}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
|
export const ItemDetailsModal = ({ item }: ItemDetailsModalProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
let body: ReactNode;
|
let body: ReactNode[] = [];
|
||||||
|
|
||||||
switch (item.itemType) {
|
switch (item.itemType) {
|
||||||
case LibraryItem.ALBUM:
|
case LibraryItem.ALBUM:
|
||||||
body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule));
|
body = AlbumPropertyMapping.map((rule) => handleRow(t, item, rule));
|
||||||
|
body.push(...handleParticipants(item));
|
||||||
break;
|
break;
|
||||||
case LibraryItem.ALBUM_ARTIST:
|
case LibraryItem.ALBUM_ARTIST:
|
||||||
body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule));
|
body = AlbumArtistPropertyMapping.map((rule) => handleRow(t, item, rule));
|
||||||
break;
|
break;
|
||||||
case LibraryItem.SONG:
|
case LibraryItem.SONG:
|
||||||
body = SongPropertyMapping.map((rule) => handleRow(t, item, rule));
|
body = SongPropertyMapping.map((rule) => handleRow(t, item, rule));
|
||||||
|
body.push(...handleParticipants(item));
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
body = null;
|
body = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue