diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index a8caec4b..f0709093 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -36,6 +36,7 @@ "ascending": "ascending", "backward": "backward", "biography": "biography", + "bitDepth": "bit depth", "bitrate": "bitrate", "bpm": "bpm", "cancel": "cancel", @@ -99,6 +100,7 @@ "resetToDefault": "reset to default", "restartRequired": "restart required", "right": "right", + "sampleRate": "sample rate", "save": "save", "saveAndReplace": "save and replace", "saveAs": "save as", diff --git a/src/renderer/features/item-details/components/item-details-modal.tsx b/src/renderer/features/item-details/components/item-details-modal.tsx index 479fa4a6..e6bba6be 100644 --- a/src/renderer/features/item-details/components/item-details-modal.tsx +++ b/src/renderer/features/item-details/components/item-details-modal.tsx @@ -274,6 +274,8 @@ const SongPropertyMapping: ItemDetailRow[] = [ { label: 'filter.isCompilation', render: (song) => BoolField(song.compilation || false) }, { key: 'container', label: 'common.codec' }, { key: 'bitRate', label: 'common.bitrate', render: (song) => `${song.bitRate} kbps` }, + { key: 'sampleRate', label: 'common.sampleRate' }, + { key: 'bitDepth', label: 'common.bitDepth' }, { key: 'channels', label: 'common.channel_other' }, { key: 'size', label: 'common.size', render: (song) => formatSizeString(song.size) }, { diff --git a/src/shared/api/jellyfin/jellyfin-normalize.ts b/src/shared/api/jellyfin/jellyfin-normalize.ts index 0d453ff2..cb522f63 100644 --- a/src/shared/api/jellyfin/jellyfin-normalize.ts +++ b/src/shared/api/jellyfin/jellyfin-normalize.ts @@ -170,6 +170,47 @@ const normalizeSong = ( deviceId: string, imageSize?: number, ): Song => { + let bitRate = 0; + let channels: null | number = null; + let container: null | string = null; + let path: null | string = null; + let sampleRate: null | number = null; + let size = 0; + let streamUrl = ''; + + if (item.MediaSources?.length) { + const source = item.MediaSources[0]; + + container = source.Container; + path = source.Path; + size = source.Size; + + streamUrl = getStreamUrl({ + container: container, + deviceId, + eTag: source.ETag, + id: item.Id, + mediaSourceId: source.Id, + server, + }); + + if ((source.MediaStreams?.length || 0) > 0) { + for (const stream of source.MediaStreams) { + if (stream.Type === 'Audio') { + bitRate = + stream.BitRate !== undefined + ? Number(Math.trunc(stream.BitRate / 1000)) + : 0; + channels = stream.Channels || null; + sampleRate = stream.SampleRate || null; + break; + } + } + } + } else { + console.warn('Jellyfin song retrieved with no media sources', item); + } + return { album: item.Album, albumArtists: item.AlbumArtists?.map((entry) => ({ @@ -184,14 +225,13 @@ const normalizeSong = ( imageUrl: null, name: entry.Name, })), - bitRate: - item.MediaSources?.[0].Bitrate && - Number(Math.trunc(item.MediaSources[0].Bitrate / 1000)), + bitDepth: null, + bitRate, bpm: null, - channels: null, + channels, comment: null, compilation: null, - container: (item.MediaSources && item.MediaSources[0]?.Container) || null, + container, createdAt: item.DateCreated, discNumber: (item.ParentIndexNumber && item.ParentIndexNumber) || 1, discSubtitle: null, @@ -220,7 +260,7 @@ const normalizeSong = ( lyrics: null, name: item.Name, participants: getPeople(item), - path: (item.MediaSources && item.MediaSources[0]?.Path) || null, + path, peak: null, playCount: (item.UserData && item.UserData.PlayCount) || 0, playlistItemId: item.PlaylistItemId, @@ -230,17 +270,11 @@ const normalizeSong = ( ? new Date(item.ProductionYear, 0, 1).toISOString() : null, releaseYear: item.ProductionYear ? String(item.ProductionYear) : null, + sampleRate, serverId: server?.id || '', serverType: ServerType.JELLYFIN, - size: item.MediaSources && item.MediaSources[0]?.Size, - streamUrl: getStreamUrl({ - container: item.MediaSources?.[0]?.Container, - deviceId, - eTag: item.MediaSources?.[0]?.ETag, - id: item.Id, - mediaSourceId: item.MediaSources?.[0]?.Id, - server, - }), + size, + streamUrl, tags: getTags(item), trackNumber: item.IndexNumber, uniqueId: nanoid(), diff --git a/src/shared/api/navidrome/navidrome-normalize.ts b/src/shared/api/navidrome/navidrome-normalize.ts index 4c624b54..af5f9e61 100644 --- a/src/shared/api/navidrome/navidrome-normalize.ts +++ b/src/shared/api/navidrome/navidrome-normalize.ts @@ -148,6 +148,7 @@ const normalizeSong = ( albumId: item.albumId, ...getArtists(item), artistName: item.artist, + bitDepth: item.bitDepth || null, bitRate: item.bitRate, bpm: item.bpm ? item.bpm : null, channels: item.channels ? item.channels : null, @@ -189,6 +190,7 @@ const normalizeSong = ( : new Date(Date.UTC(item.year, 0, 1)) ).toISOString(), releaseYear: String(item.year), + sampleRate: item.sampleRate || null, serverId: server?.id || 'unknown', serverType: ServerType.NAVIDROME, size: item.size, diff --git a/src/shared/api/navidrome/navidrome-types.ts b/src/shared/api/navidrome/navidrome-types.ts index 631af88e..39a0c002 100644 --- a/src/shared/api/navidrome/navidrome-types.ts +++ b/src/shared/api/navidrome/navidrome-types.ts @@ -184,6 +184,7 @@ const song = z.object({ albumId: z.string(), artist: z.string(), artistId: z.string(), + bitDepth: z.number().optional(), bitRate: z.number(), bookmarkPosition: z.number(), bpm: z.number().optional(), @@ -226,6 +227,7 @@ const song = z.object({ rgAlbumPeak: z.number().optional(), rgTrackGain: z.number().optional(), rgTrackPeak: z.number().optional(), + sampleRate: z.number(), size: z.number(), smallImageUrl: z.string().optional(), sortAlbumArtistName: z.string(), diff --git a/src/shared/api/subsonic/subsonic-normalize.ts b/src/shared/api/subsonic/subsonic-normalize.ts index a380868e..83098c0a 100644 --- a/src/shared/api/subsonic/subsonic-normalize.ts +++ b/src/shared/api/subsonic/subsonic-normalize.ts @@ -135,9 +135,10 @@ const normalizeSong = ( albumId: item.albumId?.toString() || '', artistName: item.artist || '', artists: getArtistList(item.artists, item.artistId, item.artist), + bitDepth: item.bitDepth || null, bitRate: item.bitRate || 0, bpm: item.bpm || null, - channels: null, + channels: item.channelCount || null, comment: null, compilation: null, container: item.contentType, @@ -172,6 +173,7 @@ const normalizeSong = ( playCount: item?.playCount || 0, releaseDate: null, releaseYear: item.year ? String(item.year) : null, + sampleRate: item.samplingRate || null, serverId: server?.id || 'unknown', serverType: ServerType.SUBSONIC, size: item.size, diff --git a/src/shared/api/subsonic/subsonic-types.ts b/src/shared/api/subsonic/subsonic-types.ts index 3ab30ae4..82a182e0 100644 --- a/src/shared/api/subsonic/subsonic-types.ts +++ b/src/shared/api/subsonic/subsonic-types.ts @@ -85,8 +85,10 @@ const song = z.object({ artistId: id.optional(), artists: z.array(simpleArtist), averageRating: z.number().optional(), + bitDepth: z.number().optional(), bitRate: z.number().optional(), bpm: z.number().optional(), + channelCount: z.number().optional(), contentType: z.string(), contributors: z.array(contributor).optional(), coverArt: z.string().optional(), @@ -103,6 +105,7 @@ const song = z.object({ path: z.string(), playCount: z.number().optional(), replayGain: songGain.optional(), + samplingRate: z.number().optional(), size: z.number(), starred: z.boolean().optional(), suffix: z.string(), diff --git a/src/shared/types/domain-types.ts b/src/shared/types/domain-types.ts index 448413fa..137d72e7 100644 --- a/src/shared/types/domain-types.ts +++ b/src/shared/types/domain-types.ts @@ -320,6 +320,7 @@ export type Song = { albumId: string; artistName: string; artists: RelatedArtist[]; + bitDepth: null | number; bitRate: number; bpm: null | number; channels: null | number; @@ -346,6 +347,7 @@ export type Song = { playlistItemId?: string; releaseDate: null | string; releaseYear: null | string; + sampleRate: null | number; serverId: string; serverType: ServerType; size: number;