From 298a4022ffbe253ca239efc5f63c15519d0125f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pierre=20de=20la=20Martini=C3=A8re?= Date: Wed, 18 Mar 2026 03:35:20 +0100 Subject: [PATCH] Add support for Album Artist Sort --- src-tauri/src/libs/database.rs | 54 ++++++-- src-tauri/src/libs/mod.rs | 4 + src-tauri/src/libs/tests/database_tests.rs | 119 ++++++++++++++---- src-tauri/src/libs/tests/track_test.rs | 22 ++++ src-tauri/src/libs/track.rs | 75 ++++++++--- .../04_add_album_artist_sort_columns.sql | 3 + src-tauri/src/plugins/db.rs | 4 +- src/components/SideNav.tsx | 8 +- src/components/SideNavLink.tsx | 1 + src/generated/typings.ts | 4 +- src/lib/__mocks__/bridge-database.ts | 6 +- src/lib/bridge-database.ts | 7 +- src/lib/utils-library.ts | 4 +- src/routes/artists.tsx | 11 +- src/routes/tracks.$trackID.tsx | 15 +++ src/translations/en.po | 46 +++---- src/translations/es.po | 46 +++---- src/translations/fr.po | 46 +++---- src/translations/ja.po | 46 +++---- src/translations/ru.po | 46 +++---- src/translations/zh-CN.po | 46 +++---- src/translations/zh-TW.po | 46 +++---- src/types/museeks.ts | 1 + 23 files changed, 442 insertions(+), 218 deletions(-) create mode 100644 src-tauri/src/libs/tests/track_test.rs create mode 100644 src-tauri/src/migrations/04_add_album_artist_sort_columns.sql diff --git a/src-tauri/src/libs/database.rs b/src-tauri/src/libs/database.rs index 7879b9b33..3b0a8b74a 100644 --- a/src-tauri/src/libs/database.rs +++ b/src-tauri/src/libs/database.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use super::error::AnyResult; use super::playlist::Playlist; -use super::track::{Track, TrackGroup}; +use super::track::{Artist, Track, TrackGroup}; // Single source of truth for supported audio formats (extension, MIME type). // KEEP IN SYNC with Tauri's file associations in tauri.conf.json @@ -137,6 +137,7 @@ impl DB { title = ?, album = ?, album_artist = ?, + album_artist_sort = ?, artists = ?, genres = ?, year = ?, @@ -153,6 +154,7 @@ impl DB { .bind(&track.title) .bind(&track.album) .bind(&track.album_artist) + .bind(&track.album_artist_sort) .bind(json!(&track.artists)) .bind(json!(&track.genres)) .bind(track.year) @@ -196,9 +198,24 @@ impl DB { sqlx::query( r#" INSERT INTO tracks ( - id, path, title, album, album_artist, artists, genres, year, - duration, track_no, track_of, disk_no, disk_of, is_compilation - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + id, + path, + title, + album, + album_artist, + album_artist_sort, + artists, + genres, + year, + duration, + track_no, + track_of, + disk_no, + disk_of, + is_compilation + ) VALUES ( + ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ? + ) "#, ) .bind(&track.id) @@ -206,6 +223,7 @@ impl DB { .bind(&track.title) .bind(&track.album) .bind(&track.album_artist) + .bind(&track.album_artist_sort) .bind(json!(&track.artists)) .bind(json!(&track.genres)) .bind(track.year) @@ -237,6 +255,7 @@ impl DB { title = ?, album = ?, album_artist = ?, + album_artist_sort = ?, artists = ?, genres = ?, year = ?, @@ -253,6 +272,7 @@ impl DB { .bind(&track.title) .bind(&track.album) .bind(&track.album_artist) + .bind(&track.album_artist_sort) .bind(json!(&track.artists)) .bind(json!(&track.genres)) .bind(track.year) @@ -274,14 +294,24 @@ impl DB { /** * Get the list of artists registered in the database, excluding compilation tracks. */ - pub async fn get_artists(&mut self) -> AnyResult> { - let result: Vec = sqlx::query_scalar( - "SELECT DISTINCT album_artist - FROM tracks - WHERE is_compilation = 0 - ORDER BY - CASE WHEN upper(substr(album_artist, 1, 1)) BETWEEN 'A' AND 'Z' THEN 0 ELSE 1 END, - album_artist COLLATE NOCASE;", + pub async fn get_artists(&mut self) -> AnyResult> { + let result = sqlx::query_as::<_, Artist>( + "SELECT + album_artist AS label, + min(album_artist_sort) AS sort_as + FROM tracks + WHERE is_compilation = 0 + GROUP BY album_artist + ORDER BY + -- Keep names starting with A-Z before symbols/digits. + CASE + WHEN upper(substr(sort_as, 1, 1)) BETWEEN 'A' AND 'Z' THEN 0 + ELSE 1 + END, + -- Primary ordering uses the normalized sort value. + sort_as COLLATE NOCASE, + -- Tie-break on display name for deterministic output. + album_artist COLLATE NOCASE;", ) .fetch_all(&mut self.connection) .await?; diff --git a/src-tauri/src/libs/mod.rs b/src-tauri/src/libs/mod.rs index fd408fef4..c5d4a202a 100644 --- a/src-tauri/src/libs/mod.rs +++ b/src-tauri/src/libs/mod.rs @@ -16,3 +16,7 @@ pub mod track; #[cfg(test)] #[path = "./tests/database_tests.rs"] mod database_tests; + +#[cfg(test)] +#[path = "./tests/track_test.rs"] +mod track_test; diff --git a/src-tauri/src/libs/tests/database_tests.rs b/src-tauri/src/libs/tests/database_tests.rs index e3ae16d53..807e8469e 100644 --- a/src-tauri/src/libs/tests/database_tests.rs +++ b/src-tauri/src/libs/tests/database_tests.rs @@ -3,7 +3,7 @@ use sqlx::{Connection, SqliteConnection, sqlite}; use std::path::PathBuf; use super::database::DB; -use super::track::Track; +use super::track::{Artist, Track}; /** ---------------------------------------------------------------------------- * Test data @@ -15,8 +15,9 @@ fn sample_track_1() -> Track { path: "/music/artist1/album1/track1.mp3".to_string(), title: "Song One".to_string(), album: "Album One".to_string(), - album_artist: "Artist One".to_string(), - artists: vec!["Artist One".to_string()], + album_artist: "A Perfect Circle".to_string(), + album_artist_sort: "Perfect Circle, A".to_string(), + artists: vec!["A Perfect Circle".to_string()], genres: vec!["Pop".to_string(), "Rock".to_string()], year: Some(2023), duration: 210, @@ -34,8 +35,9 @@ fn sample_track_2() -> Track { path: "/music/artist2/album2/track2.mp3".to_string(), title: "Song Two".to_string(), album: "Album Two".to_string(), - album_artist: "Artist Two".to_string(), - artists: vec!["Artist Two".to_string()], + album_artist: "The Beatles".to_string(), + album_artist_sort: "Beatles, The".to_string(), + artists: vec!["The Beatles".to_string()], genres: vec!["Jazz".to_string()], year: None, duration: 180, @@ -53,8 +55,9 @@ fn sample_track_3() -> Track { path: "/music/artist3/album3/track3.mp3".to_string(), title: "Song Three".to_string(), album: "Album Three".to_string(), - album_artist: "Artist Three".to_string(), - artists: vec!["Artist Three".to_string(), "Artist Four".to_string()], + album_artist: "#1 Artist".to_string(), + album_artist_sort: "#1 Artist".to_string(), + artists: vec!["#1 Artist".to_string()], genres: vec!["Hip-Hop".to_string()], year: Some(2022), duration: 240, @@ -66,6 +69,46 @@ fn sample_track_3() -> Track { } } +fn sample_track_beatles_secondary_sort() -> Track { + Track { + id: "artist-b-2".to_string(), + path: "/music/b2.mp3".to_string(), + title: "Track B2".to_string(), + album: "Album B".to_string(), + album_artist: "The Beatles".to_string(), + album_artist_sort: "Beatles".to_string(), + artists: vec!["The Beatles".to_string()], + genres: vec!["Rock".to_string()], + year: Some(2024), + duration: 180, + track_no: Some(1), + track_of: Some(1), + disk_no: Some(1), + disk_of: Some(1), + is_compilation: false, + } +} + +fn sample_track_compilation() -> Track { + Track { + id: "artist-compilation".to_string(), + path: "/music/compilation.mp3".to_string(), + title: "Track C".to_string(), + album: "Compilation".to_string(), + album_artist: "Various Artists".to_string(), + album_artist_sort: "Various Artists".to_string(), + artists: vec!["Various Artists".to_string()], + genres: vec![], + year: Some(2024), + duration: 180, + track_no: Some(1), + track_of: Some(1), + disk_no: Some(1), + disk_of: Some(1), + is_compilation: true, + } +} + async fn get_test_db() -> DB { let options = SqliteConnectOptions::new() .in_memory(true) @@ -115,25 +158,9 @@ async fn test_tracks_db() { track_to_update.title = "Song Two Point Five".to_string(); db.update_track(track_to_update).await.unwrap(); tracks = db.get_tracks(&vec!["2".to_string()]).await.unwrap(); - assert_eq!( - tracks, - vec![Track { - id: "2".to_string(), - path: "/music/artist2/album2/track2.mp3".to_string(), - title: "Song Two Point Five".to_string(), - album: "Album Two".to_string(), - album_artist: "Artist Two".to_string(), - artists: vec!["Artist Two".to_string()], - genres: vec!["Jazz".to_string()], - year: None, - duration: 180, - track_no: None, - track_of: None, - disk_no: None, - disk_of: None, - is_compilation: false, - }] - ); + let mut expected = sample_track_2(); + expected.title = "Song Two Point Five".to_string(); + assert_eq!(tracks, vec![expected]); // Test deletion db.remove_tracks(&vec!["2".to_string()]).await.unwrap(); @@ -141,6 +168,46 @@ async fn test_tracks_db() { assert_eq!(all_tracks, vec![sample_track_1(), sample_track_3()]); } +/** ---------------------------------------------------------------------------- + * Integration Test - Artists Sorting + * -------------------------------------------------------------------------- */ + +#[tokio::test] +async fn test_get_artists_uses_sort_as_and_excludes_compilations() { + let mut db = get_test_db().await; + + db.insert_tracks(vec![ + sample_track_1(), + sample_track_2(), + // Duplicate display artist with a lexicographically lower sort key. + sample_track_beatles_secondary_sort(), + // Compilation artists must be excluded from get_artists. + sample_track_compilation(), + sample_track_3(), + ]) + .await + .unwrap(); + + let artists: Vec = db.get_artists().await.unwrap(); + + assert_eq!(artists.len(), 3); + assert!( + artists + .iter() + .all(|artist| artist.label != "Various Artists") + ); + + // Order is based on sort_as and groups symbols after A-Z labels. + assert_eq!(artists[0].label, "The Beatles"); + assert_eq!(artists[0].sort_as, "Beatles"); + + assert_eq!(artists[1].label, "A Perfect Circle"); + assert_eq!(artists[1].sort_as, "Perfect Circle, A"); + + assert_eq!(artists[2].label, "#1 Artist"); + assert_eq!(artists[2].sort_as, "#1 Artist"); +} + /** ---------------------------------------------------------------------------- * Integration Test - Playlists * -------------------------------------------------------------------------- */ diff --git a/src-tauri/src/libs/tests/track_test.rs b/src-tauri/src/libs/tests/track_test.rs new file mode 100644 index 000000000..72559caa5 --- /dev/null +++ b/src-tauri/src/libs/tests/track_test.rs @@ -0,0 +1,22 @@ +use crate::libs::track::normalize_album_artist_sort; + +#[test] +fn normalize_album_artist_sort_scenarios() { + // Strip leading "The " (case-insensitive) + assert_eq!(normalize_album_artist_sort("The Beatles"), "Beatles"); + assert_eq!(normalize_album_artist_sort("the Kooks"), "Kooks"); + + // Strip leading non-alphabetic characters + assert_eq!(normalize_album_artist_sort("-M-"), "M-"); + assert_eq!(normalize_album_artist_sort("...Abba"), "Abba"); + + // Apply both transformations in order + assert_eq!(normalize_album_artist_sort("The -M-"), "M-"); + + // Preserve trimmed original if no ASCII letter remains + assert_eq!(normalize_album_artist_sort("---"), "---"); + assert_eq!(normalize_album_artist_sort(" --- "), "---"); + + // Leave regular names unchanged + assert_eq!(normalize_album_artist_sort("Metallica"), "Metallica"); +} diff --git a/src-tauri/src/libs/track.rs b/src-tauri/src/libs/track.rs index ee041b01c..f9003c8e8 100644 --- a/src-tauri/src/libs/track.rs +++ b/src-tauri/src/libs/track.rs @@ -24,11 +24,14 @@ use crate::libs::utils::is_file_valid; pub struct Track { pub id: String, pub path: String, // must be unique, ideally, a PathBuf + // Fields used for display pub title: String, pub album: String, pub album_artist: String, + pub album_artist_sort: String, #[sqlx(json)] pub artists: Vec, // JSON + // Other Metadata #[sqlx(json)] pub genres: Vec, // JSON pub year: Option, @@ -54,6 +57,35 @@ pub struct TrackGroup { pub tracks: Vec, } +#[derive(Debug, Clone, Serialize, Deserialize, FromRow, TS)] +#[ts(export, export_to = "../../src/generated/typings.ts")] +pub struct Artist { + pub label: String, + pub sort_as: String, +} + +pub(crate) fn normalize_album_artist_sort(album_artist: &str) -> String { + let trimmed = album_artist.trim(); + + let without_the = if let Some(prefix) = trimmed.get(..4) { + if prefix.eq_ignore_ascii_case("the ") { + &trimmed[4..] + } else { + trimmed + } + } else { + trimmed + }; + + let stripped = without_the.trim_start_matches(|c: char| !c.is_alphabetic()); + + if stripped.is_empty() { + trimmed.to_string() + } else { + stripped.to_string() + } +} + /** * Generate a Track struct from a Path, or nothing if it is not a valid audio * file @@ -95,34 +127,45 @@ pub fn get_track_from_file(path: &PathBuf) -> AnyResult { .get_string(ItemKey::AlbumArtist) .map(ToString::to_string) .or_else(|| artists.first().cloned()) - .filter(|s| !s.is_empty()) + .filter(|s| !s.trim().is_empty()) .unwrap_or_else(|| "Unknown Artist".to_string()); + let album_artist_sort = tag + .get_string(ItemKey::AlbumArtistSortOrder) + .filter(|s| !s.trim().is_empty()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| normalize_album_artist_sort(&album_artist)); + let id = get_track_id_for_path(path)?; let is_compilation = tag .get_string(ItemKey::FlagCompilation) .map_or(false, |v| v == "1" || v.eq_ignore_ascii_case("true")); + let title = tag + .get_string(ItemKey::TrackTitle) + .filter(|s| !s.trim().is_empty()) + .map(ToString::to_string) + .unwrap_or_else(|| { + path.file_name() + .and_then(|f| f.to_str()) + .unwrap_or("Unknown") + .to_string() + }); + + let album = tag + .get_string(ItemKey::AlbumTitle) + .filter(|s| !s.trim().is_empty()) + .map(ToString::to_string) + .unwrap_or_else(|| "Unknown".to_string()); + Ok(Track { id, path: path.to_string_lossy().into_owned(), - title: tag - .get_string(ItemKey::TrackTitle) - .filter(|s| !s.is_empty()) - .map(ToString::to_string) - .unwrap_or_else(|| { - path.file_name() - .and_then(|f| f.to_str()) - .unwrap_or("Unknown") - .to_string() - }), - album: tag - .get_string(ItemKey::AlbumTitle) - .filter(|s| !s.is_empty()) - .map(ToString::to_string) - .unwrap_or_else(|| "Unknown".to_string()), + title, + album, album_artist, + album_artist_sort, artists, genres: tag .get_strings(ItemKey::Genre) diff --git a/src-tauri/src/migrations/04_add_album_artist_sort_columns.sql b/src-tauri/src/migrations/04_add_album_artist_sort_columns.sql new file mode 100644 index 000000000..fef67b0f6 --- /dev/null +++ b/src-tauri/src/migrations/04_add_album_artist_sort_columns.sql @@ -0,0 +1,3 @@ +ALTER TABLE tracks ADD COLUMN album_artist_sort TEXT NOT NULL DEFAULT ''; + +UPDATE tracks SET album_artist_sort = album_artist; diff --git a/src-tauri/src/plugins/db.rs b/src-tauri/src/plugins/db.rs index 9e874b13c..369d0593f 100644 --- a/src-tauri/src/plugins/db.rs +++ b/src-tauri/src/plugins/db.rs @@ -18,7 +18,7 @@ use crate::libs::database::{DB, SUPPORTED_PLAYLISTS_EXTENSIONS, SUPPORTED_TRACKS use crate::libs::error::{AnyResult, MuseeksError, handle_fatal_error}; use crate::libs::events::IPCEvent; use crate::libs::playlist::Playlist; -use crate::libs::track::{Track, TrackGroup, get_track_from_file, get_track_id_for_path}; +use crate::libs::track::{Artist, Track, TrackGroup, get_track_from_file, get_track_id_for_path}; use crate::libs::utils::{TimeLogger, scan_dirs}; use super::config::get_storage_dir; @@ -306,7 +306,7 @@ async fn remove_tracks(db_state: State<'_, DBState>, ids: Vec) -> AnyRes } #[tauri::command] -async fn get_artists(db_state: State<'_, DBState>) -> AnyResult> { +async fn get_artists(db_state: State<'_, DBState>) -> AnyResult> { db_state.get_lock().await.get_artists().await } diff --git a/src/components/SideNav.tsx b/src/components/SideNav.tsx index 79caef880..cd0fee70d 100644 --- a/src/components/SideNav.tsx +++ b/src/components/SideNav.tsx @@ -18,11 +18,13 @@ export default function SideNav(props: Props) { // Let's group the children by first character const groupedChildren = useMemo(() => { const groups = groupBy(props.children, (child) => { - const stripped = stripAccents(child.props.label[0]).toUpperCase(); - const code = stripped.charCodeAt(0); + const labelForGrouping = child.props.groupingLabel ?? child.props.label; + const firstChar = + stripAccents(labelForGrouping).toUpperCase().trim()[0] ?? ''; + const code = firstChar.charCodeAt(0); // Group under # if the first character is not a letter - return code >= 65 && code <= 90 ? stripped : '#'; + return code >= 65 && code <= 90 ? firstChar : '#'; }); return groups; diff --git a/src/components/SideNavLink.tsx b/src/components/SideNavLink.tsx index ea5c18143..f765fee40 100644 --- a/src/components/SideNavLink.tsx +++ b/src/components/SideNavLink.tsx @@ -24,6 +24,7 @@ interface Props< > { linkOptions: ValidateLinkOptions; label: string; + groupingLabel?: string; id: string; onRename?: (id: string, name: string) => void; contextMenuItems?: Array; diff --git a/src/generated/typings.ts b/src/generated/typings.ts index 6404f4047..f284e4ee3 100644 --- a/src/generated/typings.ts +++ b/src/generated/typings.ts @@ -1,5 +1,7 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +export type Artist = { label: string, sort_as: string, }; + export type Config = { language: string, theme: string, ui_accent_color: string | null, audio_volume: number, audio_playback_rate: number | null, audio_follow_playing_track: boolean, audio_muted: boolean, audio_shuffle: boolean, audio_repeat: Repeat, audio_stream_server: boolean, default_view: DefaultView, library_sort_by: SortBy, library_sort_order: SortOrder, library_folders: Array, library_autorefresh: boolean, sleepblocker: boolean, auto_update_checker: boolean, notifications: boolean, track_view_density: TrackViewDensity, wayland_compat: boolean, menu_bar_visible: boolean, }; export type DefaultView = "Library" | "Artists" | "Playlists"; @@ -29,7 +31,7 @@ export type SortOrder = "Asc" | "Dsc"; * Track * represent a single track, id and path should be unique */ -export type Track = { id: string, path: string, title: string, album: string, album_artist: string, artists: Array, genres: Array, year: number | null, duration: number, track_no: number | null, track_of: number | null, disk_no: number | null, disk_of: number | null, is_compilation: boolean, }; +export type Track = { id: string, path: string, title: string, album: string, album_artist: string, album_artist_sort: string, artists: Array, genres: Array, year: number | null, duration: number, track_no: number | null, track_of: number | null, disk_no: number | null, disk_of: number | null, is_compilation: boolean, }; /** * Represents a group of tracks, grouped by "something", lib artist name, or diff --git a/src/lib/__mocks__/bridge-database.ts b/src/lib/__mocks__/bridge-database.ts index 15e593a7e..ec3020e33 100644 --- a/src/lib/__mocks__/bridge-database.ts +++ b/src/lib/__mocks__/bridge-database.ts @@ -1,4 +1,5 @@ import type { + Artist, Playlist, ScanResult, Track, @@ -15,6 +16,7 @@ const MOCK_TRACKS: Array = [ album: 'Another Album', duration: 300, album_artist: 'Captain_Sleepy', + album_artist_sort: 'Captain_Sleepy', year: 2025, disk_no: 1, disk_of: 1, @@ -31,6 +33,7 @@ const MOCK_TRACKS: Array = [ album: 'Pixabay', duration: 300, album_artist: 'Desicomix07', + album_artist_sort: 'Desicomix07', year: 2025, disk_no: 1, disk_of: 1, @@ -47,6 +50,7 @@ const MOCK_TRACKS: Array = [ album: 'Pixabay', duration: 300, album_artist: 'Jean-Paul-V', + album_artist_sort: 'Jean-Paul-V', year: 2025, disk_no: 1, disk_of: 1, @@ -92,7 +96,7 @@ class DatabaseBridge implements DatabaseBridgeInterface { }; } - async getAllArtists(): Promise> { + async getAllArtists(): Promise> { return []; } diff --git a/src/lib/bridge-database.ts b/src/lib/bridge-database.ts index 63f632253..3866afd68 100644 --- a/src/lib/bridge-database.ts +++ b/src/lib/bridge-database.ts @@ -2,6 +2,7 @@ import { invoke } from '@tauri-apps/api/core'; import { info } from '@tauri-apps/plugin-log'; import type { + Artist, Playlist, ScanResult, Track, @@ -17,7 +18,7 @@ export interface DatabaseBridgeInterface { importPaths: Array, refresh?: boolean, ): Promise; - getAllArtists(): Promise>; + getAllArtists(): Promise>; getArtistTracks(artist: string): Promise>; hasCompilations(): Promise; getCompilationAlbums(): Promise>; @@ -78,8 +79,8 @@ class DatabaseBridge implements DatabaseBridgeInterface { } @LogExecutionTime - async getAllArtists(): Promise> { - return invoke('plugin:database|get_artists'); + async getAllArtists(): Promise> { + return invoke>('plugin:database|get_artists'); } @LogExecutionTime diff --git a/src/lib/utils-library.ts b/src/lib/utils-library.ts index 60243beca..5392d3827 100644 --- a/src/lib/utils-library.ts +++ b/src/lib/utils-library.ts @@ -83,10 +83,10 @@ export const removeRedundantFolders = (paths: Array): Array => { * Sort utilities * -------------------------------------------------------------------------- */ -// For perforances reasons, otherwise _.orderBy will perform weird checks +// For performance reasons, otherwise _.orderBy will perform weird checks // that are far more resource/time impactful const ARTIST = (t: Track): string => - stripAccents(t.artists.toString().toLowerCase()); + stripAccents(t.album_artist_sort.toString().toLowerCase()); const GENRE = (t: Track): string => stripAccents(t.genres.toString().toLowerCase()); const ALBUM = (t: Track): string => stripAccents(t.album.toLowerCase()); diff --git a/src/routes/artists.tsx b/src/routes/artists.tsx index 925a167b5..09b0d8009 100644 --- a/src/routes/artists.tsx +++ b/src/routes/artists.tsx @@ -36,7 +36,7 @@ export const Route = createFileRoute('/artists')({ if (artists.length > 0) { throw redirect({ to: '/artists/$artistID', - params: { artistID: artists[0] }, + params: { artistID: artists[0].label }, }); } @@ -75,12 +75,13 @@ function ViewArtists() { > {artists.map((artist) => ( ))} diff --git a/src/routes/tracks.$trackID.tsx b/src/routes/tracks.$trackID.tsx index 4cd6e3419..33f3bdfb3 100644 --- a/src/routes/tracks.$trackID.tsx +++ b/src/routes/tracks.$trackID.tsx @@ -48,6 +48,7 @@ function ViewTrackDetails() { album: track.album ?? '', artists: track.artists, album_artist: track.album_artist ?? '', + album_artist_sort: track.album_artist_sort ?? '', genres: track.genres, year: track.year, track_no: track.track_no ?? null, @@ -119,6 +120,20 @@ function ViewTrackDetails() { }} /> + + { + setFormData({ + ...formData, + album_artist_sort: e.currentTarget.value, + }); + }} + /> +