fix(library): multi-genre local index with track_genre and backfill (#1059)

* fix(library): multi-genre local index with track_genre and backfill

Restore atomic genre browse, filters, and counts via track_genre:
OpenSubsonic genres[] first with Navidrome-default split fallback, sync
write path, read-path query switches, blocking startup backfill with
progress, and v12 repair migration for DBs that recorded legacy 002–011.
TS fallback adds genreTagsFor and migration gate i18n across locales.

* fix(library): address multi-genre review — robust TS genres and scope join

genreTagsFor routes raw genres through parseItemGenres (single-object
Subsonic quirk and bare strings). Library-scoped genre browse/counts join
track for raw_json library_id fallback. Statistics keeps empty-genre bucket.

* docs: CHANGELOG and credits for multi-genre local index (PR #1059)

* docs(changelog): credit HiveMind on Discord for multi-genre report (PR #1059)
This commit is contained in:
cucadmuh
2026-06-11 01:02:35 +03:00
committed by GitHub
parent 8593858f3a
commit 5cd01c90ac
55 changed files with 1322 additions and 145 deletions
+21
View File
@@ -892,3 +892,24 @@ export function subscribeLibrarySyncIdle(
handler(payload),
);
}
// ── Genre tags startup backfill (multi-genre local index) ───────────────
export interface GenreTagsInspectDto {
needed: boolean;
totalTracks: number;
doneTracks: number;
}
export interface GenreTagsProgressEvent {
done: number;
total: number;
}
export function libraryGenreTagsInspect(): Promise<GenreTagsInspectDto> {
return invoke<GenreTagsInspectDto>('library_genre_tags_inspect');
}
export function libraryGenreTagsRun(): Promise<void> {
return invoke<void>('library_genre_tags_run');
}
+4
View File
@@ -1,4 +1,5 @@
import type { SubsonicAlbum, SubsonicArtist, SubsonicSong } from './subsonicTypes';
import { parseItemGenres } from '../utils/library/genreTags';
import { invoke } from '@tauri-apps/api/core';
import { useAuthStore } from '../store/authStore';
import { ndLogin } from './navidromeAdmin';
@@ -54,6 +55,7 @@ function mapNdSong(o: Record<string, unknown>): SubsonicSong {
userRating: asNumber(o.rating),
starred: o.starred ? asString(o.starredAt) || 'true' : undefined,
genre: typeof o.genre === 'string' ? o.genre : undefined,
genres: parseItemGenres(o.genres),
bitRate: asNumber(o.bitRate),
suffix: typeof o.suffix === 'string' ? o.suffix : undefined,
contentType: typeof o.contentType === 'string' ? o.contentType : undefined,
@@ -166,6 +168,7 @@ function mapNdAlbum(o: Record<string, unknown>): SubsonicAlbum {
duration: asNumber(o.duration) ?? 0,
year: asNumber(o.maxYear) ?? asNumber(o.year),
genre: typeof o.genre === 'string' ? o.genre : undefined,
genres: parseItemGenres(o.genres),
starred: starredFlag ? (starredAt ?? 'true') : undefined,
userRating: asNumber(o.rating),
isCompilation: o.compilation === true,
@@ -383,6 +386,7 @@ export async function ndListLosslessAlbumsPage(req: NdLosslessPageRequest): Prom
duration: 0,
year: asNumber(o.year),
genre: typeof o.genre === 'string' ? o.genre : undefined,
genres: parseItemGenres(o.genres),
};
pageEntries.push({ album, bitDepth, sampleRate: asNumber(o.sampleRate) ?? 0 });
}
+11 -7
View File
@@ -1,4 +1,5 @@
import { useAuthStore } from '../store/authStore';
import { genreTagsFor } from '../utils/library/genreTags';
import { getArtists } from './subsonicArtists';
import { getAlbumList, getRandomSongs } from './subsonicLibrary';
import type {
@@ -54,14 +55,17 @@ export async function fetchStatisticsLibraryAggregates(): Promise<StatisticsLibr
albumsCounted += 1;
const sc = a.songCount ?? 0;
songsCounted += sc;
const label = (a.genre?.trim()) ? a.genre.trim() : '';
let g = genreAgg.get(label);
if (!g) {
g = { songCount: 0, albumCount: 0 };
genreAgg.set(label, g);
const tags = genreTagsFor(a);
const labels = tags.length > 0 ? tags : [''];
for (const label of labels) {
let g = genreAgg.get(label);
if (!g) {
g = { songCount: 0, albumCount: 0 };
genreAgg.set(label, g);
}
g.songCount += sc;
g.albumCount += 1;
}
g.songCount += sc;
g.albumCount += 1;
}
if (albums.length < pageSize) break;
offset += pageSize;
+9
View File
@@ -1,5 +1,10 @@
import type { SubsonicServerIdentity } from '../utils/server/subsonicServerIdentity';
/** OpenSubsonic `ItemGenre` on songs/albums (atomic genres from the server). */
export interface SubsonicItemGenre {
name: string;
}
export interface SubsonicAlbum {
id: string;
name: string;
@@ -11,6 +16,8 @@ export interface SubsonicAlbum {
playCount?: number;
year?: number;
genre?: string;
/** OpenSubsonic atomic genres — preferred over splitting `genre`. */
genres?: SubsonicItemGenre[];
starred?: string;
recordLabel?: string;
created?: string;
@@ -72,6 +79,8 @@ export interface SubsonicSong {
channelCount?: number;
starred?: string;
genre?: string;
/** OpenSubsonic atomic genres — preferred over splitting `genre`. */
genres?: SubsonicItemGenre[];
path?: string;
albumArtist?: string;
/** OpenSubsonic: single-string album-artist for display (mirrors `albumArtists` joined). */