From 6aeec1e89c399887b928179f38296a3058d5ec86 Mon Sep 17 00:00:00 2001 From: jeffvli Date: Thu, 1 Jan 2026 21:46:58 -0800 Subject: [PATCH] add filepath replacement setting (#1402) --- src/i18n/locales/en.json | 5 + src/renderer/api/controller.ts | 257 +++++++++++------- .../api/jellyfin/jellyfin-controller.ts | 86 +++++- .../api/navidrome/navidrome-controller.ts | 56 +++- .../api/subsonic/subsonic-controller.ts | 215 ++++++++++++--- .../general/application-settings.tsx | 2 + .../components/general/path-settings.tsx | 83 ++++++ src/renderer/store/settings.store.ts | 4 + src/shared/api/jellyfin/jellyfin-normalize.ts | 8 +- .../api/navidrome/navidrome-normalize.ts | 11 +- src/shared/api/subsonic/subsonic-normalize.ts | 16 +- src/shared/api/utils.ts | 8 + src/shared/types/domain-types.ts | 8 + 13 files changed, 594 insertions(+), 165 deletions(-) create mode 100644 src/renderer/features/settings/components/general/path-settings.tsx diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 8e01270d5..eb70bda99 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -83,6 +83,7 @@ "edit": "edit", "enable": "enable", "expand": "expand", + "example": "example", "externalLinks": "external links", "faster": "faster", "favorite": "favorite", @@ -872,6 +873,10 @@ "neteaseTranslation": "Enable NetEase translations", "notify": "enable song notifications", "notify_description": "show notifications when changing the current song", + "pathReplace": "file path replacement", + "pathReplace_description": "replace your server's default filepath", + "pathReplace_optionRemovePrefix": "remove prefix", + "pathReplace_optionAddPrefix": "add prefix", "passwordStore_description": "what password/secret store to use. change this if you are having issues storing passwords", "passwordStore": "passwords/secret store", "playerFilters": "Filter songs from the queue", diff --git a/src/renderer/api/controller.ts b/src/renderer/api/controller.ts index 4fea4cfca..dffa34e78 100644 --- a/src/renderer/api/controller.ts +++ b/src/renderer/api/controller.ts @@ -3,7 +3,7 @@ import { JellyfinController } from '/@/renderer/api/jellyfin/jellyfin-controller import { NavidromeController } from '/@/renderer/api/navidrome/navidrome-controller'; import { SubsonicController } from '/@/renderer/api/subsonic/subsonic-controller'; import { mergeMusicFolderId } from '/@/renderer/api/utils-music-folder'; -import { getServerById, useAuthStore } from '/@/renderer/store'; +import { getServerById, useAuthStore, useSettingsStore } from '/@/renderer/store'; import { toast } from '/@/shared/components/toast/toast'; import { AuthenticationResponse, @@ -60,6 +60,22 @@ const apiController = ( return controllerFn; }; +const getPathReplaceSettings = () => { + const { pathReplace, pathReplaceWith } = useSettingsStore.getState().general; + return { pathReplace, pathReplaceWith }; +}; + +const addContext = (args: T): T => { + const pathSettings = getPathReplaceSettings(); + return { + ...args, + context: { + ...(args.context || {}), + ...pathSettings, + }, + }; +}; + export interface GeneralController extends Omit, 'authenticate'> { authenticate: ( url: string, @@ -81,7 +97,7 @@ export const controller: GeneralController = { return apiController( 'addToPlaylist', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, authenticate(url, body, type) { return apiController('authenticate', type)(url, body); @@ -98,7 +114,7 @@ export const controller: GeneralController = { return apiController( 'createFavorite', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, createInternetRadioStation(args) { const server = getServerById(args.apiClientProps.serverId); @@ -112,7 +128,7 @@ export const controller: GeneralController = { return apiController( 'createInternetRadioStation', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, createPlaylist(args) { const server = getServerById(args.apiClientProps.serverId); @@ -126,7 +142,7 @@ export const controller: GeneralController = { return apiController( 'createPlaylist', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, deleteFavorite(args) { const server = getServerById(args.apiClientProps.serverId); @@ -140,7 +156,7 @@ export const controller: GeneralController = { return apiController( 'deleteFavorite', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, deleteInternetRadioStation(args) { const server = getServerById(args.apiClientProps.serverId); @@ -154,7 +170,7 @@ export const controller: GeneralController = { return apiController( 'deleteInternetRadioStation', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, deletePlaylist(args) { const server = getServerById(args.apiClientProps.serverId); @@ -168,7 +184,7 @@ export const controller: GeneralController = { return apiController( 'deletePlaylist', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getAlbumArtistDetail(args) { const server = getServerById(args.apiClientProps.serverId); @@ -182,7 +198,7 @@ export const controller: GeneralController = { return apiController( 'getAlbumArtistDetail', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getAlbumArtistList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -196,11 +212,13 @@ export const controller: GeneralController = { return apiController( 'getAlbumArtistList', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getAlbumArtistListCount(args) { const server = getServerById(args.apiClientProps.serverId); @@ -214,11 +232,13 @@ export const controller: GeneralController = { return apiController( 'getAlbumArtistListCount', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getAlbumDetail(args) { const server = getServerById(args.apiClientProps.serverId); @@ -232,7 +252,7 @@ export const controller: GeneralController = { return apiController( 'getAlbumDetail', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getAlbumInfo(args) { const server = getServerById(args.apiClientProps.serverId); @@ -246,7 +266,7 @@ export const controller: GeneralController = { return apiController( 'getAlbumInfo', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getAlbumList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -260,11 +280,13 @@ export const controller: GeneralController = { return apiController( 'getAlbumList', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getAlbumListCount(args) { const server = getServerById(args.apiClientProps.serverId); @@ -278,11 +300,13 @@ export const controller: GeneralController = { return apiController( 'getAlbumListCount', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getArtistList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -296,11 +320,13 @@ export const controller: GeneralController = { return apiController( 'getArtistList', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getArtistListCount(args) { const server = getServerById(args.apiClientProps.serverId); @@ -314,11 +340,13 @@ export const controller: GeneralController = { return apiController( 'getArtistListCount', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getArtistRadio(args) { const server = getServerById(args.apiClientProps.serverId); @@ -332,7 +360,7 @@ export const controller: GeneralController = { return apiController( 'getArtistRadio', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getDownloadUrl(args) { const server = getServerById(args.apiClientProps.serverId); @@ -346,7 +374,7 @@ export const controller: GeneralController = { return apiController( 'getDownloadUrl', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getFolder(args) { const server = getServerById(args.apiClientProps.serverId); @@ -360,11 +388,13 @@ export const controller: GeneralController = { return apiController( 'getFolder', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getGenreList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -378,11 +408,13 @@ export const controller: GeneralController = { return apiController( 'getGenreList', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getImageUrl(args) { const server = getServerById(args.apiClientProps.serverId); @@ -395,7 +427,12 @@ export const controller: GeneralController = { apiController( 'getImageUrl', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }) || null + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + }), + ) || null ); }, getInternetRadioStations(args) { @@ -409,7 +446,7 @@ export const controller: GeneralController = { return apiController( 'getInternetRadioStations', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getLyrics(args) { const server = getServerById(args.apiClientProps.serverId); @@ -423,7 +460,7 @@ export const controller: GeneralController = { return apiController( 'getLyrics', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getMusicFolderList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -437,7 +474,7 @@ export const controller: GeneralController = { return apiController( 'getMusicFolderList', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getPlaylistDetail(args) { const server = getServerById(args.apiClientProps.serverId); @@ -451,7 +488,7 @@ export const controller: GeneralController = { return apiController( 'getPlaylistDetail', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getPlaylistList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -465,7 +502,7 @@ export const controller: GeneralController = { return apiController( 'getPlaylistList', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getPlaylistListCount(args) { const server = getServerById(args.apiClientProps.serverId); @@ -479,7 +516,7 @@ export const controller: GeneralController = { return apiController( 'getPlaylistListCount', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getPlaylistSongList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -493,7 +530,7 @@ export const controller: GeneralController = { return apiController( 'getPlaylistSongList', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getPlayQueue(args) { const server = getServerById(args.apiClientProps.serverId); @@ -507,7 +544,7 @@ export const controller: GeneralController = { return apiController( 'getPlayQueue', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getRandomSongList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -521,11 +558,13 @@ export const controller: GeneralController = { return apiController( 'getRandomSongList', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getRoles(args) { const server = getServerById(args.apiClientProps.serverId); @@ -539,7 +578,7 @@ export const controller: GeneralController = { return apiController( 'getRoles', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getServerInfo(args) { const server = getServerById(args.apiClientProps.serverId); @@ -553,7 +592,7 @@ export const controller: GeneralController = { return apiController( 'getServerInfo', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getSimilarSongs(args) { const server = getServerById(args.apiClientProps.serverId); @@ -567,11 +606,13 @@ export const controller: GeneralController = { return apiController( 'getSimilarSongs', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getSongDetail(args) { const server = getServerById(args.apiClientProps.serverId); @@ -585,7 +626,7 @@ export const controller: GeneralController = { return apiController( 'getSongDetail', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getSongList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -599,11 +640,13 @@ export const controller: GeneralController = { return apiController( 'getSongList', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getSongListCount(args) { const server = getServerById(args.apiClientProps.serverId); @@ -617,11 +660,13 @@ export const controller: GeneralController = { return apiController( 'getSongListCount', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, getStreamUrl(args) { const server = getServerById(args.apiClientProps.serverId); @@ -633,7 +678,7 @@ export const controller: GeneralController = { return apiController( 'getStreamUrl', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getStructuredLyrics(args) { const server = getServerById(args.apiClientProps.serverId); @@ -647,7 +692,7 @@ export const controller: GeneralController = { return apiController( 'getStructuredLyrics', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getTagList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -661,7 +706,7 @@ export const controller: GeneralController = { return apiController( 'getTagList', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getTopSongs(args) { const server = getServerById(args.apiClientProps.serverId); @@ -675,7 +720,7 @@ export const controller: GeneralController = { return apiController( 'getTopSongs', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getUserInfo(args) { const server = getServerById(args.apiClientProps.serverId); @@ -689,7 +734,7 @@ export const controller: GeneralController = { return apiController( 'getUserInfo', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, getUserList(args) { const server = getServerById(args.apiClientProps.serverId); @@ -703,7 +748,7 @@ export const controller: GeneralController = { return apiController( 'getUserList', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, movePlaylistItem(args) { const server = getServerById(args.apiClientProps.serverId); @@ -717,7 +762,7 @@ export const controller: GeneralController = { return apiController( 'movePlaylistItem', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, removeFromPlaylist(args) { const server = getServerById(args.apiClientProps.serverId); @@ -731,7 +776,7 @@ export const controller: GeneralController = { return apiController( 'removeFromPlaylist', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, replacePlaylist(args) { const server = getServerById(args.apiClientProps.serverId); @@ -745,7 +790,7 @@ export const controller: GeneralController = { return apiController( 'replacePlaylist', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, savePlayQueue(args) { const server = getServerById(args.apiClientProps.serverId); @@ -759,7 +804,7 @@ export const controller: GeneralController = { return apiController( 'savePlayQueue', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, scrobble(args) { const server = getServerById(args.apiClientProps.serverId); @@ -773,7 +818,7 @@ export const controller: GeneralController = { return apiController( 'scrobble', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, search(args) { const server = getServerById(args.apiClientProps.serverId); @@ -787,11 +832,13 @@ export const controller: GeneralController = { return apiController( 'search', server.type, - )?.({ - ...args, - apiClientProps: { ...args.apiClientProps, server }, - query: mergeMusicFolderId(args.query, server), - }); + )?.( + addContext({ + ...args, + apiClientProps: { ...args.apiClientProps, server }, + query: mergeMusicFolderId(args.query, server), + }), + ); }, setRating(args) { const server = getServerById(args.apiClientProps.serverId); @@ -805,7 +852,7 @@ export const controller: GeneralController = { return apiController( 'setRating', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, shareItem(args) { const server = getServerById(args.apiClientProps.serverId); @@ -819,7 +866,7 @@ export const controller: GeneralController = { return apiController( 'shareItem', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, updateInternetRadioStation(args) { const server = getServerById(args.apiClientProps.serverId); @@ -833,7 +880,7 @@ export const controller: GeneralController = { return apiController( 'updateInternetRadioStation', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, updatePlaylist(args) { const server = getServerById(args.apiClientProps.serverId); @@ -847,6 +894,6 @@ export const controller: GeneralController = { return apiController( 'updatePlaylist', server.type, - )?.({ ...args, apiClientProps: { ...args.apiClientProps, server } }); + )?.(addContext({ ...args, apiClientProps: { ...args.apiClientProps, server } })); }, }; diff --git a/src/renderer/api/jellyfin/jellyfin-controller.ts b/src/renderer/api/jellyfin/jellyfin-controller.ts index 243bee5a1..98e7c68c3 100644 --- a/src/renderer/api/jellyfin/jellyfin-controller.ts +++ b/src/renderer/api/jellyfin/jellyfin-controller.ts @@ -447,7 +447,14 @@ export const JellyfinController: InternalControllerEndpoint = { throw new Error('Failed to get artist radio songs'); } - return res.body.Items.map((song) => jfNormalize.song(song, apiClientProps.server)); + return res.body.Items.map((song) => + jfNormalize.song( + song, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ); }, getDownloadUrl: (args) => { const { apiClientProps, query } = args; @@ -858,7 +865,14 @@ export const JellyfinController: InternalControllerEndpoint = { } return { - items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)), + items: res.body.Items.map((item) => + jfNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), startIndex: 0, totalRecordCount: res.body.TotalRecordCount, }; @@ -911,7 +925,14 @@ export const JellyfinController: InternalControllerEndpoint = { } return { - items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)), + items: res.body.Items.map((item) => + jfNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), startIndex: 0, totalRecordCount: res.body.Items.length || 0, }; @@ -961,7 +982,14 @@ export const JellyfinController: InternalControllerEndpoint = { if (res.status === 200 && res.body.Items.length) { const results = res.body.Items.reduce((acc, song) => { if (song.Id !== query.songId) { - acc.push(jfNormalize.song(song, apiClientProps.server)); + acc.push( + jfNormalize.song( + song, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ); } return acc; @@ -990,7 +1018,14 @@ export const JellyfinController: InternalControllerEndpoint = { return mix.body.Items.reduce((acc, song) => { if (song.Id !== query.songId) { - acc.push(jfNormalize.song(song, apiClientProps.server)); + acc.push( + jfNormalize.song( + song, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ); } return acc; @@ -1010,7 +1045,12 @@ export const JellyfinController: InternalControllerEndpoint = { throw new Error('Failed to get song detail'); } - return jfNormalize.song(res.body, apiClientProps.server); + return jfNormalize.song( + res.body, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ); }, getSongList: async (args) => { const { apiClientProps, query } = args; @@ -1122,7 +1162,14 @@ export const JellyfinController: InternalControllerEndpoint = { } return { - items: items.map((item) => jfNormalize.song(item, apiClientProps.server)), + items: items.map((item) => + jfNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), startIndex: query.startIndex, totalRecordCount, }; @@ -1221,7 +1268,14 @@ export const JellyfinController: InternalControllerEndpoint = { } return { - items: res.body.Items.map((item) => jfNormalize.song(item, apiClientProps.server)), + items: res.body.Items.map((item) => + jfNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), startIndex: 0, totalRecordCount: res.body.TotalRecordCount, }; @@ -1306,7 +1360,12 @@ export const JellyfinController: InternalControllerEndpoint = { } const existingSongs = existingSongsRes.body.Items.map((item) => - jfNormalize.song(item, apiClientProps.server), + jfNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), ); // 2. Get playlist detail to get the name @@ -1546,7 +1605,14 @@ export const JellyfinController: InternalControllerEndpoint = { jfNormalize.albumArtist(item, apiClientProps.server), ), albums: albums.map((item) => jfNormalize.album(item, apiClientProps.server)), - songs: songs.map((item) => jfNormalize.song(item, apiClientProps.server)), + songs: songs.map((item) => + jfNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), }; }, updateInternetRadioStation: async (args) => { diff --git a/src/renderer/api/navidrome/navidrome-controller.ts b/src/renderer/api/navidrome/navidrome-controller.ts index 80b82e73b..0fb63c70c 100644 --- a/src/renderer/api/navidrome/navidrome-controller.ts +++ b/src/renderer/api/navidrome/navidrome-controller.ts @@ -268,7 +268,7 @@ export const NavidromeController: InternalControllerEndpoint = { query: { ...query, limit: 1, startIndex: 0 }, }).then((result) => result!.totalRecordCount!), getAlbumDetail: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const albumRes = await ndApiClient(apiClientProps).getAlbumDetail({ params: { @@ -294,6 +294,8 @@ export const NavidromeController: InternalControllerEndpoint = { return ndNormalize.album( { ...albumRes.body.data, songs: songsData.body.data }, apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, ); }, getAlbumInfo: async (args) => { @@ -317,7 +319,7 @@ export const NavidromeController: InternalControllerEndpoint = { }; }, getAlbumList: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const genres = hasFeature(apiClientProps.server, ServerFeature.BFR) ? query.genreIds @@ -352,7 +354,14 @@ export const NavidromeController: InternalControllerEndpoint = { } return { - items: res.body.data.map((album) => ndNormalize.album(album, apiClientProps.server)), + items: res.body.data.map((album) => + ndNormalize.album( + album, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ), startIndex: query?.startIndex || 0, totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), }; @@ -560,7 +569,14 @@ export const NavidromeController: InternalControllerEndpoint = { } return { - items: res.body.data.map((item) => ndNormalize.song(item, apiClientProps.server)), + items: res.body.data.map((item) => + ndNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), startIndex: 0, totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), }; @@ -577,7 +593,14 @@ export const NavidromeController: InternalControllerEndpoint = { const { changedBy, current, items, position, updatedAt } = res.body.data; - const entries = items.map((song) => ndNormalize.song(song, apiClientProps.server)); + const entries = items.map((song) => + ndNormalize.song( + song, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ); return { changed: updatedAt, @@ -668,7 +691,12 @@ export const NavidromeController: InternalControllerEndpoint = { throw new Error('Failed to get song detail'); } - return ndNormalize.song(res.body.data, apiClientProps.server); + return ndNormalize.song( + res.body.data, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ); }, getSongList: async (args) => { const { apiClientProps, query } = args; @@ -696,7 +724,14 @@ export const NavidromeController: InternalControllerEndpoint = { } return { - items: res.body.data.map((song) => ndNormalize.song(song, apiClientProps.server)), + items: res.body.data.map((song) => + ndNormalize.song( + song, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), startIndex: query?.startIndex || 0, totalRecordCount: Number(res.body.headers.get('x-total-count') || 0), }; @@ -847,7 +882,12 @@ export const NavidromeController: InternalControllerEndpoint = { } const existingSongs = existingSongsRes.body.data.map((item) => - ndNormalize.song(item, apiClientProps.server), + ndNormalize.song( + item, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), ); // 2. Get playlist detail to get the name diff --git a/src/renderer/api/subsonic/subsonic-controller.ts b/src/renderer/api/subsonic/subsonic-controller.ts index 54991697f..e06b38202 100644 --- a/src/renderer/api/subsonic/subsonic-controller.ts +++ b/src/renderer/api/subsonic/subsonic-controller.ts @@ -282,7 +282,14 @@ export const SubsonicController: InternalControllerEndpoint = { return { ...ssNormalize.albumArtist(artist, apiClientProps.server), - albums: artist.album?.map((album) => ssNormalize.album(album, apiClientProps.server)), + albums: artist.album?.map((album) => + ssNormalize.album( + album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ), similarArtists: artistInfo?.similarArtist?.map((artist) => ssNormalize.albumArtist(artist, apiClientProps.server), @@ -327,6 +334,7 @@ export const SubsonicController: InternalControllerEndpoint = { getAlbumArtistListCount: (args) => SubsonicController.getAlbumArtistList({ ...args, + context: args.context, query: { ...args.query, startIndex: 0 }, }).then((res) => res!.totalRecordCount!), getAlbumDetail: async (args) => { @@ -342,7 +350,12 @@ export const SubsonicController: InternalControllerEndpoint = { throw new Error('Failed to get album detail'); } - return ssNormalize.album(res.body.album, apiClientProps.server); + return ssNormalize.album( + res.body.album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ); }, getAlbumList: async (args) => { const { apiClientProps, query } = args; @@ -367,7 +380,12 @@ export const SubsonicController: InternalControllerEndpoint = { const results = res.body.searchResult3?.album?.map((album) => - ssNormalize.album(album, apiClientProps.server), + ssNormalize.album( + album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), ) || []; return { @@ -402,7 +420,14 @@ export const SubsonicController: InternalControllerEndpoint = { return artist.body.artist.album ?? []; }); - const items = albums.map((album) => ssNormalize.album(album, apiClientProps.server)); + const items = albums.map((album) => + ssNormalize.album( + album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), + ); return { items: sortAlbumList(items, query.sortBy, query.sortOrder), @@ -424,7 +449,12 @@ export const SubsonicController: InternalControllerEndpoint = { const allResults = res.body.starred?.album?.map((album) => - ssNormalize.album(album, apiClientProps.server), + ssNormalize.album( + album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), ) || []; return sortAndPaginate(allResults, { @@ -489,7 +519,12 @@ export const SubsonicController: InternalControllerEndpoint = { return { items: res.body.albumList2.album?.map((album) => - ssNormalize.album(album, apiClientProps.server), + ssNormalize.album( + album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), ) || [], startIndex: query.startIndex, totalRecordCount: null, @@ -682,10 +717,11 @@ export const SubsonicController: InternalControllerEndpoint = { getArtistListCount: async (args) => SubsonicController.getArtistList({ ...args, + context: args.context, query: { ...args.query, startIndex: 0 }, }).then((res) => res!.totalRecordCount!), getArtistRadio: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const res = await ssApiClient(apiClientProps).getSimilarSongs2({ query: { @@ -703,7 +739,12 @@ export const SubsonicController: InternalControllerEndpoint = { } return res.body.similarSongs2.song.map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ); }, getDownloadUrl: (args) => { @@ -717,7 +758,7 @@ export const SubsonicController: InternalControllerEndpoint = { '&c=Feishin' ); }, - getFolder: async ({ apiClientProps, query }) => { + getFolder: async ({ apiClientProps, context, query }) => { const sortOrder = (query.sortOrder?.toLowerCase() ?? 'asc') as 'asc' | 'desc'; const isRootFolderId = /^\d+$/.test(query.id); @@ -749,7 +790,14 @@ export const SubsonicController: InternalControllerEndpoint = { }); } - let folders = items.map((item) => ssNormalize.folder(item, apiClientProps.server)); + let folders = items.map((item) => + ssNormalize.folder( + item, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ); folders = orderBy(folders, [(v) => v.name.toLowerCase()], [sortOrder]); @@ -777,7 +825,12 @@ export const SubsonicController: InternalControllerEndpoint = { throw new Error('Failed to get folder'); } - const folder = ssNormalize.folder(directoryRes.body.directory, apiClientProps.server); + const folder = ssNormalize.folder( + directoryRes.body.directory, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ); let filteredFolders = folder.children?.folders || []; let filteredSongs = folder.children?.songs || []; @@ -991,7 +1044,7 @@ export const SubsonicController: InternalControllerEndpoint = { return results.length; }, - getPlaylistSongList: async ({ apiClientProps, query }) => { + getPlaylistSongList: async ({ apiClientProps, context, query }) => { const res = await ssApiClient(apiClientProps).getPlaylist({ query: { id: query.id, @@ -1003,8 +1056,14 @@ export const SubsonicController: InternalControllerEndpoint = { } const items = - res.body.playlist.entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || - []; + res.body.playlist.entry?.map((song) => + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ) || []; return { items, @@ -1012,7 +1071,7 @@ export const SubsonicController: InternalControllerEndpoint = { totalRecordCount: items.length, }; }, - getPlayQueue: async ({ apiClientProps }) => { + getPlayQueue: async ({ apiClientProps, context }) => { if (hasFeature(apiClientProps.server, ServerFeature.SERVER_PLAY_QUEUE)) { const res = await ssApiClient(apiClientProps).getPlayQueueByIndex(); @@ -1027,7 +1086,15 @@ export const SubsonicController: InternalControllerEndpoint = { changed, changedBy, currentIndex: currentIndex ?? 0, - entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [], + entry: + entry?.map((song) => + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ) || [], positionMs: position ?? 0, username, }; @@ -1044,14 +1111,22 @@ export const SubsonicController: InternalControllerEndpoint = { changed, changedBy, currentIndex: current ? entry.findIndex((item) => item.id === current) : 0, - entry: entry?.map((song) => ssNormalize.song(song, apiClientProps.server)) || [], + entry: + entry?.map((song) => + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ) || [], positionMs: position ?? 0, username, }; } }, getRandomSongList: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const res = await ssApiClient(apiClientProps).getRandomSongList({ query: { @@ -1069,7 +1144,12 @@ export const SubsonicController: InternalControllerEndpoint = { const results = res.body.randomSongs?.song || []; const normalizedResults = results.map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ); return { @@ -1147,7 +1227,7 @@ export const SubsonicController: InternalControllerEndpoint = { return { features, id: apiClientProps.server?.id, version: ping.body.serverVersion }; }, getSimilarSongs: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const res = await ssApiClient(apiClientProps).getSimilarSongs({ query: { @@ -1166,14 +1246,21 @@ export const SubsonicController: InternalControllerEndpoint = { return res.body.similarSongs.song.reduce((acc, song) => { if (song.id !== query.songId) { - acc.push(ssNormalize.song(song, apiClientProps.server)); + acc.push( + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ); } return acc; }, []); }, getSongDetail: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const res = await ssApiClient(apiClientProps).getSong({ query: { @@ -1185,9 +1272,14 @@ export const SubsonicController: InternalControllerEndpoint = { throw new Error('Failed to get song detail'); } - return ssNormalize.song(res.body.song, apiClientProps.server); + return ssNormalize.song( + res.body.song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ); }, - getSongList: async ({ apiClientProps, query }) => { + getSongList: async ({ apiClientProps, context, query }) => { const fromAlbumPromises: Promise>[] = []; const artistDetailPromises: Promise>[] = []; @@ -1212,7 +1304,12 @@ export const SubsonicController: InternalControllerEndpoint = { return { items: res.body.searchResult3?.song?.map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ) || [], startIndex: query.startIndex, totalRecordCount: null, @@ -1236,7 +1333,15 @@ export const SubsonicController: InternalControllerEndpoint = { const results = res.body.songsByGenre?.song || []; return { - items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [], + items: + results.map((song) => + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ) || [], startIndex: 0, totalRecordCount: null, }; @@ -1255,7 +1360,12 @@ export const SubsonicController: InternalControllerEndpoint = { const allResults = (res.body.starred?.song || []).map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ) || []; return sortAndPaginate(allResults, { @@ -1331,7 +1441,15 @@ export const SubsonicController: InternalControllerEndpoint = { } return { - items: results.map((song) => ssNormalize.song(song, apiClientProps.server)) || [], + items: + results.map((song) => + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), + ) || [], startIndex: 0, totalRecordCount: results.length, }; @@ -1357,7 +1475,12 @@ export const SubsonicController: InternalControllerEndpoint = { return { items: res.body.searchResult3?.song?.map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ) || [], startIndex: 0, totalRecordCount: null, @@ -1596,7 +1719,7 @@ export const SubsonicController: InternalControllerEndpoint = { }); }, getTopSongs: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const res = await ssApiClient(apiClientProps).getTopSongsList({ query: { @@ -1612,7 +1735,12 @@ export const SubsonicController: InternalControllerEndpoint = { return { items: res.body.topSongs?.song?.map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ) || [], startIndex: 0, totalRecordCount: res.body.topSongs?.song?.length || 0, @@ -1652,7 +1780,7 @@ export const SubsonicController: InternalControllerEndpoint = { return null; }, replacePlaylist: async (args) => { - const { apiClientProps, body, query } = args; + const { apiClientProps, body, context, query } = args; // 1. Fetch existing songs from the playlist const existingSongsRes = await ssApiClient(apiClientProps).getPlaylist({ @@ -1667,7 +1795,12 @@ export const SubsonicController: InternalControllerEndpoint = { const existingSongs = existingSongsRes.body.playlist.entry?.map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ) || []; // 2. Get playlist detail to get the name @@ -1779,7 +1912,7 @@ export const SubsonicController: InternalControllerEndpoint = { return null; }, search: async (args) => { - const { apiClientProps, query } = args; + const { apiClientProps, context, query } = args; const res = await ssApiClient(apiClientProps).search3({ query: { @@ -1803,10 +1936,20 @@ export const SubsonicController: InternalControllerEndpoint = { ssNormalize.albumArtist(artist, apiClientProps.server), ), albums: (res.body.searchResult3?.album || []).map((album) => - ssNormalize.album(album, apiClientProps.server), + ssNormalize.album( + album, + apiClientProps.server, + args.context?.pathReplace, + args.context?.pathReplaceWith, + ), ), songs: (res.body.searchResult3?.song || []).map((song) => - ssNormalize.song(song, apiClientProps.server), + ssNormalize.song( + song, + apiClientProps.server, + context?.pathReplace, + context?.pathReplaceWith, + ), ), }; }, diff --git a/src/renderer/features/settings/components/general/application-settings.tsx b/src/renderer/features/settings/components/general/application-settings.tsx index 9b07b8be3..d137d733c 100644 --- a/src/renderer/features/settings/components/general/application-settings.tsx +++ b/src/renderer/features/settings/components/general/application-settings.tsx @@ -12,6 +12,7 @@ import { ArtistSettings, } from '/@/renderer/features/settings/components/general/artist-settings'; import { HomeSettings } from '/@/renderer/features/settings/components/general/home-settings'; +import { PathSettings } from '/@/renderer/features/settings/components/general/path-settings'; import { SettingOption, SettingsSection, @@ -610,6 +611,7 @@ export const ApplicationSettings = () => { + } options={options} diff --git a/src/renderer/features/settings/components/general/path-settings.tsx b/src/renderer/features/settings/components/general/path-settings.tsx new file mode 100644 index 000000000..554965317 --- /dev/null +++ b/src/renderer/features/settings/components/general/path-settings.tsx @@ -0,0 +1,83 @@ +import { useQuery } from '@tanstack/react-query'; +import { useTranslation } from 'react-i18next'; + +import { songsQueries } from '/@/renderer/features/songs/api/songs-api'; +import { + useCurrentServerId, + useGeneralSettings, + useSettingsStore, + useSettingsStoreActions, +} from '/@/renderer/store'; +import { ActionIcon } from '/@/shared/components/action-icon/action-icon'; +import { Code } from '/@/shared/components/code/code'; +import { Group } from '/@/shared/components/group/group'; +import { Stack } from '/@/shared/components/stack/stack'; +import { TextInput } from '/@/shared/components/text-input/text-input'; +import { Text } from '/@/shared/components/text/text'; +import { Played } from '/@/shared/types/domain-types'; + +export const PathSettings = () => { + const { t } = useTranslation(); + const serverId = useCurrentServerId(); + const randomSong = useQuery({ + ...songsQueries.random({ + query: { limit: 1, played: Played.All }, + serverId, + }), + gcTime: Infinity, + staleTime: Infinity, + }); + + const { pathReplace, pathReplaceWith } = useGeneralSettings(); + const { setSettings } = useSettingsStoreActions(); + + return ( + + + {t('setting.pathReplace', { postProcess: 'sentenceCase' })} + randomSong.refetch()} + size="xs" + variant="transparent" + /> + + + + {randomSong.data?.items[0]?.path || ''} + + + + + setSettings({ + general: { + ...useSettingsStore.getState().general, + pathReplace: e.currentTarget.value, + }, + }) + } + placeholder={t('setting.pathReplace_optionRemovePrefix', { + postProcess: 'sentenceCase', + })} + value={pathReplace} + /> + + setSettings({ + general: { + ...useSettingsStore.getState().general, + pathReplaceWith: e.currentTarget.value, + }, + }) + } + placeholder={t('setting.pathReplace_optionAddPrefix', { + postProcess: 'sentenceCase', + })} + value={pathReplaceWith} + /> + + + ); +}; diff --git a/src/renderer/store/settings.store.ts b/src/renderer/store/settings.store.ts index b7de0572a..9871bf8f1 100644 --- a/src/renderer/store/settings.store.ts +++ b/src/renderer/store/settings.store.ts @@ -400,6 +400,8 @@ export const GeneralSettingsSchema = z.object({ musicBrainz: z.boolean(), nativeAspectRatio: z.boolean(), passwordStore: z.string().optional(), + pathReplace: z.string(), + pathReplaceWith: z.string(), playButtonBehavior: z.nativeEnum(Play), playerbarOpenDrawer: z.boolean(), playerbarSlider: PlayerbarSliderSchema, @@ -953,6 +955,8 @@ const initialState: SettingsState = { musicBrainz: true, nativeAspectRatio: false, passwordStore: undefined, + pathReplace: '', + pathReplaceWith: '', playButtonBehavior: Play.NOW, playerbarOpenDrawer: false, playerbarSlider: { diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index fbb56a61f..de64cc7f4 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { jfType } from '/@/shared/api/jellyfin/jellyfin-types'; +import { replacePathPrefix } from '/@/shared/api/utils'; import { Album, AlbumArtist, @@ -108,6 +109,8 @@ const getPlaylistImageId = (item: z.infer): nu const normalizeSong = ( item: z.infer, server: null | ServerListItem, + pathReplace?: string, + pathReplaceWith?: string, ): Song => { let bitRate = 0; let channels: null | number = null; @@ -208,7 +211,10 @@ const normalizeSong = ( mbzTrackId: item.ProviderIds?.MusicBrainzTrack || null, name: item.Name, participants: getPeople(item), - path, + path: + path && (pathReplace || pathReplaceWith) + ? replacePathPrefix(path, pathReplace || '', pathReplaceWith || '') + : path, peak: null, playCount: (item.UserData && item.UserData.PlayCount) || 0, playlistItemId: item.PlaylistItemId, diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index 84f522fb6..5bdb4a6e8 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -2,6 +2,7 @@ import z from 'zod'; import { ndType } from '/@/shared/api/navidrome/navidrome-types'; import { ssType } from '/@/shared/api/subsonic/subsonic-types'; +import { replacePathPrefix } from '/@/shared/api/utils'; import { Album, AlbumArtist, @@ -139,6 +140,8 @@ const getArtists = ( const normalizeSong = ( item: z.infer | z.infer, server?: null | ServerListItem, + pathReplace?: string, + pathReplaceWith?: string, ): Song => { let id; let playlistItemId; @@ -202,7 +205,7 @@ const normalizeSong = ( name: item.title, // Thankfully, Windows is merciful and allows a mix of separators. So, we can use the // POSIX separator here instead - path: (item.libraryPath ? item.libraryPath + '/' : '') + item.path, + path: item.path ? replacePathPrefix(item.path, pathReplace, pathReplaceWith) : null, peak: item.rgAlbumPeak || item.rgTrackPeak ? { album: item.rgAlbumPeak, track: item.rgTrackPeak } @@ -267,6 +270,8 @@ const normalizeAlbum = ( songs?: z.infer; }, server?: null | ServerListItem, + pathReplace?: string, + pathReplaceWith?: string, ): Album => { return { ...parseAlbumTags(item), @@ -309,7 +314,9 @@ const normalizeAlbum = ( releaseYear: item.maxYear || null, size: item.size, songCount: item.songCount, - songs: item.songs ? item.songs.map((song) => normalizeSong(song, server)) : undefined, + songs: item.songs + ? item.songs.map((song) => normalizeSong(song, server, pathReplace, pathReplaceWith)) + : undefined, tags: item.tags || null, updatedAt: item.updatedAt, userFavorite: item.starred || false, diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index 15c07d883..e54683f63 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -1,6 +1,7 @@ import { z } from 'zod'; import { ssType } from '/@/shared/api/subsonic/subsonic-types'; +import { replacePathPrefix } from '/@/shared/api/utils'; import { Album, AlbumArtist, @@ -117,6 +118,8 @@ const getGenres = ( const normalizeSong = ( item: z.infer, server?: null | ServerListItemWithCredential, + pathReplace?: string, + pathReplaceWith?: string, ): Song => { return { _itemType: LibraryItem.SONG, @@ -162,7 +165,10 @@ const normalizeSong = ( mbzTrackId: null, name: item.title, participants: getParticipants(item), - path: item.path, + path: + pathReplace || pathReplaceWith + ? replacePathPrefix(item.path || '', pathReplace, pathReplaceWith) + : item.path, peak: item.replayGain && (item.replayGain.albumPeak || item.replayGain.trackPeak) ? { @@ -243,6 +249,8 @@ const getReleaseType = ( const normalizeAlbum = ( item: z.infer | z.infer, server?: null | ServerListItemWithCredential, + pathReplace?: string, + pathReplaceWith?: string, ): Album => { return { _itemType: LibraryItem.ALBUM, @@ -286,7 +294,7 @@ const normalizeAlbum = ( songCount: item.songCount, songs: (item as z.infer).song?.map((song) => - normalizeSong(song, server), + normalizeSong(song, server, pathReplace, pathReplaceWith), ) || [], tags: null, updatedAt: item.created, @@ -341,6 +349,8 @@ const normalizeGenre = ( const normalizeFolder = ( item: z.infer, server?: null | ServerListItemWithCredential, + pathReplace?: string, + pathReplaceWith?: string, ): Folder => { const results = item.child?.reduce( (acc: { folders: Folder[]; songs: Song[] }, item) => { @@ -350,7 +360,7 @@ const normalizeFolder = ( const folder = normalizeFolder(item, server); acc.folders.push(folder); } else { - const song = normalizeSong(item, server); + const song = normalizeSong(item, server, pathReplace, pathReplaceWith); acc.songs.push(song); } diff --git a/src/shared/api/utils.ts b/src/shared/api/utils.ts index 9f4aa2b92..f1335f8b9 100644 --- a/src/shared/api/utils.ts +++ b/src/shared/api/utils.ts @@ -471,3 +471,11 @@ export const sortRadioList = ( return results; }; + +export const replacePathPrefix = (path: string, replacePrefix?: string, addPrefix?: string) => { + if (replacePrefix && path.startsWith(replacePrefix)) { + return path.slice(replacePrefix.length); + } + + return addPrefix ? addPrefix + path : path; +}; diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index d3f4d4fd8..25464fa7d 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -403,6 +403,10 @@ type BaseEndpointArgs = { serverId: string; signal?: AbortSignal; }; + context?: { + pathReplace?: string; + pathReplaceWith?: string; + }; }; type GenreListSortMap = { @@ -1652,4 +1656,8 @@ type BaseEndpointArgsWithServer = { serverId: string; signal?: AbortSignal; }; + context?: { + pathReplace?: string; + pathReplaceWith?: string; + }; };