UI: Alben sind Kategorien — Reiter Lieder/Kategorie/Kuenstler

Auf Wunsch von Dustin: seine beste Freundin sortiert ihre Sammlung ueber
das Album-Feld. In Melo ist der Album-Titel deshalb ab jetzt die Kategorie
— sie muss nichts neu machen.

1) Album = Kategorie
- kategorienAusTags(): Album-Titel steht VORN in der Kategorienliste (die
  erste Kategorie bestimmt das Coverbild), Genres dahinter.
- Beide Scans (android_scan + scan_service) tragen ihn ein; von Hand
  gepflegte Kategorien (categoriesEdited) bleiben unberuehrt.
- DB-Schema 9: einmalige Nachruestung bestehender Bibliotheken, damit das
  nicht erst beim naechsten vollstaendigen Scan sichtbar wird (der auf
  Android nur laeuft, wenn sich die Dateianzahl aendert).

2) Reiter: 'Songs/Kuenstler/Alben' -> 'Lieder/Kategorie/Kuenstler'
- Neu: library/category_list.dart mit groupByCategory(); Lieder ohne
  Kategorie sammeln sich am Ende unter "Ohne Kategorie".
- Entfernt: library/album_list.dart, groupByAlbum(), albumArtistLabel() —
  mit dem Alben-Reiter tot geworden. Das Album-FELD bleibt erhalten.

3) YouTube-Downloads ohne Original-Album
- Feld "Kategorie (optional)" im YouTube-Bereich, mit Vorschlaegen aus der
  Bibliothek und freier Eingabe.
- ordneDownloadEin() verwirft nach dem Scan das Album-Tag (yt-dlp leitet es
  aus Kanal/Playlist ab — als Kategorie waere das Unsinn) und setzt
  stattdessen die gewaehlte Kategorie. Beides als "von Hand gesetzt"
  markiert, damit der naechste Scan es nicht zurueckholt.
- MeloDb.songByPath() und MeloDb.verwirfAlbum() neu.
- CategoryService.alleNamen: Kategorienamen ohne zusaetzliche Abfrage —
  ohne das flackerte die Vorschlagsliste und im Widget-Test blieb ein
  Aufraeum-Timer von drift haengen.

295 Tests gruen (19 neue, 1 uebersprungen), flutter analyze ohne Befund.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01FpPu4nuKjKKeX1RpdDeX81
This commit is contained in:
Hermes (Server)
2026-08-21 09:36:46 +02:00
co-authored by Claude Opus 5
parent cd79213bb3
commit c7a7a7c458
17 changed files with 726 additions and 157 deletions
+56 -1
View File
@@ -1,6 +1,7 @@
import 'dart:io';
import 'package:drift/drift.dart';
import 'package:flutter/foundation.dart' show visibleForTesting;
import 'package:drift/native.dart';
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
@@ -129,7 +130,7 @@ class MeloDb extends _$MeloDb {
MeloDb([QueryExecutor? executor]) : super(executor ?? _open());
@override
int get schemaVersion => 8;
int get schemaVersion => 9;
@override
MigrationStrategy get migration => MigrationStrategy(
@@ -160,6 +161,14 @@ class MeloDb extends _$MeloDb {
if (from < 8) {
await m.addColumn(songs, songs.cloudId);
}
if (from < 9) {
// Album-Titel gelten ab jetzt als Kategorie (siehe
// kategorienAusTags). Ohne diese Nachrüstung bekämen bestehende
// Bibliotheken das erst beim nächsten vollständigen Scan zu
// sehen — der auf Android nur läuft, wenn sich die Anzahl der
// Dateien ändert.
await _ergaenzeAlbumKategorien();
}
},
);
@@ -448,6 +457,52 @@ class MeloDb extends _$MeloDb {
));
}
/// Der Song mit genau diesem Dateipfad, oder `null`.
Future<Song?> songByPath(String path) =>
(select(songs)..where((s) => s.path.equals(path))).getSingleOrNull();
/// Verwirft das Album-Tag eines Titels und markiert ihn als von Hand
/// bearbeitet, damit der nächste Scan es nicht wieder hereinholt.
/// Für YouTube-Downloads: deren Album-Tag ist keine sinnvolle Kategorie.
Future<void> verwirfAlbum(String songId) async {
await (update(songs)..where((s) => s.id.equals(songId))).write(
const SongsCompanion(
album: Value(null),
metadataEdited: Value(true),
),
);
}
/// Nur für Tests: führt die Nachrüstung aus Schema 9 auf einer bereits
/// angelegten Datenbank aus.
@visibleForTesting
Future<void> ergaenzeAlbumKategorienFuerTest() => _ergaenzeAlbumKategorien();
/// Trägt für jeden Titel seinen Album-Namen als **erste** Kategorie nach.
/// Von Hand gepflegte Kategorien (`categoriesEdited`) bleiben unberührt.
Future<void> _ergaenzeAlbumKategorien() async {
const betroffen = "SELECT id, TRIM(album) AS album_name FROM songs "
"WHERE album IS NOT NULL AND TRIM(album) <> '' "
"AND categories_edited = 0";
// 1) War der Album-Name schon Kategorie, kommt er gleich wieder vorn
// dazu — den alten Eintrag deshalb entfernen.
await customStatement(
'DELETE FROM song_categories WHERE EXISTS ('
'SELECT 1 FROM ($betroffen) b '
'WHERE b.id = song_categories.song_id AND b.album_name = song_categories.name)',
);
// 2) Platz an Position 0 schaffen.
await customStatement(
'UPDATE song_categories SET position = position + 1 '
'WHERE song_id IN (SELECT id FROM ($betroffen))',
);
// 3) Album-Name als erste Kategorie setzen.
await customStatement(
'INSERT INTO song_categories (song_id, name, position) '
'SELECT id, album_name, 0 FROM ($betroffen)',
);
}
// === Cloud-Sync ===
/// Verknüpft einen Titel des Geräts mit seinem Gegenstück in der Cloud.
Future<void> setCloudId(String songId, String cloudId) async {