diff --git a/CHANGELOG.md b/CHANGELOG.md index d83f9012..1428d52a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [Unreleased] + +### Fixed + +- Normalize artist timestamp parsing in the frontend New additions view to correctly handle seconds/milliseconds and ignore invalid values. [#289](https://github.com/pSpitzner/beets-flask/issues/289) + ## [1.2.1] - 25-12-28 ### Fixed diff --git a/backend/beets_flask/importer/session.py b/backend/beets_flask/importer/session.py index 8cad55ac..e95c62bc 100644 --- a/backend/beets_flask/importer/session.py +++ b/backend/beets_flask/importer/session.py @@ -271,10 +271,10 @@ def get_config_value(self, key: str, type_func: Callable | None = None) -> Any: # get settings from user settings, this is not a dict, but confuse config # the confuse config views do not throw key errors, and their .get() is not # the same as dict.get(), but rather resolves the value. - default = get_config() + config_view = get_config() for p in path: - default = default[p] - default = default.get(type_func) if type_func else default.get() + config_view = config_view[p] + default = config_view.get(type_func) if type_func else config_view.get() return default # -------------------------- State handling helpers -------------------------- # diff --git a/frontend/src/api/library.ts b/frontend/src/api/library.ts index ca16b73e..6c2d09e5 100644 --- a/frontend/src/api/library.ts +++ b/frontend/src/api/library.ts @@ -1,6 +1,7 @@ import { infiniteQueryOptions, queryOptions } from '@tanstack/react-query'; import { toHex } from '@/components/common/strings'; +import { parseApiDate } from '@/components/common/units/time'; import { AlbumResponse, AlbumResponseExpanded, @@ -272,19 +273,10 @@ export const artistsQueryOptions = () => ({ for (let i = 0; i < artists.length; i++) { const artist = artists[i]; - // Convert timestamps to Date objects - if (artist.last_item_added) { - artist.last_item_added = new Date(artist.last_item_added); - } - if (artist.last_album_added) { - artist.last_album_added = new Date(artist.last_album_added); - } - if (artist.first_item_added) { - artist.first_item_added = new Date(artist.first_item_added); - } - if (artist.first_album_added) { - artist.first_album_added = new Date(artist.first_album_added); - } + artist.last_item_added = parseApiDate(artist.last_item_added); + artist.last_album_added = parseApiDate(artist.last_album_added); + artist.first_item_added = parseApiDate(artist.first_item_added); + artist.first_album_added = parseApiDate(artist.first_album_added); } return artists; // TODO: fill cache data for single artists queries diff --git a/frontend/src/components/common/units/time.tsx b/frontend/src/components/common/units/time.tsx index 4d398439..35f09668 100644 --- a/frontend/src/components/common/units/time.tsx +++ b/frontend/src/components/common/units/time.tsx @@ -1,3 +1,24 @@ +export const parseApiDate = (value: unknown): Date | undefined => { + if (value === null || value === undefined) { + return undefined; + } + + const numeric = typeof value === 'number' ? value : Number(value); + if (!Number.isNaN(numeric)) { + // Some API responses provide UNIX seconds while others use milliseconds. + const timestamp = numeric < 1e12 ? numeric * 1000 : numeric; + const date = new Date(timestamp); + return Number.isNaN(date.getTime()) ? undefined : date; + } + + if (typeof value === 'string') { + const date = new Date(value); + return Number.isNaN(date.getTime()) ? undefined : date; + } + + return undefined; +}; + export const relativeTime = (date?: Date | null) => { if (!date) return 'never';