mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-07-05 04:08:02 +02:00
feat: add stable cover cache keys, Qobuz album-search fallback, metadata filters and extended sort options
- Introduce coverCacheKey parameter through Go backend and Kotlin bridge for stable SAF cover caching - Add MetadataFromFilename flag to skip filename-only metadata and retry via temp-file copy - Add Qobuz album-search fallback between API search and store scraping - Extract buildReEnrichFFmpegMetadata to skip empty metadata fields - Add metadata completeness filter (complete, missing year/genre/album artist) - Add sort modes: artist, album, release date, genre (asc/desc) - Prune stale library cover cache files after full scan - Skip empty values and zero track/disc numbers in FFmpeg metadata - Add new l10n keys for metadata filter and sort options
This commit is contained in:
@@ -3478,6 +3478,42 @@ abstract class AppLocalizations {
|
||||
/// **'Format'**
|
||||
String get libraryFilterFormat;
|
||||
|
||||
/// Filter section - metadata completeness
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Metadata'**
|
||||
String get libraryFilterMetadata;
|
||||
|
||||
/// Filter option - items with complete metadata
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Complete metadata'**
|
||||
String get libraryFilterMetadataComplete;
|
||||
|
||||
/// Filter option - items missing any tracked metadata field
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Missing any metadata'**
|
||||
String get libraryFilterMetadataMissingAny;
|
||||
|
||||
/// Filter option - items missing release year/date
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Missing year'**
|
||||
String get libraryFilterMetadataMissingYear;
|
||||
|
||||
/// Filter option - items missing genre
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Missing genre'**
|
||||
String get libraryFilterMetadataMissingGenre;
|
||||
|
||||
/// Filter option - items missing album artist
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Missing album artist'**
|
||||
String get libraryFilterMetadataMissingAlbumArtist;
|
||||
|
||||
/// Filter section - sort order
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
@@ -3496,6 +3532,30 @@ abstract class AppLocalizations {
|
||||
/// **'Oldest'**
|
||||
String get libraryFilterSortOldest;
|
||||
|
||||
/// Sort option - album ascending
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Album (A-Z)'**
|
||||
String get libraryFilterSortAlbumAsc;
|
||||
|
||||
/// Sort option - album descending
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Album (Z-A)'**
|
||||
String get libraryFilterSortAlbumDesc;
|
||||
|
||||
/// Sort option - genre ascending
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Genre (A-Z)'**
|
||||
String get libraryFilterSortGenreAsc;
|
||||
|
||||
/// Sort option - genre descending
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
/// **'Genre (Z-A)'**
|
||||
String get libraryFilterSortGenreDesc;
|
||||
|
||||
/// Relative time - less than a minute ago
|
||||
///
|
||||
/// In en, this message translates to:
|
||||
|
||||
@@ -1930,6 +1930,24 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sortieren';
|
||||
|
||||
@@ -1939,6 +1957,18 @@ class AppLocalizationsDe extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Älteste';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Gerade eben';
|
||||
|
||||
|
||||
@@ -1902,6 +1902,24 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1911,6 +1929,18 @@ class AppLocalizationsEn extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1902,6 +1902,24 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1911,6 +1929,18 @@ class AppLocalizationsEs extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1904,6 +1904,24 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1913,6 +1931,18 @@ class AppLocalizationsFr extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1902,6 +1902,24 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1911,6 +1929,18 @@ class AppLocalizationsHi extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1912,6 +1912,24 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1921,6 +1939,18 @@ class AppLocalizationsId extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1889,6 +1889,24 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => '形式';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1898,6 +1916,18 @@ class AppLocalizationsJa extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1882,6 +1882,24 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1891,6 +1909,18 @@ class AppLocalizationsKo extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1902,6 +1902,24 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1911,6 +1929,18 @@ class AppLocalizationsNl extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1902,6 +1902,24 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1911,6 +1929,18 @@ class AppLocalizationsPt extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1948,6 +1948,24 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Формат';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Сортировка';
|
||||
|
||||
@@ -1957,6 +1975,18 @@ class AppLocalizationsRu extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Старые';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Только что';
|
||||
|
||||
|
||||
@@ -1908,6 +1908,24 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1917,6 +1935,18 @@ class AppLocalizationsTr extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -1902,6 +1902,24 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterFormat => 'Format';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadata => 'Metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataComplete => 'Complete metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAny => 'Missing any metadata';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingYear => 'Missing year';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingGenre => 'Missing genre';
|
||||
|
||||
@override
|
||||
String get libraryFilterMetadataMissingAlbumArtist => 'Missing album artist';
|
||||
|
||||
@override
|
||||
String get libraryFilterSort => 'Sort';
|
||||
|
||||
@@ -1911,6 +1929,18 @@ class AppLocalizationsZh extends AppLocalizations {
|
||||
@override
|
||||
String get libraryFilterSortOldest => 'Oldest';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumAsc => 'Album (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortAlbumDesc => 'Album (Z-A)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreAsc => 'Genre (A-Z)';
|
||||
|
||||
@override
|
||||
String get libraryFilterSortGenreDesc => 'Genre (Z-A)';
|
||||
|
||||
@override
|
||||
String get timeJustNow => 'Just now';
|
||||
|
||||
|
||||
@@ -2513,6 +2513,30 @@
|
||||
"@libraryFilterFormat": {
|
||||
"description": "Filter section - file format"
|
||||
},
|
||||
"libraryFilterMetadata": "Metadata",
|
||||
"@libraryFilterMetadata": {
|
||||
"description": "Filter section - metadata completeness"
|
||||
},
|
||||
"libraryFilterMetadataComplete": "Complete metadata",
|
||||
"@libraryFilterMetadataComplete": {
|
||||
"description": "Filter option - items with complete metadata"
|
||||
},
|
||||
"libraryFilterMetadataMissingAny": "Missing any metadata",
|
||||
"@libraryFilterMetadataMissingAny": {
|
||||
"description": "Filter option - items missing any tracked metadata field"
|
||||
},
|
||||
"libraryFilterMetadataMissingYear": "Missing year",
|
||||
"@libraryFilterMetadataMissingYear": {
|
||||
"description": "Filter option - items missing release year/date"
|
||||
},
|
||||
"libraryFilterMetadataMissingGenre": "Missing genre",
|
||||
"@libraryFilterMetadataMissingGenre": {
|
||||
"description": "Filter option - items missing genre"
|
||||
},
|
||||
"libraryFilterMetadataMissingAlbumArtist": "Missing album artist",
|
||||
"@libraryFilterMetadataMissingAlbumArtist": {
|
||||
"description": "Filter option - items missing album artist"
|
||||
},
|
||||
"libraryFilterSort": "Sort",
|
||||
"@libraryFilterSort": {
|
||||
"description": "Filter section - sort order"
|
||||
@@ -2525,6 +2549,22 @@
|
||||
"@libraryFilterSortOldest": {
|
||||
"description": "Sort option - oldest first"
|
||||
},
|
||||
"libraryFilterSortAlbumAsc": "Album (A-Z)",
|
||||
"@libraryFilterSortAlbumAsc": {
|
||||
"description": "Sort option - album ascending"
|
||||
},
|
||||
"libraryFilterSortAlbumDesc": "Album (Z-A)",
|
||||
"@libraryFilterSortAlbumDesc": {
|
||||
"description": "Sort option - album descending"
|
||||
},
|
||||
"libraryFilterSortGenreAsc": "Genre (A-Z)",
|
||||
"@libraryFilterSortGenreAsc": {
|
||||
"description": "Sort option - genre ascending"
|
||||
},
|
||||
"libraryFilterSortGenreDesc": "Genre (Z-A)",
|
||||
"@libraryFilterSortGenreDesc": {
|
||||
"description": "Sort option - genre descending"
|
||||
},
|
||||
"timeJustNow": "Just now",
|
||||
"@timeJustNow": {
|
||||
"description": "Relative time - less than a minute ago"
|
||||
|
||||
@@ -339,6 +339,7 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
scanWasCancelled: false,
|
||||
excludedDownloadedCount: skippedDownloads,
|
||||
);
|
||||
await _pruneLibraryCoverCache(persistedItems);
|
||||
|
||||
_log.i(
|
||||
'Full scan complete: ${persistedItems.length} tracks found, '
|
||||
@@ -815,6 +816,46 @@ class LocalLibraryNotifier extends Notifier<LocalLibraryState> {
|
||||
_log.i('Library cleared');
|
||||
}
|
||||
|
||||
Future<void> _pruneLibraryCoverCache(Iterable<LocalLibraryItem> items) async {
|
||||
try {
|
||||
final appSupportDir = await getApplicationSupportDirectory();
|
||||
final libraryCoverDir = Directory('${appSupportDir.path}/library_covers');
|
||||
if (!await libraryCoverDir.exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
final referencedCoverPaths = items
|
||||
.map((item) => item.coverPath)
|
||||
.whereType<String>()
|
||||
.where((path) => path.isNotEmpty)
|
||||
.toSet();
|
||||
|
||||
var deletedCount = 0;
|
||||
await for (final entity in libraryCoverDir.list(
|
||||
recursive: true,
|
||||
followLinks: false,
|
||||
)) {
|
||||
if (entity is! File || referencedCoverPaths.contains(entity.path)) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
await entity.delete();
|
||||
deletedCount++;
|
||||
} catch (e) {
|
||||
_log.w(
|
||||
'Failed deleting stale library cover cache ${entity.path}: $e',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (deletedCount > 0) {
|
||||
_log.i('Pruned $deletedCount stale library cover cache files');
|
||||
}
|
||||
} catch (e) {
|
||||
_log.w('Failed pruning library cover cache: $e');
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> removeItem(String id) async {
|
||||
await _db.delete(id);
|
||||
state = state.copyWith(
|
||||
|
||||
+593
-4
@@ -124,6 +124,12 @@ class UnifiedLibraryItem {
|
||||
coverUrl != null ||
|
||||
(localCoverPath != null && localCoverPath!.isNotEmpty);
|
||||
|
||||
String? get albumArtist => historyItem?.albumArtist ?? localItem?.albumArtist;
|
||||
|
||||
String? get releaseDate => historyItem?.releaseDate ?? localItem?.releaseDate;
|
||||
|
||||
String? get genre => historyItem?.genre ?? localItem?.genre;
|
||||
|
||||
String get searchKey =>
|
||||
'${trackName.toLowerCase()}|${artistName.toLowerCase()}|${albumName.toLowerCase()}';
|
||||
String get albumKey =>
|
||||
@@ -319,6 +325,7 @@ class _QueueGroupedAlbumFilterRequest {
|
||||
final String? filterSource;
|
||||
final String? filterQuality;
|
||||
final String? filterFormat;
|
||||
final String? filterMetadata;
|
||||
final String sortMode;
|
||||
|
||||
const _QueueGroupedAlbumFilterRequest({
|
||||
@@ -326,6 +333,7 @@ class _QueueGroupedAlbumFilterRequest {
|
||||
required this.filterSource,
|
||||
required this.filterQuality,
|
||||
required this.filterFormat,
|
||||
required this.filterMetadata,
|
||||
required this.sortMode,
|
||||
});
|
||||
|
||||
@@ -337,6 +345,7 @@ class _QueueGroupedAlbumFilterRequest {
|
||||
filterSource == other.filterSource &&
|
||||
filterQuality == other.filterQuality &&
|
||||
filterFormat == other.filterFormat &&
|
||||
filterMetadata == other.filterMetadata &&
|
||||
sortMode == other.sortMode;
|
||||
|
||||
@override
|
||||
@@ -345,6 +354,7 @@ class _QueueGroupedAlbumFilterRequest {
|
||||
filterSource,
|
||||
filterQuality,
|
||||
filterFormat,
|
||||
filterMetadata,
|
||||
sortMode,
|
||||
);
|
||||
}
|
||||
@@ -358,6 +368,161 @@ String _queueFileExtLower(String filePath) {
|
||||
return filePath.substring(dotIndex + 1).toLowerCase();
|
||||
}
|
||||
|
||||
bool _queueHasMetadataValue(String? value) {
|
||||
return value != null && value.trim().isNotEmpty;
|
||||
}
|
||||
|
||||
String _queueNormalizedMetadataValue(String? value) {
|
||||
return value?.trim().toLowerCase() ?? '';
|
||||
}
|
||||
|
||||
DateTime? _queueParseReleaseDate(String? value) {
|
||||
final trimmed = value?.trim() ?? '';
|
||||
if (trimmed.isEmpty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final parsed = DateTime.tryParse(trimmed);
|
||||
if (parsed != null) {
|
||||
return parsed;
|
||||
}
|
||||
|
||||
final yearMatch = RegExp(r'(\d{4})').firstMatch(trimmed);
|
||||
if (yearMatch == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final year = int.tryParse(yearMatch.group(1)!);
|
||||
if (year == null || year <= 0) {
|
||||
return null;
|
||||
}
|
||||
return DateTime(year);
|
||||
}
|
||||
|
||||
bool _queueMatchesMetadataFilter({
|
||||
required String? filterMetadata,
|
||||
required String? albumArtist,
|
||||
required String? releaseDate,
|
||||
required String? genre,
|
||||
}) {
|
||||
if (filterMetadata == null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
final hasAlbumArtist = _queueHasMetadataValue(albumArtist);
|
||||
final hasReleaseDate = _queueParseReleaseDate(releaseDate) != null;
|
||||
final hasGenre = _queueHasMetadataValue(genre);
|
||||
final isComplete = hasAlbumArtist && hasReleaseDate && hasGenre;
|
||||
|
||||
switch (filterMetadata) {
|
||||
case 'complete':
|
||||
return isComplete;
|
||||
case 'missing-any':
|
||||
return !isComplete;
|
||||
case 'missing-year':
|
||||
return !hasReleaseDate;
|
||||
case 'missing-genre':
|
||||
return !hasGenre;
|
||||
case 'missing-album-artist':
|
||||
return !hasAlbumArtist;
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool _queueUnifiedItemMatchesMetadataFilter(
|
||||
UnifiedLibraryItem item,
|
||||
String? filterMetadata,
|
||||
) {
|
||||
return _queueMatchesMetadataFilter(
|
||||
filterMetadata: filterMetadata,
|
||||
albumArtist: item.albumArtist,
|
||||
releaseDate: item.releaseDate,
|
||||
genre: item.genre,
|
||||
);
|
||||
}
|
||||
|
||||
int _queueCompareOptionalText(
|
||||
String? left,
|
||||
String? right, {
|
||||
bool descending = false,
|
||||
}) {
|
||||
final normalizedLeft = _queueNormalizedMetadataValue(left);
|
||||
final normalizedRight = _queueNormalizedMetadataValue(right);
|
||||
final leftEmpty = normalizedLeft.isEmpty;
|
||||
final rightEmpty = normalizedRight.isEmpty;
|
||||
|
||||
if (leftEmpty && rightEmpty) {
|
||||
return 0;
|
||||
}
|
||||
if (leftEmpty) {
|
||||
return 1;
|
||||
}
|
||||
if (rightEmpty) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final comparison = normalizedLeft.compareTo(normalizedRight);
|
||||
return descending ? -comparison : comparison;
|
||||
}
|
||||
|
||||
int _queueCompareOptionalDate(
|
||||
DateTime? left,
|
||||
DateTime? right, {
|
||||
bool descending = false,
|
||||
}) {
|
||||
if (left == null && right == null) {
|
||||
return 0;
|
||||
}
|
||||
if (left == null) {
|
||||
return 1;
|
||||
}
|
||||
if (right == null) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
final comparison = left.compareTo(right);
|
||||
return descending ? -comparison : comparison;
|
||||
}
|
||||
|
||||
DateTime? _queueGroupedAlbumReleaseDate(_GroupedAlbum album) {
|
||||
for (final track in album.tracks) {
|
||||
final releaseDate = _queueParseReleaseDate(track.releaseDate);
|
||||
if (releaseDate != null) {
|
||||
return releaseDate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
DateTime? _queueGroupedLocalAlbumReleaseDate(_GroupedLocalAlbum album) {
|
||||
for (final track in album.tracks) {
|
||||
final releaseDate = _queueParseReleaseDate(track.releaseDate);
|
||||
if (releaseDate != null) {
|
||||
return releaseDate;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _queueGroupedAlbumGenre(_GroupedAlbum album) {
|
||||
for (final track in album.tracks) {
|
||||
if (_queueHasMetadataValue(track.genre)) {
|
||||
return track.genre;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _queueGroupedLocalAlbumGenre(_GroupedLocalAlbum album) {
|
||||
for (final track in album.tracks) {
|
||||
if (_queueHasMetadataValue(track.genre)) {
|
||||
return track.genre;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String? _queueLocalQualityLabel(LocalLibraryItem item) {
|
||||
if (item.bitrate != null && item.bitrate! > 0) {
|
||||
return '${item.bitrate}kbps';
|
||||
@@ -519,6 +684,7 @@ List<_GroupedAlbum> _queueFilterGroupedAlbums(
|
||||
if (request.filterSource == null &&
|
||||
request.filterQuality == null &&
|
||||
request.filterFormat == null &&
|
||||
request.filterMetadata == null &&
|
||||
request.searchQuery.isEmpty &&
|
||||
request.sortMode == 'latest') {
|
||||
return albums;
|
||||
@@ -531,7 +697,9 @@ List<_GroupedAlbum> _queueFilterGroupedAlbums(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (request.filterQuality != null || request.filterFormat != null) {
|
||||
if (request.filterQuality != null ||
|
||||
request.filterFormat != null ||
|
||||
request.filterMetadata != null) {
|
||||
var hasMatchingTrack = false;
|
||||
for (final track in album.tracks) {
|
||||
if (!_queuePassesQualityFilter(request.filterQuality, track.quality)) {
|
||||
@@ -540,6 +708,14 @@ List<_GroupedAlbum> _queueFilterGroupedAlbums(
|
||||
if (!_queuePassesFormatFilter(request.filterFormat, track.filePath)) {
|
||||
continue;
|
||||
}
|
||||
if (!_queueMatchesMetadataFilter(
|
||||
filterMetadata: request.filterMetadata,
|
||||
albumArtist: track.albumArtist,
|
||||
releaseDate: track.releaseDate,
|
||||
genre: track.genre,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
hasMatchingTrack = true;
|
||||
break;
|
||||
}
|
||||
@@ -552,6 +728,29 @@ List<_GroupedAlbum> _queueFilterGroupedAlbums(
|
||||
switch (request.sortMode) {
|
||||
case 'oldest':
|
||||
result.sort((a, b) => a.latestDownload.compareTo(b.latestDownload));
|
||||
case 'artist-asc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'artist-desc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'a-z':
|
||||
result.sort(
|
||||
(a, b) =>
|
||||
@@ -562,6 +761,64 @@ List<_GroupedAlbum> _queueFilterGroupedAlbums(
|
||||
(a, b) =>
|
||||
b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()),
|
||||
);
|
||||
case 'album-asc':
|
||||
result.sort(
|
||||
(a, b) => _queueCompareOptionalText(a.albumName, b.albumName),
|
||||
);
|
||||
case 'album-desc':
|
||||
result.sort(
|
||||
(a, b) => _queueCompareOptionalText(
|
||||
a.albumName,
|
||||
b.albumName,
|
||||
descending: true,
|
||||
),
|
||||
);
|
||||
case 'release-oldest':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueGroupedAlbumReleaseDate(a),
|
||||
_queueGroupedAlbumReleaseDate(b),
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'release-newest':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueGroupedAlbumReleaseDate(a),
|
||||
_queueGroupedAlbumReleaseDate(b),
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'genre-asc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
_queueGroupedAlbumGenre(a),
|
||||
_queueGroupedAlbumGenre(b),
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'genre-desc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
_queueGroupedAlbumGenre(a),
|
||||
_queueGroupedAlbumGenre(b),
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -576,6 +833,7 @@ List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums(
|
||||
if (request.filterSource == null &&
|
||||
request.filterQuality == null &&
|
||||
request.filterFormat == null &&
|
||||
request.filterMetadata == null &&
|
||||
request.searchQuery.isEmpty &&
|
||||
request.sortMode == 'latest') {
|
||||
return albums;
|
||||
@@ -588,7 +846,9 @@ List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (request.filterQuality != null || request.filterFormat != null) {
|
||||
if (request.filterQuality != null ||
|
||||
request.filterFormat != null ||
|
||||
request.filterMetadata != null) {
|
||||
var hasMatchingTrack = false;
|
||||
for (final track in album.tracks) {
|
||||
if (!_queuePassesQualityFilter(
|
||||
@@ -600,6 +860,14 @@ List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums(
|
||||
if (!_queuePassesFormatFilter(request.filterFormat, track.filePath)) {
|
||||
continue;
|
||||
}
|
||||
if (!_queueMatchesMetadataFilter(
|
||||
filterMetadata: request.filterMetadata,
|
||||
albumArtist: track.albumArtist,
|
||||
releaseDate: track.releaseDate,
|
||||
genre: track.genre,
|
||||
)) {
|
||||
continue;
|
||||
}
|
||||
hasMatchingTrack = true;
|
||||
break;
|
||||
}
|
||||
@@ -612,6 +880,29 @@ List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums(
|
||||
switch (request.sortMode) {
|
||||
case 'oldest':
|
||||
result.sort((a, b) => a.latestScanned.compareTo(b.latestScanned));
|
||||
case 'artist-asc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'artist-desc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'a-z':
|
||||
result.sort(
|
||||
(a, b) =>
|
||||
@@ -622,6 +913,64 @@ List<_GroupedLocalAlbum> _queueFilterGroupedLocalAlbums(
|
||||
(a, b) =>
|
||||
b.albumName.toLowerCase().compareTo(a.albumName.toLowerCase()),
|
||||
);
|
||||
case 'album-asc':
|
||||
result.sort(
|
||||
(a, b) => _queueCompareOptionalText(a.albumName, b.albumName),
|
||||
);
|
||||
case 'album-desc':
|
||||
result.sort(
|
||||
(a, b) => _queueCompareOptionalText(
|
||||
a.albumName,
|
||||
b.albumName,
|
||||
descending: true,
|
||||
),
|
||||
);
|
||||
case 'release-oldest':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueGroupedLocalAlbumReleaseDate(a),
|
||||
_queueGroupedLocalAlbumReleaseDate(b),
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'release-newest':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueGroupedLocalAlbumReleaseDate(a),
|
||||
_queueGroupedLocalAlbumReleaseDate(b),
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'genre-asc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
_queueGroupedLocalAlbumGenre(a),
|
||||
_queueGroupedLocalAlbumGenre(b),
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
case 'genre-desc':
|
||||
result.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
_queueGroupedLocalAlbumGenre(a),
|
||||
_queueGroupedLocalAlbumGenre(b),
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.albumName, b.albumName);
|
||||
});
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -781,10 +1130,12 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
String? _filterCacheSource;
|
||||
String? _filterCacheQuality;
|
||||
String? _filterCacheFormat;
|
||||
String? _filterCacheMetadata;
|
||||
String _filterCacheSortMode = 'latest';
|
||||
String? _filterSource; // null = all, 'downloaded', 'local'
|
||||
String? _filterQuality; // null = all, 'hires', 'cd', 'lossy'
|
||||
String? _filterFormat; // null = all, 'flac', 'mp3', 'm4a', 'opus', 'ogg'
|
||||
String? _filterMetadata; // null = all, 'complete', 'missing-*'
|
||||
String _sortMode = 'latest'; // 'latest', 'oldest', 'a-z', 'z-a'
|
||||
|
||||
double _effectiveTextScale() {
|
||||
@@ -871,6 +1222,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_filterCacheSource == _filterSource &&
|
||||
_filterCacheQuality == _filterQuality &&
|
||||
_filterCacheFormat == _filterFormat &&
|
||||
_filterCacheMetadata == _filterMetadata &&
|
||||
_filterCacheSortMode == _sortMode;
|
||||
|
||||
if (isCacheValid) {
|
||||
@@ -886,6 +1238,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_filterCacheSource = _filterSource;
|
||||
_filterCacheQuality = _filterQuality;
|
||||
_filterCacheFormat = _filterFormat;
|
||||
_filterCacheMetadata = _filterMetadata;
|
||||
_filterCacheSortMode = _sortMode;
|
||||
}
|
||||
|
||||
@@ -1868,6 +2221,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
if (_filterSource != null) count++;
|
||||
if (_filterQuality != null) count++;
|
||||
if (_filterFormat != null) count++;
|
||||
if (_filterMetadata != null) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
@@ -1876,6 +2230,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_filterSource = null;
|
||||
_filterQuality = null;
|
||||
_filterFormat = null;
|
||||
_filterMetadata = null;
|
||||
_sortMode = 'latest';
|
||||
_unifiedItemsCache.clear();
|
||||
_invalidateFilterContentCache();
|
||||
@@ -1931,6 +2286,13 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
if (ext != _filterFormat) return false;
|
||||
}
|
||||
|
||||
if (!_queueUnifiedItemMatchesMetadataFilter(
|
||||
item,
|
||||
_filterMetadata,
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
})
|
||||
.toList(growable: false);
|
||||
@@ -1957,6 +2319,95 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
(a, b) =>
|
||||
b.trackName.toLowerCase().compareTo(a.trackName.toLowerCase()),
|
||||
);
|
||||
case 'artist-asc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'artist-desc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.artistName,
|
||||
b.artistName,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'album-asc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.albumName,
|
||||
b.albumName,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'album-desc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.albumName,
|
||||
b.albumName,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'release-oldest':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueParseReleaseDate(a.releaseDate),
|
||||
_queueParseReleaseDate(b.releaseDate),
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'release-newest':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalDate(
|
||||
_queueParseReleaseDate(a.releaseDate),
|
||||
_queueParseReleaseDate(b.releaseDate),
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'genre-asc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(a.genre, b.genre);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
case 'genre-desc':
|
||||
sorted.sort((a, b) {
|
||||
final comparison = _queueCompareOptionalText(
|
||||
a.genre,
|
||||
b.genre,
|
||||
descending: true,
|
||||
);
|
||||
if (comparison != 0) {
|
||||
return comparison;
|
||||
}
|
||||
return _queueCompareOptionalText(a.trackName, b.trackName);
|
||||
});
|
||||
}
|
||||
return sorted;
|
||||
}
|
||||
@@ -1982,6 +2433,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
String? tempSource = _filterSource;
|
||||
String? tempQuality = _filterQuality;
|
||||
String? tempFormat = _filterFormat;
|
||||
String? tempMetadata = _filterMetadata;
|
||||
String tempSortMode = _sortMode;
|
||||
|
||||
showModalBottomSheet<void>(
|
||||
@@ -2034,6 +2486,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
tempSource = null;
|
||||
tempQuality = null;
|
||||
tempFormat = null;
|
||||
tempMetadata = null;
|
||||
tempSortMode = 'latest';
|
||||
});
|
||||
},
|
||||
@@ -2147,6 +2600,76 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
context.l10n.libraryFilterMetadata,
|
||||
style: Theme.of(context).textTheme.titleSmall
|
||||
?.copyWith(fontWeight: FontWeight.w600),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
FilterChip(
|
||||
label: Text(context.l10n.libraryFilterAll),
|
||||
selected: tempMetadata == null,
|
||||
onSelected: (_) =>
|
||||
setSheetState(() => tempMetadata = null),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterMetadataComplete,
|
||||
),
|
||||
selected: tempMetadata == 'complete',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempMetadata = 'complete',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterMetadataMissingAny,
|
||||
),
|
||||
selected: tempMetadata == 'missing-any',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempMetadata = 'missing-any',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterMetadataMissingYear,
|
||||
),
|
||||
selected: tempMetadata == 'missing-year',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempMetadata = 'missing-year',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context
|
||||
.l10n
|
||||
.libraryFilterMetadataMissingGenre,
|
||||
),
|
||||
selected: tempMetadata == 'missing-genre',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempMetadata = 'missing-genre',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context
|
||||
.l10n
|
||||
.libraryFilterMetadataMissingAlbumArtist,
|
||||
),
|
||||
selected:
|
||||
tempMetadata == 'missing-album-artist',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempMetadata = 'missing-album-artist',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
Text(
|
||||
context.l10n.libraryFilterSort,
|
||||
style: Theme.of(context).textTheme.titleSmall
|
||||
@@ -2175,17 +2698,81 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.l10n.sortAlphaAsc),
|
||||
label: Text(context.l10n.searchSortTitleAZ),
|
||||
selected: tempSortMode == 'a-z',
|
||||
onSelected: (_) =>
|
||||
setSheetState(() => tempSortMode = 'a-z'),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.l10n.sortAlphaDesc),
|
||||
label: Text(context.l10n.searchSortTitleZA),
|
||||
selected: tempSortMode == 'z-a',
|
||||
onSelected: (_) =>
|
||||
setSheetState(() => tempSortMode = 'z-a'),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.l10n.searchSortArtistAZ),
|
||||
selected: tempSortMode == 'artist-asc',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'artist-asc',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.l10n.searchSortArtistZA),
|
||||
selected: tempSortMode == 'artist-desc',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'artist-desc',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterSortAlbumAsc,
|
||||
),
|
||||
selected: tempSortMode == 'album-asc',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'album-asc',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterSortAlbumDesc,
|
||||
),
|
||||
selected: tempSortMode == 'album-desc',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'album-desc',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.l10n.searchSortDateNewest),
|
||||
selected: tempSortMode == 'release-newest',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'release-newest',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(context.l10n.searchSortDateOldest),
|
||||
selected: tempSortMode == 'release-oldest',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'release-oldest',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterSortGenreAsc,
|
||||
),
|
||||
selected: tempSortMode == 'genre-asc',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'genre-asc',
|
||||
),
|
||||
),
|
||||
FilterChip(
|
||||
label: Text(
|
||||
context.l10n.libraryFilterSortGenreDesc,
|
||||
),
|
||||
selected: tempSortMode == 'genre-desc',
|
||||
onSelected: (_) => setSheetState(
|
||||
() => tempSortMode = 'genre-desc',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
@@ -2198,6 +2785,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
_filterSource = tempSource;
|
||||
_filterQuality = tempQuality;
|
||||
_filterFormat = tempFormat;
|
||||
_filterMetadata = tempMetadata;
|
||||
_sortMode = tempSortMode;
|
||||
_unifiedItemsCache.clear();
|
||||
_invalidateFilterContentCache();
|
||||
@@ -2738,6 +3326,7 @@ class _QueueTabState extends ConsumerState<QueueTab> {
|
||||
filterSource: _filterSource,
|
||||
filterQuality: _filterQuality,
|
||||
filterFormat: _filterFormat,
|
||||
filterMetadata: _filterMetadata,
|
||||
sortMode: _sortMode,
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1691,6 +1691,9 @@ class FFmpegService {
|
||||
final key = entry.key.toUpperCase();
|
||||
final normalizedKey = key.replaceAll(RegExp(r'[^A-Z0-9]'), '');
|
||||
final value = entry.value;
|
||||
if (value.trim().isEmpty) {
|
||||
continue;
|
||||
}
|
||||
|
||||
switch (normalizedKey) {
|
||||
case 'TITLE':
|
||||
@@ -1708,12 +1711,16 @@ class FFmpegService {
|
||||
case 'TRACKNUMBER':
|
||||
case 'TRACK':
|
||||
case 'TRCK':
|
||||
id3Map['track'] = value;
|
||||
if (value != '0') {
|
||||
id3Map['track'] = value;
|
||||
}
|
||||
break;
|
||||
case 'DISCNUMBER':
|
||||
case 'DISC':
|
||||
case 'TPOS':
|
||||
id3Map['disc'] = value;
|
||||
if (value != '0') {
|
||||
id3Map['disc'] = value;
|
||||
}
|
||||
break;
|
||||
case 'DATE':
|
||||
case 'YEAR':
|
||||
|
||||
Reference in New Issue
Block a user