import 'package:sqflite/sqflite.dart'; import 'package:path/path.dart' as p; import '../models/song.dart'; import '../models/tag.dart'; class DbHelper { static final DbHelper _instanz = DbHelper._(); factory DbHelper() => _instanz; DbHelper._(); Database? _db; Future get db async { if (_db != null) return _db!; _db = await _init(); return _db!; } Future _init() async { final pfad = await getDatabasesPath(); return openDatabase( p.join(pfad, 'melo.db'), version: 1, onCreate: (db, version) async { await db.execute(''' CREATE TABLE songs ( id INTEGER PRIMARY KEY AUTOINCREMENT, titel TEXT NOT NULL, kuenstler TEXT NOT NULL, album TEXT DEFAULT '', dauer_sekunden INTEGER NOT NULL, datei_pfad TEXT NOT NULL UNIQUE, cover_pfad TEXT, groesse_bytes INTEGER DEFAULT 0, ist_heruntergeladen INTEGER DEFAULT 0, hinzugefuegt_am TEXT NOT NULL, download_quelle TEXT DEFAULT 'local', zuletzt_position INTEGER ) '''); await db.execute(''' CREATE TABLE tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, icon TEXT, farbe_hex TEXT ) '''); await db.execute(''' CREATE TABLE song_tags ( song_id INTEGER NOT NULL, tag_id INTEGER NOT NULL, PRIMARY KEY (song_id, tag_id), FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE, FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE ) '''); await db.execute(''' CREATE TABLE playlists ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, erstellt_am TEXT NOT NULL ) '''); await db.execute(''' CREATE TABLE playlist_songs ( playlist_id INTEGER NOT NULL, song_id INTEGER NOT NULL, position INTEGER NOT NULL, PRIMARY KEY (playlist_id, song_id), FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE, FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE ) '''); await db.execute(''' CREATE TABLE wiedergabe_verlauf ( id INTEGER PRIMARY KEY AUTOINCREMENT, song_id INTEGER NOT NULL, position INTEGER DEFAULT 0, zuletzt_abgespielt TEXT NOT NULL, FOREIGN KEY (song_id) REFERENCES songs(id) ON DELETE CASCADE ) '''); }, ); } // ─── Songs ────────────────────────────────────── Future songEinfuegen(Song song) async { final d = await db; return d.insert('songs', song.toMap(), conflictAlgorithm: ConflictAlgorithm.ignore); } Future songsEinfuegen(List songs) async { final d = await db; final batch = d.batch(); for (final song in songs) { batch.insert('songs', song.toMap(), conflictAlgorithm: ConflictAlgorithm.ignore); } await batch.commit(noResult: true); } Future> alleSongs() async { final d = await db; final rows = await d.query('songs', orderBy: 'hinzugefuegt_am DESC'); return rows.map((r) => Song.fromMap(r)).toList(); } Future songNachId(int id) async { final d = await db; final rows = await d.query('songs', where: 'id = ?', whereArgs: [id]); if (rows.isEmpty) return null; return Song.fromMap(rows.first); } Future songNachPfad(String pfad) async { final d = await db; final rows = await d.query('songs', where: 'datei_pfad = ?', whereArgs: [pfad]); if (rows.isEmpty) return null; return Song.fromMap(rows.first); } Future positionAktualisieren(int songId, int position) async { final d = await db; await d.update('songs', {'zuletzt_position': position}, where: 'id = ?', whereArgs: [songId]); final rows = await d.update('wiedergabe_verlauf', {'position': position, 'zuletzt_abgespielt': DateTime.now().toIso8601String()}, where: 'song_id = ?', whereArgs: [songId]); if (rows == 0) { await d.insert('wiedergabe_verlauf', { 'song_id': songId, 'position': position, 'zuletzt_abgespielt': DateTime.now().toIso8601String(), }); } } // ─── Tags ──────────────────────────────────────── Future tagErstellen(String name, {String? icon, String? farbe}) async { final d = await db; return d.insert('tags', { 'name': name, 'icon': icon, 'farbe_hex': farbe, }, conflictAlgorithm: ConflictAlgorithm.ignore); } Future> alleTags() async { final d = await db; final rows = await d.query('tags', orderBy: 'name'); return rows.map((r) => Tag.fromMap(r)).toList(); } Future songTagHinzufuegen(int songId, int tagId) async { final d = await db; await d.insert('song_tags', {'song_id': songId, 'tag_id': tagId}, conflictAlgorithm: ConflictAlgorithm.ignore); } Future> tagsFuerSong(int songId) async { final d = await db; final rows = await d.rawQuery(''' SELECT t.* FROM tags t JOIN song_tags st ON t.id = st.tag_id WHERE st.song_id = ? ''', [songId]); return rows.map((r) => Tag.fromMap(r)).toList(); } // ─── Playlists ─────────────────────────────────── Future playlistErstellen(String name) async { final d = await db; return d.insert('playlists', { 'name': name, 'erstellt_am': DateTime.now().toIso8601String(), }); } Future songZurPlaylist(int playlistId, int songId, int position) async { final d = await db; await d.insert('playlist_songs', { 'playlist_id': playlistId, 'song_id': songId, 'position': position, }); } Future>> allePlaylists() async { final d = await db; return d.query('playlists', orderBy: 'erstellt_am DESC'); } Future songAusPlaylistEntfernen(int playlistId, int songId) async { final d = await db; await d.delete('playlist_songs', where: 'playlist_id = ? AND song_id = ?', whereArgs: [playlistId, songId]); } Future metadatenAktualisieren(int songId, {String? titel, String? kuenstler, String? album}) async { final d = await db; final update = {}; if (titel != null) update['titel'] = titel; if (kuenstler != null) update['kuenstler'] = kuenstler; if (album != null) update['album'] = album; if (update.isNotEmpty) { await d.update('songs', update, where: 'id = ?', whereArgs: [songId]); } } Future> songsDerPlaylist(int playlistId) async { final d = await db; final rows = await d.rawQuery(''' SELECT s.* FROM songs s JOIN playlist_songs ps ON s.id = ps.song_id WHERE ps.playlist_id = ? ORDER BY ps.position ''', [playlistId]); return rows.map((r) => Song.fromMap(r)).toList(); } Future loeschen() async { final d = await db; await d.transaction((txn) async { await txn.delete('wiedergabe_verlauf'); await txn.delete('song_tags'); await txn.delete('playlist_songs'); await txn.delete('playlists'); await txn.delete('tags'); await txn.delete('songs'); }); } }