Files
Psychotoxical-psysonic/src/api/subsonicTypes.ts
T
cucadmuh 003b280a77 feat(enrichment): oximedia BPM/mood facts, mood search, and queue display (#863)
* feat(enrichment): oximedia BPM/mood facts, mood search, and queue UI

Run client-side oximedia analysis after CPU seed and persist BPM, mood JSON,
and searchable mood_tag facts. Add product mood groups (joy/sadness/dance/work/
romance) with Advanced Search filter on the local index, queue BPM/mood display,
migration 008 mood_tag index, and refreshed licenses for oximedia crates.

* fix(enrichment): keep mood_groups module comment in English

* feat(search): virtual mood groups, anger filter, and Advanced Search UX

Expand mood search via overlapping virtual groups (tag expansion only),
add anger/Злость group, skip album/artist shortcuts for track-only filters,
and simplify mood search UI (songs-only, hide type tabs). Fix CustomSelect
spurious scrollbar on short option lists.

* feat(analysis): unified track analysis plan and enqueue path

Add TrackAnalysisPlan (waveform, LUFS, enrichment) with a single
enqueue_track_analysis entry for all byte-backed triggers. Run enrichment
when cache is full but library facts are missing; route playback, cache,
and backfill through the planner. Fix browseTextSearch LocalSearchOpts tsc
gap and remove obsolete read_seed_bytes_if_needed helper.

* fix(analysis): wire playback dispatch, preload enrichment, and UI refresh

Route stream, gapless, preload, and local-file playback through analysis_dispatch
so BPM/mood enrichment runs when waveform/LUFS are already cached. Fix audio_preload
cache-hit and hot-cache paths, emit preload-cancelled for retry, and add
analysis:enrichment-updated plus content_cache_coverage key resolution.

* fix(audio): preload local files from disk and stop analysis retry loop

Seed hot/offline next tracks via LocalFilePlayback (512 MiB) instead of copying
into the RAM preload slot. Keep bytePreloadingId set after preload-ready so
progress ticks do not re-invoke audio_preload every second.

* fix(enrichment): clippy, album bpm filter routing, and queue mood display

Clippy-clean analysis_dispatch and engine imports; restrict track-derived album
routing to mood_group/mood_tag only so bpm is skipped on album queries. Log
enrichment plan errors with retry-all plan; filter queue mood labels to oximedia ids.

* fix(enrichment): simplify mood_tag backfill branch in plan_track_enrichment

Remove empty if-block; keep same behaviour when backfill fails and moods row exists.

* docs: CHANGELOG and credits for track enrichment PR #863

* docs(credits): track enrichment PR #863 contributor line

* chore(enrichment): clippy-clean plan branch and trim dead exports

Collapse mood_tag backfill if for clippy; remove unused moodGroupById and
OXIMEDIA_MOOD_LABELS re-exports; stop poll when server BPM is already known.

* fix(enrichment): close R2/S1–S3 limits and Song Info BPM fallback

Return TrackEnrichmentOutcome::Failed on oximedia errors so retries are not
masked as complete; extract mood Advanced Search SQL, unify top-3 mood tag
selection in mood_groups with TS invariant tests, and show measured BPM in
Song Info when tag BPM is missing or zero.

* fix(enrichment): restore offline coverage and show mood in Song Info

Add unit tests for offline download cancel/clear registry after the analysis
seed refactor dropped read_seed_bytes coverage; show localized mood labels in
Song Info when library enrichment facts exist.

* fix(enrichment): soft mood scoring and unblock offline cancel tests

Replace oximedia quadrant happy/excited mapping with valence/arousal
soft scores across all mood tags for display, storage, and backfill;
fix offline cancel unit tests that deadlocked by calling clear while
holding the global offline_cancel_flags mutex.

* fix(enrichment): dedupe joy cluster and cap mood display at two labels

Never show happy and excited together; pick one tag per V/A cluster,
tighten oximedia recalibration, and limit queue/Song Info to two moods
that pass a relative score floor.

* fix(enrichment): disable oximedia mood labels in UI and search tags

Oximedia 0.1.7 mood is a spectral energy heuristic, not independent mood
weights; valence correlates with loud/bright audio and false-labels metal
and lyrical tracks as happy. Hide queue/Song Info mood and stop writing
mood_tag facts until a reliable detector lands; keep V/A/moods JSON stored.

* fix(enrichment): disable oximedia mood analysis and add BPM advanced search

Stop planning, running, and storing oximedia mood facts; purge accumulated
mood rows via migration 009. Hide mood filters in Advanced Search, expose
BPM range filter with dual-storage resolution, and show a BPM column in song
results when that filter is active.

* feat(search): analysis BPM priority, source tooltip, and filter UX

Prefer analysis track_fact over file tags for BPM resolution; show source
in list tooltips. Validate BPM range on blur, add clear button, fix double tooltip.

* fix(enrichment): prefer analysis BPM in Song Info and queue tech row

Show measured track_fact BPM before file tags until analysis completes;
pick the highest-confidence analysis fact when several exist.
2026-05-23 18:54:04 +03:00

264 lines
6.4 KiB
TypeScript

import type { SubsonicServerIdentity } from '../utils/server/subsonicServerIdentity';
export interface SubsonicAlbum {
id: string;
name: string;
artist: string;
artistId: string;
coverArt?: string;
songCount: number;
duration: number;
playCount?: number;
year?: number;
genre?: string;
starred?: string;
recordLabel?: string;
created?: string;
/** Present on some servers (e.g. OpenSubsonic) for album-level rating. */
userRating?: number;
/** OpenSubsonic: true when the album is tagged as a compilation. */
isCompilation?: boolean;
/** OpenSubsonic: release types from MusicBrainz tags (e.g. "Album", "EP", "Single", "Compilation", "Live"). */
releaseTypes?: string[];
/** OpenSubsonic: structured album-artist credits (e.g. featured guests on the album). */
artists?: SubsonicOpenArtistRef[];
/** OpenSubsonic: single-string album-artist for display (mirrors `artists` joined). */
displayArtist?: string;
/** OpenSubsonic: per-disc subtitles (e.g. "Sessions" on CD 3 of a deluxe edition). */
discTitles?: SubsonicDiscTitle[];
}
export interface SubsonicDiscTitle {
disc: number;
title: string;
}
/** OpenSubsonic `artists` / `albumArtists` entries on a child song (may include `userRating`). */
export interface SubsonicOpenArtistRef {
id?: string;
name?: string;
userRating?: number;
/** Navidrome / alternate OpenSubsonic payloads (same meaning as `userRating`). */
rating?: number;
}
export interface SubsonicSong {
id: string;
title: string;
artist: string;
album: string;
albumId: string;
artistId?: string;
duration: number;
track?: number;
discNumber?: number;
coverArt?: string;
year?: number;
userRating?: number;
/** Some OpenSubsonic responses attach parent ratings on child songs. */
albumUserRating?: number;
artistUserRating?: number;
artists?: SubsonicOpenArtistRef[];
albumArtists?: SubsonicOpenArtistRef[];
// Audio technical info
bitRate?: number;
suffix?: string;
contentType?: string;
size?: number;
samplingRate?: number;
bitDepth?: number;
channelCount?: number;
starred?: string;
genre?: string;
path?: string;
albumArtist?: string;
/** ISRC code when available (e.g., Navidrome) */
isrc?: string;
/** Times the track has been played, surfaced by Navidrome's Subsonic API. */
playCount?: number;
/** ISO datetime of the last play, surfaced by Navidrome (OpenSubsonic). */
played?: string;
/** Beats per minute, surfaced by Navidrome when the tag is set on the file. */
bpm?: number;
/** Local index Advanced Search: `'tag'` (server/file tag) or `'analysis'` (measured). */
localBpmSource?: 'tag' | 'analysis';
replayGain?: {
trackGain?: number;
albumGain?: number;
trackPeak?: number;
albumPeak?: number;
};
/** OpenSubsonic: structured composer credit (string for back-compat). */
displayComposer?: string;
/** OpenSubsonic: structured contributors list — Navidrome ≥ 0.55. */
contributors?: Array<{
role: string;
subRole?: string;
artist: { id?: string; name: string };
}>;
}
export interface InternetRadioStation {
id: string;
name: string;
streamUrl: string;
homepageUrl?: string;
coverArt?: string; // Navidrome v0.61.0+
}
export interface RadioBrowserStation {
stationuuid: string;
name: string;
url: string;
favicon: string;
tags: string;
}
export interface SubsonicPlaylist {
id: string;
name: string;
songCount: number;
duration: number;
created: string;
changed: string;
owner?: string;
public?: boolean;
comment?: string;
coverArt?: string;
}
export interface SubsonicNowPlaying extends SubsonicSong {
username: string;
minutesAgo: number;
playerId: number;
playerName: string;
}
export interface SubsonicArtist {
id: string;
name: string;
albumCount?: number;
coverArt?: string;
starred?: string;
/** Present on some servers (e.g. OpenSubsonic) for artist-level rating. */
userRating?: number;
}
export interface SubsonicGenre {
value: string;
songCount: number;
albumCount: number;
}
export interface SubsonicMusicFolder {
id: string;
name: string;
}
export interface SubsonicArtistInfo {
biography?: string;
musicBrainzId?: string;
lastFmUrl?: string;
smallImageUrl?: string;
mediumImageUrl?: string;
largeImageUrl?: string;
similarArtist?: Array<{ id: string; name: string; albumCount?: number }>;
}
export interface SubsonicDirectoryEntry {
id: string;
parent?: string;
title: string;
isDir: boolean;
album?: string;
artist?: string;
albumId?: string;
artistId?: string;
coverArt?: string;
duration?: number;
track?: number;
year?: number;
bitRate?: number;
suffix?: string;
size?: number;
genre?: string;
starred?: string;
userRating?: number;
}
export interface SubsonicDirectory {
id: string;
parent?: string;
name: string;
child: SubsonicDirectoryEntry[];
}
export type PingWithCredentialsResult = SubsonicServerIdentity & { ok: boolean };
export interface RandomSongsFilters {
size?: number;
genre?: string;
fromYear?: number;
toYear?: number;
}
export interface StatisticsLibraryAggregates {
playtimeSec: number;
albumsCounted: number;
songsCounted: number;
capped: boolean;
genres: SubsonicGenre[];
}
export interface StatisticsOverviewData {
recent: SubsonicAlbum[];
frequent: SubsonicAlbum[];
highest: SubsonicAlbum[];
artistCount: number;
}
export interface StatisticsFormatSample {
rows: { format: string; count: number }[];
sampleSize: number;
}
export interface SearchResults {
artists: SubsonicArtist[];
albums: SubsonicAlbum[];
songs: SubsonicSong[];
}
export interface StarredResults {
artists: SubsonicArtist[];
albums: SubsonicAlbum[];
songs: SubsonicSong[];
}
export type EntityRatingSupportLevel = 'track_only' | 'full';
export interface AlbumInfo {
largeImageUrl?: string;
mediumImageUrl?: string;
smallImageUrl?: string;
notes?: string;
}
export const RADIO_PAGE_SIZE = 25;
export interface SubsonicLyricLine {
start?: number; // milliseconds — absent when unsynced
value: string;
}
export interface SubsonicStructuredLyrics {
/** OpenSubsonic spec field name (Navidrome ≥ 0.50.0 / any OpenSubsonic server). */
synced?: boolean;
/** Legacy / alternate casing used by some older Subsonic-compatible servers. */
issynced?: boolean;
lang?: string;
offset?: number;
displayArtist?: string;
displayTitle?: string;
line: SubsonicLyricLine[];
}