Compare commits

..

6 Commits

Author SHA1 Message Date
jeffvli e3d7e8e856 add default values for optional docker env (#1500) 2026-01-04 15:36:32 -08:00
jeffvli 5ec8f1a904 add fallback to year for navidrome release display (#1498) 2026-01-04 15:22:37 -08:00
jeffvli 83b20d9086 fix card container height when inconsistent row count 2026-01-04 15:07:34 -08:00
jeffvli 211f09fe19 fix presets delete (#1497) 2026-01-04 14:42:44 -08:00
jeffvli 03c1fb0ff2 add missing table body to hotkeys manager 2026-01-04 14:31:18 -08:00
jeffvli 834412ad31 fix release notes not displayed on version change 2026-01-04 14:24:34 -08:00
11 changed files with 314 additions and 154 deletions
+8 -1
View File
@@ -18,8 +18,15 @@ FROM nginx:alpine-slim
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
COPY ./settings.js.template /etc/nginx/templates/settings.js.template COPY ./settings.js.template /etc/nginx/templates/settings.js.template
COPY ng.conf.template /etc/nginx/templates/default.conf.template COPY ./ng.conf.template /etc/nginx/templates/default.conf.template
COPY ./docker-entrypoint.sh /docker-entrypoint.sh
RUN chmod +x /docker-entrypoint.sh
ENV PUBLIC_PATH="/" ENV PUBLIC_PATH="/"
ENV LEGACY_AUTHENTICATION="false"
ENV ANALYTICS_DISABLED="false"
EXPOSE 9180 EXPOSE 9180
ENTRYPOINT ["/docker-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"] CMD ["nginx", "-g", "daemon off;"]
+3 -2
View File
@@ -1,7 +1,7 @@
services: services:
feishin: feishin:
container_name: feishin container_name: feishin
image: 'ghcr.io/jeffvli/feishin:latest' image: ghcr.io/jeffvli/feishin:latest
restart: unless-stopped restart: unless-stopped
environment: environment:
- SERVER_NAME=jellyfin # pre-defined server name - SERVER_NAME=jellyfin # pre-defined server name
@@ -9,6 +9,7 @@ services:
- SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive - SERVER_TYPE=jellyfin # the allowed types are: jellyfin, navidrome, subsonic. These values are case insensitive
- SERVER_URL= # http://address:port or https://address:port - SERVER_URL= # http://address:port or https://address:port
- LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false) - LEGACY_AUTHENTICATION=false # When SERVER_LOCK is true, sets the legacyauth flag for server authentication (true or false)
- ANALYTICS_DISABLED=false # When true, disables analytics
ports: ports:
- 9180:9180 - 9180:9180
# Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190 # Alternatively, to restrict to only localhost, - 127.0.0.1:9180:8190
+7
View File
@@ -0,0 +1,7 @@
#!/bin/sh
# Set default values for environment variables if not already set
export LEGACY_AUTHENTICATION=${LEGACY_AUTHENTICATION:-false}
export ANALYTICS_DISABLED=${ANALYTICS_DISABLED:-false}
# Execute the original nginx command
exec "$@"
@@ -3,6 +3,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
width: 100%; width: 100%;
height: 100%;
padding: var(--theme-spacing-md); padding: var(--theme-spacing-md);
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
@@ -100,27 +100,55 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
const originalDifferentFromRelease = const originalDifferentFromRelease =
album?.originalDate && album?.originalDate !== album?.releaseDate; album?.originalDate && album?.originalDate !== album?.releaseDate;
const originalYearDifferentFromRelease = album?.originalYear !== album?.releaseYear;
const playCount = album?.playCount; const playCount = album?.playCount;
const releasePrefix = originalDifferentFromRelease const releasePrefix = originalDifferentFromRelease
? t('page.albumDetail.released', { postProcess: 'sentenceCase' }) ? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
: '♫'; : '♫';
if (originalDifferentFromRelease && album.originalDate) { const releaseYearPrefix = originalYearDifferentFromRelease
items.push({ ? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
id: 'originalDate', : '';
value: `${formatDateAbsoluteUTC(album.originalDate)}`,
}); if (album.originalDate) {
if (originalDifferentFromRelease) {
items.push({
id: 'originalDate',
value: `${formatDateAbsoluteUTC(album.originalDate)}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
});
}
} else if (album.originalYear) {
if (originalYearDifferentFromRelease) {
items.push({
id: 'originalYear',
value: `${album.originalYear}`,
});
}
if (releaseDate) {
items.push({
id: 'releaseDate',
value: `${releaseYearPrefix} ${formatDateAbsoluteUTC(releaseDate)}`,
});
} else if (releaseYear) {
items.push({
id: 'releaseYear',
value: `${releaseYearPrefix} ${releaseYear}`,
});
}
} }
items.push( items.push(
...[ ...[
{
id: 'releaseDate',
value: releaseDate
? `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`
: releaseYear,
},
{ {
id: 'songCount', id: 'songCount',
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }), value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
@@ -266,97 +266,102 @@ export const HotkeyManagerSettings = memo(() => {
/> />
<div className={styles.container}> <div className={styles.container}>
<Table withColumnBorders withRowBorders> <Table withColumnBorders withRowBorders>
{filteredBindings.map((binding) => ( <Table.Tbody>
<Table.Tr key={`hotkey-${binding}`}> {filteredBindings.map((binding) => (
<Table.Td style={{ userSelect: 'none' }}> <Table.Tr key={`hotkey-${binding}`}>
{BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]} <Table.Td style={{ userSelect: 'none' }}>
</Table.Td> {BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
<Table.Td> </Table.Td>
<TextInput <Table.Td>
id={`hotkey-${binding}`} <TextInput
leftSection={<Icon icon="keyboard" />} id={`hotkey-${binding}`}
onBlur={() => setSelected(null)} leftSection={<Icon icon="keyboard" />}
onChange={() => {}} onBlur={() => setSelected(null)}
onKeyDownCapture={(e) => { onChange={() => {}}
if (selected !== (binding as BindingActions)) onKeyDownCapture={(e) => {
return; if (selected !== (binding as BindingActions))
handleSetHotkey(binding as BindingActions, e); return;
}} handleSetHotkey(binding as BindingActions, e);
readOnly }}
rightSection={ readOnly
<ActionIcon rightSection={
icon="edit" <ActionIcon
onClick={() => { icon="edit"
setSelected(binding as BindingActions); onClick={() => {
document setSelected(binding as BindingActions);
.getElementById(`hotkey-${binding}`) document
?.focus(); .getElementById(`hotkey-${binding}`)
?.focus();
}}
variant="transparent"
/>
}
style={{
opacity:
selected === (binding as BindingActions)
? 0.8
: 1,
outline: duplicateHotkeyMap.includes(
bindings[
binding as keyof typeof BINDINGS_MAP
].hotkey!,
)
? '1px dashed red'
: undefined,
}}
value={
bindings[binding as keyof typeof BINDINGS_MAP]
.hotkey
}
/>
</Table.Td>
{isElectron() && (
<Table.Td>
<Checkbox
checked={
bindings[
binding as keyof typeof BINDINGS_MAP
].isGlobal
}
disabled={
bindings[
binding as keyof typeof BINDINGS_MAP
].hotkey === ''
}
onChange={(e) =>
handleSetGlobalHotkey(
binding as BindingActions,
e,
)
}
size="md"
style={{
opacity: bindings[
binding as keyof typeof BINDINGS_MAP
].allowGlobal
? 1
: 0,
}} }}
/>
</Table.Td>
)}
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
<Table.Td>
<ActionIcon
icon="x"
iconProps={{
color: 'error',
}}
onClick={() =>
handleClearHotkey(binding as BindingActions)
}
variant="transparent" variant="transparent"
/> />
} </Table.Td>
style={{ )}
opacity: </Table.Tr>
selected === (binding as BindingActions) ))}
? 0.8 </Table.Tbody>
: 1,
outline: duplicateHotkeyMap.includes(
bindings[binding as keyof typeof BINDINGS_MAP]
.hotkey!,
)
? '1px dashed red'
: undefined,
}}
value={
bindings[binding as keyof typeof BINDINGS_MAP]
.hotkey
}
/>
</Table.Td>
{isElectron() && (
<Table.Td>
<Checkbox
checked={
bindings[binding as keyof typeof BINDINGS_MAP]
.isGlobal
}
disabled={
bindings[binding as keyof typeof BINDINGS_MAP]
.hotkey === ''
}
onChange={(e) =>
handleSetGlobalHotkey(
binding as BindingActions,
e,
)
}
size="md"
style={{
opacity: bindings[
binding as keyof typeof BINDINGS_MAP
].allowGlobal
? 1
: 0,
}}
/>
</Table.Td>
)}
{bindings[binding as keyof typeof BINDINGS_MAP].hotkey && (
<Table.Td>
<ActionIcon
icon="x"
iconProps={{
color: 'error',
}}
onClick={() =>
handleClearHotkey(binding as BindingActions)
}
variant="transparent"
/>
</Table.Td>
)}
</Table.Tr>
))}
</Table> </Table>
</div> </div>
</> </>
@@ -1,5 +1,8 @@
import { nanoid } from 'nanoid';
export const audiomotionanalyzerPresets = [ export const audiomotionanalyzerPresets = [
{ {
id: nanoid(),
name: 'Preset 1', name: 'Preset 1',
value: { value: {
alphaBars: false, alphaBars: false,
@@ -52,6 +55,7 @@ export const audiomotionanalyzerPresets = [
}, },
}, },
{ {
id: nanoid(),
name: 'Preset 2', name: 'Preset 2',
value: { value: {
alphaBars: false, alphaBars: false,
@@ -104,6 +108,7 @@ export const audiomotionanalyzerPresets = [
}, },
}, },
{ {
id: nanoid(),
name: 'Preset 3', name: 'Preset 3',
value: { value: {
alphaBars: false, alphaBars: false,
@@ -158,6 +163,7 @@ export const audiomotionanalyzerPresets = [
}, },
}, },
{ {
id: nanoid(),
name: 'Preset 4', name: 'Preset 4',
value: { value: {
alphaBars: false, alphaBars: false,
@@ -1,4 +1,5 @@
import butterchurnPresets from 'butterchurn-presets'; import butterchurnPresets from 'butterchurn-presets';
import { nanoid } from 'nanoid';
import { useEffect, useMemo, useRef, useState } from 'react'; import { useEffect, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@@ -390,10 +391,10 @@ const PresetSettings = () => {
const [isPasting, setIsPasting] = useState(false); const [isPasting, setIsPasting] = useState(false);
const [pasteValue, setPasteValue] = useState(''); const [pasteValue, setPasteValue] = useState('');
const applyPreset = (presetName: null | string) => { const applyPreset = (presetId: null | string) => {
if (!presetName) return; if (!presetId) return;
const preset = visualizer.audiomotionanalyzer.presets.find((p) => p.name === presetName); const preset = visualizer.audiomotionanalyzer.presets.find((p) => p.id === presetId);
if (!preset) return; if (!preset) return;
@@ -484,7 +485,7 @@ const PresetSettings = () => {
if (existingPreset) { if (existingPreset) {
// Update existing preset // Update existing preset
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) => const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
p.name === newPresetName.trim() p.id === existingPreset.id
? { ? {
...p, ...p,
value: getCurrentSettingsAsPresetValue(), value: getCurrentSettingsAsPresetValue(),
@@ -499,9 +500,12 @@ const PresetSettings = () => {
}, },
}, },
}); });
setSelectedPreset(existingPreset.id);
} else { } else {
// Add new preset // Add new preset
const newPreset = { const newPreset = {
id: nanoid(),
name: newPresetName.trim(), name: newPresetName.trim(),
value: getCurrentSettingsAsPresetValue(), value: getCurrentSettingsAsPresetValue(),
}; };
@@ -513,11 +517,12 @@ const PresetSettings = () => {
}, },
}, },
}); });
setSelectedPreset(newPreset.id);
} }
setNewPresetName(''); setNewPresetName('');
setIsSaving(false); setIsSaving(false);
setSelectedPreset(newPresetName.trim());
}; };
const getCurrentSettingsAsPresetValue = () => { const getCurrentSettingsAsPresetValue = () => {
@@ -579,12 +584,17 @@ const PresetSettings = () => {
const handleUpdatePreset = () => { const handleUpdatePreset = () => {
if (!selectedPreset || !newPresetName.trim()) return; if (!selectedPreset || !newPresetName.trim()) return;
const selectedPresetObj = visualizer.audiomotionanalyzer.presets.find(
(p) => p.id === selectedPreset,
);
if (!selectedPresetObj) return;
let trimmedName = newPresetName.trim(); let trimmedName = newPresetName.trim();
const isRenaming = trimmedName !== selectedPreset; const isRenaming = trimmedName !== selectedPresetObj.name;
if (isRenaming) { if (isRenaming) {
const existingNames = visualizer.audiomotionanalyzer.presets const existingNames = visualizer.audiomotionanalyzer.presets
.filter((p) => p.name !== selectedPreset) .filter((p) => p.id !== selectedPreset)
.map((p) => p.name); .map((p) => p.name);
if (existingNames.includes(trimmedName)) { if (existingNames.includes(trimmedName)) {
@@ -600,7 +610,7 @@ const PresetSettings = () => {
} }
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) => const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
p.name === selectedPreset p.id === selectedPreset
? { ? {
...p, ...p,
name: trimmedName, name: trimmedName,
@@ -621,14 +631,13 @@ const PresetSettings = () => {
setNewPresetName(''); setNewPresetName('');
setIsRenaming(false); setIsRenaming(false);
setSelectedPreset(trimmedName);
}; };
const handleDeletePreset = () => { const handleDeletePreset = () => {
if (!selectedPreset) return; if (!selectedPreset) return;
const updatedPresets = visualizer.audiomotionanalyzer.presets.filter( const updatedPresets = visualizer.audiomotionanalyzer.presets.filter(
(p) => p.name !== selectedPreset, (p) => p.id !== selectedPreset,
); );
setSettings({ setSettings({
@@ -797,7 +806,7 @@ const PresetSettings = () => {
const presetOptions = useMemo(() => { const presetOptions = useMemo(() => {
return visualizer.audiomotionanalyzer.presets.map((preset) => ({ return visualizer.audiomotionanalyzer.presets.map((preset) => ({
label: preset.name, label: preset.name,
value: preset.name, value: preset.id,
})); }));
}, [visualizer.audiomotionanalyzer.presets]); }, [visualizer.audiomotionanalyzer.presets]);
@@ -907,8 +916,13 @@ const PresetSettings = () => {
<> <>
<Button <Button
onClick={() => { onClick={() => {
setNewPresetName(selectedPreset); const preset = visualizer.audiomotionanalyzer.presets.find(
setIsRenaming(true); (p) => p.id === selectedPreset,
);
if (preset) {
setNewPresetName(preset.name);
setIsRenaming(true);
}
}} }}
variant="default" variant="default"
> >
+30 -20
View File
@@ -130,7 +130,12 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
height: '400px', height: '400px',
}} }}
> >
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} /> <Text
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
fw={400}
lh="1.5"
size="md"
/>
</ScrollArea> </ScrollArea>
<Group justify="flex-end"> <Group justify="flex-end">
<Button <Button
@@ -151,11 +156,13 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
); );
}; };
const WAIT_FOR_LOCAL_STORAGE = 1000 * 2;
export const ReleaseNotesModal = () => { export const ReleaseNotesModal = () => {
const { version } = packageJson; const { version } = packageJson;
const { t } = useTranslation(); const { t } = useTranslation();
const [value, setValue] = useLocalStorage({ key: 'version' }); const [, setValue] = useLocalStorage({ key: 'version' });
const handleDismiss = useCallback(() => { const handleDismiss = useCallback(() => {
setValue(version); setValue(version);
@@ -163,25 +170,28 @@ export const ReleaseNotesModal = () => {
}, [setValue, version]); }, [setValue, version]);
useEffect(() => { useEffect(() => {
// If value is undefined, set it to current version but don't show modal const timeoutId = setTimeout(() => {
if (value === undefined) { const valueFromLocalStorage = localStorage.getItem('version');
setValue(version); const versionString = `"${version}"`;
return;
}
// Only show modal if the stored version is different from current version // Only show modal if the stored version is different from current version
if (value !== version) { if (valueFromLocalStorage !== versionString) {
openModal({ openModal({
children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />, children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />,
onClose: handleDismiss, onClose: handleDismiss,
size: 'xl', size: 'xl',
title: t('common.newVersion', { title: t('common.newVersion', {
postProcess: 'sentenceCase', postProcess: 'sentenceCase',
version, version,
}) as string, }) as string,
}); });
} }
}, [handleDismiss, value, version, t, setValue]); }, WAIT_FOR_LOCAL_STORAGE);
return () => {
clearTimeout(timeoutId);
};
}, [handleDismiss, t, version]);
return null; return null;
}; };
+33 -3
View File
@@ -1,5 +1,6 @@
import isElectron from 'is-electron'; import isElectron from 'is-electron';
import merge from 'lodash/merge'; import mergeWith from 'lodash/mergeWith';
import { nanoid } from 'nanoid';
import { generatePath } from 'react-router'; import { generatePath } from 'react-router';
import { z } from 'zod'; import { z } from 'zod';
import { devtools, persist } from 'zustand/middleware'; import { devtools, persist } from 'zustand/middleware';
@@ -51,7 +52,16 @@ const deepMergeIntoState = <T extends Record<string, any>>(
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { actions, ...updatesWithoutActions } = updates as any; const { actions, ...updatesWithoutActions } = updates as any;
merge(state, updatesWithoutActions); // Use mergeWith to replace arrays instead of merging them by index
mergeWith(state, updatesWithoutActions, (_objValue, srcValue) => {
// If source value is an array, replace the entire array instead of merging
if (Array.isArray(srcValue)) {
return srcValue;
}
// Default merge behavior
return undefined;
});
}; };
const HomeItemSchema = z.enum([ const HomeItemSchema = z.enum([
@@ -331,6 +341,7 @@ const AudioMotionAnalyzerSettingsSchema = z.object({
peakLine: z.boolean(), peakLine: z.boolean(),
presets: z.array( presets: z.array(
z.object({ z.object({
id: z.string(),
name: z.string(), name: z.string(),
value: z.any(), value: z.any(),
}), }),
@@ -1928,10 +1939,29 @@ export const useSettingsStore = createWithEqualityFn<SettingsSlice>()(
} }
} }
if (version <= 19) {
// Add IDs to presets that don't have them
if (
state.visualizer?.audiomotionanalyzer?.presets &&
Array.isArray(state.visualizer.audiomotionanalyzer.presets)
) {
state.visualizer.audiomotionanalyzer.presets =
state.visualizer.audiomotionanalyzer.presets.map((preset) => {
if (!preset.id) {
return {
...preset,
id: nanoid(),
};
}
return preset;
});
}
}
return persistedState; return persistedState;
}, },
name: 'store_settings', name: 'store_settings',
version: 19, version: 20,
}, },
), ),
); );
+67 -16
View File
@@ -37,36 +37,89 @@ const matchesFullDate = (date: string) => {
return Boolean(date.match(/^\d{4}-\d{2}-\d{2}$/)); return Boolean(date.match(/^\d{4}-\d{2}-\d{2}$/));
}; };
const normalizeReleaseDate = (item: { date?: string; releaseDate?: string }) => { const matchesYearOnly = (date: string) => {
return Boolean(date.match(/^\d{4}$/));
};
const normalizeReleaseDate = (item: {
date?: string;
releaseDate?: string;
}): { date: null | string; year: null | number } => {
if (item.releaseDate && matchesFullDate(item.releaseDate)) { if (item.releaseDate && matchesFullDate(item.releaseDate)) {
return item.releaseDate; return {
date: item.releaseDate,
year: parseInt(item.releaseDate.split('-')[0]),
};
} else if (item.releaseDate && matchesYearOnly(item.releaseDate)) {
return {
date: null,
year: parseInt(item.releaseDate),
};
} }
if (item.date && matchesFullDate(item.date)) { if (item.date && matchesFullDate(item.date)) {
return item.date; return {
date: item.date,
year: parseInt(item.date.split('-')[0]),
};
} else if (item.date && matchesYearOnly(item.date)) {
return {
date: null,
year: parseInt(item.date),
};
} }
return null; return {
date: null,
year: null,
};
}; };
const normalizeOriginalDate = (item: { const normalizeOriginalDate = (item: {
date?: string; date?: string;
originalDate?: string; originalDate?: string;
releaseDate?: string; releaseDate?: string;
}) => { }): { date: null | string; year: null | number } => {
if (item.originalDate && matchesFullDate(item.originalDate)) { if (item.originalDate && matchesFullDate(item.originalDate)) {
return item.originalDate; return {
date: item.originalDate,
year: parseInt(item.originalDate.split('-')[0]),
};
} else if (item.originalDate && matchesYearOnly(item.originalDate)) {
return {
date: null,
year: parseInt(item.originalDate),
};
} }
if (item.releaseDate && matchesFullDate(item.releaseDate)) { if (item.releaseDate && matchesFullDate(item.releaseDate)) {
return item.releaseDate; return {
date: item.releaseDate,
year: parseInt(item.releaseDate.split('-')[0]),
};
} else if (item.releaseDate && matchesYearOnly(item.releaseDate)) {
return {
date: null,
year: parseInt(item.releaseDate),
};
} }
if (item.date && matchesFullDate(item.date)) { if (item.date && matchesFullDate(item.date)) {
return item.date; return {
date: item.date,
year: parseInt(item.date.split('-')[0]),
};
} else if (item.date && matchesYearOnly(item.date)) {
return {
date: null,
year: parseInt(item.date),
};
} }
return null; return {
date: null,
year: null,
};
}; };
const getArtists = ( const getArtists = (
@@ -240,7 +293,7 @@ const normalizeSong = (
: null, : null,
playCount: item.playCount || 0, playCount: item.playCount || 0,
playlistItemId, playlistItemId,
releaseDate: normalizeReleaseDate(item), releaseDate: normalizeReleaseDate(item).date,
releaseYear: item.year || null, releaseYear: item.year || null,
sampleRate: item.sampleRate || null, sampleRate: item.sampleRate || null,
size: item.size, size: item.size,
@@ -303,9 +356,7 @@ const normalizeAlbum = (
pathReplaceWith?: string, pathReplaceWith?: string,
): Album => { ): Album => {
const releaseDate = normalizeReleaseDate(item); const releaseDate = normalizeReleaseDate(item);
const releaseYear = releaseDate ? parseInt(releaseDate.split('-')[0]) : null;
const originalDate = normalizeOriginalDate(item); const originalDate = normalizeOriginalDate(item);
const originalYear = originalDate ? parseInt(originalDate.split('-')[0]) : null;
return { return {
...parseAlbumTags(item), ...parseAlbumTags(item),
@@ -341,12 +392,12 @@ const normalizeAlbum = (
lastPlayedAt: normalizePlayDate(item), lastPlayedAt: normalizePlayDate(item),
mbzId: item.mbzAlbumId || null, mbzId: item.mbzAlbumId || null,
name: item.name, name: item.name,
originalDate, originalDate: originalDate.date,
originalYear, originalYear: originalDate.year,
playCount: item.playCount || 0, playCount: item.playCount || 0,
releaseDate, releaseDate: releaseDate.date,
releaseType: item.mbzAlbumType || null, releaseType: item.mbzAlbumType || null,
releaseYear, releaseYear: releaseDate.year,
size: item.size, size: item.size,
songCount: item.songCount, songCount: item.songCount,
songs: item.songs songs: item.songs