mirror of
https://github.com/jeffvli/feishin.git
synced 2026-06-28 06:47:35 +02:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e3d7e8e856 | |||
| 5ec8f1a904 | |||
| 83b20d9086 | |||
| 211f09fe19 | |||
| 03c1fb0ff2 | |||
| 834412ad31 |
+8
-1
@@ -18,8 +18,15 @@ FROM nginx:alpine-slim
|
||||
|
||||
COPY --chown=nginx:nginx --from=builder /app/out/web /usr/share/nginx/html
|
||||
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 LEGACY_AUTHENTICATION="false"
|
||||
ENV ANALYTICS_DISABLED="false"
|
||||
|
||||
EXPOSE 9180
|
||||
ENTRYPOINT ["/docker-entrypoint.sh"]
|
||||
CMD ["nginx", "-g", "daemon off;"]
|
||||
|
||||
+3
-2
@@ -1,7 +1,7 @@
|
||||
services:
|
||||
feishin:
|
||||
container_name: feishin
|
||||
image: 'ghcr.io/jeffvli/feishin:latest'
|
||||
image: ghcr.io/jeffvli/feishin:latest
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- 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_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)
|
||||
- ANALYTICS_DISABLED=false # When true, disables analytics
|
||||
ports:
|
||||
- 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
|
||||
|
||||
@@ -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;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: var(--theme-spacing-md);
|
||||
overflow: hidden;
|
||||
user-select: none;
|
||||
|
||||
@@ -100,27 +100,55 @@ export const AlbumDetailHeader = forwardRef<HTMLDivElement>((_props, ref) => {
|
||||
const originalDifferentFromRelease =
|
||||
album?.originalDate && album?.originalDate !== album?.releaseDate;
|
||||
|
||||
const originalYearDifferentFromRelease = album?.originalYear !== album?.releaseYear;
|
||||
|
||||
const playCount = album?.playCount;
|
||||
|
||||
const releasePrefix = originalDifferentFromRelease
|
||||
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
||||
: '♫';
|
||||
|
||||
if (originalDifferentFromRelease && album.originalDate) {
|
||||
items.push({
|
||||
id: 'originalDate',
|
||||
value: `♫ ${formatDateAbsoluteUTC(album.originalDate)}`,
|
||||
});
|
||||
const releaseYearPrefix = originalYearDifferentFromRelease
|
||||
? t('page.albumDetail.released', { postProcess: 'sentenceCase' })
|
||||
: '♫';
|
||||
|
||||
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(
|
||||
...[
|
||||
{
|
||||
id: 'releaseDate',
|
||||
value: releaseDate
|
||||
? `${releasePrefix} ${formatDateAbsoluteUTC(releaseDate)}`
|
||||
: releaseYear,
|
||||
},
|
||||
{
|
||||
id: 'songCount',
|
||||
value: t('entity.trackWithCount', { count: detailQuery?.data?.songCount || 0 }),
|
||||
|
||||
@@ -266,97 +266,102 @@ export const HotkeyManagerSettings = memo(() => {
|
||||
/>
|
||||
<div className={styles.container}>
|
||||
<Table withColumnBorders withRowBorders>
|
||||
{filteredBindings.map((binding) => (
|
||||
<Table.Tr key={`hotkey-${binding}`}>
|
||||
<Table.Td style={{ userSelect: 'none' }}>
|
||||
{BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<TextInput
|
||||
id={`hotkey-${binding}`}
|
||||
leftSection={<Icon icon="keyboard" />}
|
||||
onBlur={() => setSelected(null)}
|
||||
onChange={() => {}}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (selected !== (binding as BindingActions))
|
||||
return;
|
||||
handleSetHotkey(binding as BindingActions, e);
|
||||
}}
|
||||
readOnly
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
icon="edit"
|
||||
onClick={() => {
|
||||
setSelected(binding as BindingActions);
|
||||
document
|
||||
.getElementById(`hotkey-${binding}`)
|
||||
?.focus();
|
||||
<Table.Tbody>
|
||||
{filteredBindings.map((binding) => (
|
||||
<Table.Tr key={`hotkey-${binding}`}>
|
||||
<Table.Td style={{ userSelect: 'none' }}>
|
||||
{BINDINGS_MAP[binding as keyof typeof BINDINGS_MAP]}
|
||||
</Table.Td>
|
||||
<Table.Td>
|
||||
<TextInput
|
||||
id={`hotkey-${binding}`}
|
||||
leftSection={<Icon icon="keyboard" />}
|
||||
onBlur={() => setSelected(null)}
|
||||
onChange={() => {}}
|
||||
onKeyDownCapture={(e) => {
|
||||
if (selected !== (binding as BindingActions))
|
||||
return;
|
||||
handleSetHotkey(binding as BindingActions, e);
|
||||
}}
|
||||
readOnly
|
||||
rightSection={
|
||||
<ActionIcon
|
||||
icon="edit"
|
||||
onClick={() => {
|
||||
setSelected(binding as BindingActions);
|
||||
document
|
||||
.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"
|
||||
/>
|
||||
}
|
||||
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"
|
||||
/>
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Td>
|
||||
)}
|
||||
</Table.Tr>
|
||||
))}
|
||||
</Table.Tbody>
|
||||
</Table>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { nanoid } from 'nanoid';
|
||||
|
||||
export const audiomotionanalyzerPresets = [
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Preset 1',
|
||||
value: {
|
||||
alphaBars: false,
|
||||
@@ -52,6 +55,7 @@ export const audiomotionanalyzerPresets = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Preset 2',
|
||||
value: {
|
||||
alphaBars: false,
|
||||
@@ -104,6 +108,7 @@ export const audiomotionanalyzerPresets = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Preset 3',
|
||||
value: {
|
||||
alphaBars: false,
|
||||
@@ -158,6 +163,7 @@ export const audiomotionanalyzerPresets = [
|
||||
},
|
||||
},
|
||||
{
|
||||
id: nanoid(),
|
||||
name: 'Preset 4',
|
||||
value: {
|
||||
alphaBars: false,
|
||||
|
||||
+27
-13
@@ -1,4 +1,5 @@
|
||||
import butterchurnPresets from 'butterchurn-presets';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@@ -390,10 +391,10 @@ const PresetSettings = () => {
|
||||
const [isPasting, setIsPasting] = useState(false);
|
||||
const [pasteValue, setPasteValue] = useState('');
|
||||
|
||||
const applyPreset = (presetName: null | string) => {
|
||||
if (!presetName) return;
|
||||
const applyPreset = (presetId: null | string) => {
|
||||
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;
|
||||
|
||||
@@ -484,7 +485,7 @@ const PresetSettings = () => {
|
||||
if (existingPreset) {
|
||||
// Update existing preset
|
||||
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
|
||||
p.name === newPresetName.trim()
|
||||
p.id === existingPreset.id
|
||||
? {
|
||||
...p,
|
||||
value: getCurrentSettingsAsPresetValue(),
|
||||
@@ -499,9 +500,12 @@ const PresetSettings = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setSelectedPreset(existingPreset.id);
|
||||
} else {
|
||||
// Add new preset
|
||||
const newPreset = {
|
||||
id: nanoid(),
|
||||
name: newPresetName.trim(),
|
||||
value: getCurrentSettingsAsPresetValue(),
|
||||
};
|
||||
@@ -513,11 +517,12 @@ const PresetSettings = () => {
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
setSelectedPreset(newPreset.id);
|
||||
}
|
||||
|
||||
setNewPresetName('');
|
||||
setIsSaving(false);
|
||||
setSelectedPreset(newPresetName.trim());
|
||||
};
|
||||
|
||||
const getCurrentSettingsAsPresetValue = () => {
|
||||
@@ -579,12 +584,17 @@ const PresetSettings = () => {
|
||||
const handleUpdatePreset = () => {
|
||||
if (!selectedPreset || !newPresetName.trim()) return;
|
||||
|
||||
const selectedPresetObj = visualizer.audiomotionanalyzer.presets.find(
|
||||
(p) => p.id === selectedPreset,
|
||||
);
|
||||
if (!selectedPresetObj) return;
|
||||
|
||||
let trimmedName = newPresetName.trim();
|
||||
const isRenaming = trimmedName !== selectedPreset;
|
||||
const isRenaming = trimmedName !== selectedPresetObj.name;
|
||||
|
||||
if (isRenaming) {
|
||||
const existingNames = visualizer.audiomotionanalyzer.presets
|
||||
.filter((p) => p.name !== selectedPreset)
|
||||
.filter((p) => p.id !== selectedPreset)
|
||||
.map((p) => p.name);
|
||||
|
||||
if (existingNames.includes(trimmedName)) {
|
||||
@@ -600,7 +610,7 @@ const PresetSettings = () => {
|
||||
}
|
||||
|
||||
const updatedPresets = visualizer.audiomotionanalyzer.presets.map((p) =>
|
||||
p.name === selectedPreset
|
||||
p.id === selectedPreset
|
||||
? {
|
||||
...p,
|
||||
name: trimmedName,
|
||||
@@ -621,14 +631,13 @@ const PresetSettings = () => {
|
||||
|
||||
setNewPresetName('');
|
||||
setIsRenaming(false);
|
||||
setSelectedPreset(trimmedName);
|
||||
};
|
||||
|
||||
const handleDeletePreset = () => {
|
||||
if (!selectedPreset) return;
|
||||
|
||||
const updatedPresets = visualizer.audiomotionanalyzer.presets.filter(
|
||||
(p) => p.name !== selectedPreset,
|
||||
(p) => p.id !== selectedPreset,
|
||||
);
|
||||
|
||||
setSettings({
|
||||
@@ -797,7 +806,7 @@ const PresetSettings = () => {
|
||||
const presetOptions = useMemo(() => {
|
||||
return visualizer.audiomotionanalyzer.presets.map((preset) => ({
|
||||
label: preset.name,
|
||||
value: preset.name,
|
||||
value: preset.id,
|
||||
}));
|
||||
}, [visualizer.audiomotionanalyzer.presets]);
|
||||
|
||||
@@ -907,8 +916,13 @@ const PresetSettings = () => {
|
||||
<>
|
||||
<Button
|
||||
onClick={() => {
|
||||
setNewPresetName(selectedPreset);
|
||||
setIsRenaming(true);
|
||||
const preset = visualizer.audiomotionanalyzer.presets.find(
|
||||
(p) => p.id === selectedPreset,
|
||||
);
|
||||
if (preset) {
|
||||
setNewPresetName(preset.name);
|
||||
setIsRenaming(true);
|
||||
}
|
||||
}}
|
||||
variant="default"
|
||||
>
|
||||
|
||||
@@ -130,7 +130,12 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
||||
height: '400px',
|
||||
}}
|
||||
>
|
||||
<div dangerouslySetInnerHTML={{ __html: sanitizedHtml }} />
|
||||
<Text
|
||||
dangerouslySetInnerHTML={{ __html: sanitizedHtml }}
|
||||
fw={400}
|
||||
lh="1.5"
|
||||
size="md"
|
||||
/>
|
||||
</ScrollArea>
|
||||
<Group justify="flex-end">
|
||||
<Button
|
||||
@@ -151,11 +156,13 @@ const ReleaseNotesContent = ({ onDismiss, version }: ReleaseNotesContentProps) =
|
||||
);
|
||||
};
|
||||
|
||||
const WAIT_FOR_LOCAL_STORAGE = 1000 * 2;
|
||||
|
||||
export const ReleaseNotesModal = () => {
|
||||
const { version } = packageJson;
|
||||
const { t } = useTranslation();
|
||||
|
||||
const [value, setValue] = useLocalStorage({ key: 'version' });
|
||||
const [, setValue] = useLocalStorage({ key: 'version' });
|
||||
|
||||
const handleDismiss = useCallback(() => {
|
||||
setValue(version);
|
||||
@@ -163,25 +170,28 @@ export const ReleaseNotesModal = () => {
|
||||
}, [setValue, version]);
|
||||
|
||||
useEffect(() => {
|
||||
// If value is undefined, set it to current version but don't show modal
|
||||
if (value === undefined) {
|
||||
setValue(version);
|
||||
return;
|
||||
}
|
||||
const timeoutId = setTimeout(() => {
|
||||
const valueFromLocalStorage = localStorage.getItem('version');
|
||||
const versionString = `"${version}"`;
|
||||
|
||||
// Only show modal if the stored version is different from current version
|
||||
if (value !== version) {
|
||||
openModal({
|
||||
children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />,
|
||||
onClose: handleDismiss,
|
||||
size: 'xl',
|
||||
title: t('common.newVersion', {
|
||||
postProcess: 'sentenceCase',
|
||||
version,
|
||||
}) as string,
|
||||
});
|
||||
}
|
||||
}, [handleDismiss, value, version, t, setValue]);
|
||||
// Only show modal if the stored version is different from current version
|
||||
if (valueFromLocalStorage !== versionString) {
|
||||
openModal({
|
||||
children: <ReleaseNotesContent onDismiss={handleDismiss} version={version} />,
|
||||
onClose: handleDismiss,
|
||||
size: 'xl',
|
||||
title: t('common.newVersion', {
|
||||
postProcess: 'sentenceCase',
|
||||
version,
|
||||
}) as string,
|
||||
});
|
||||
}
|
||||
}, WAIT_FOR_LOCAL_STORAGE);
|
||||
|
||||
return () => {
|
||||
clearTimeout(timeoutId);
|
||||
};
|
||||
}, [handleDismiss, t, version]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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 { z } from 'zod';
|
||||
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
|
||||
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([
|
||||
@@ -331,6 +341,7 @@ const AudioMotionAnalyzerSettingsSchema = z.object({
|
||||
peakLine: z.boolean(),
|
||||
presets: z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
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;
|
||||
},
|
||||
name: 'store_settings',
|
||||
version: 19,
|
||||
version: 20,
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
@@ -37,36 +37,89 @@ const matchesFullDate = (date: string) => {
|
||||
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)) {
|
||||
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)) {
|
||||
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: {
|
||||
date?: string;
|
||||
originalDate?: string;
|
||||
releaseDate?: string;
|
||||
}) => {
|
||||
}): { date: null | string; year: null | number } => {
|
||||
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)) {
|
||||
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)) {
|
||||
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 = (
|
||||
@@ -240,7 +293,7 @@ const normalizeSong = (
|
||||
: null,
|
||||
playCount: item.playCount || 0,
|
||||
playlistItemId,
|
||||
releaseDate: normalizeReleaseDate(item),
|
||||
releaseDate: normalizeReleaseDate(item).date,
|
||||
releaseYear: item.year || null,
|
||||
sampleRate: item.sampleRate || null,
|
||||
size: item.size,
|
||||
@@ -303,9 +356,7 @@ const normalizeAlbum = (
|
||||
pathReplaceWith?: string,
|
||||
): Album => {
|
||||
const releaseDate = normalizeReleaseDate(item);
|
||||
const releaseYear = releaseDate ? parseInt(releaseDate.split('-')[0]) : null;
|
||||
const originalDate = normalizeOriginalDate(item);
|
||||
const originalYear = originalDate ? parseInt(originalDate.split('-')[0]) : null;
|
||||
|
||||
return {
|
||||
...parseAlbumTags(item),
|
||||
@@ -341,12 +392,12 @@ const normalizeAlbum = (
|
||||
lastPlayedAt: normalizePlayDate(item),
|
||||
mbzId: item.mbzAlbumId || null,
|
||||
name: item.name,
|
||||
originalDate,
|
||||
originalYear,
|
||||
originalDate: originalDate.date,
|
||||
originalYear: originalDate.year,
|
||||
playCount: item.playCount || 0,
|
||||
releaseDate,
|
||||
releaseDate: releaseDate.date,
|
||||
releaseType: item.mbzAlbumType || null,
|
||||
releaseYear,
|
||||
releaseYear: releaseDate.year,
|
||||
size: item.size,
|
||||
songCount: item.songCount,
|
||||
songs: item.songs
|
||||
|
||||
Reference in New Issue
Block a user