This repository has been archived on 2026-08-19. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
melo-app/lib/database/db_helper.dart
T

371 lines
12 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<Database> get db async {
if (_db != null) return _db!;
_db = await _init();
return _db!;
}
Future<Database> _init() async {
final pfad = await getDatabasesPath();
return openDatabase(
p.join(pfad, 'melo.db'),
version: 4,
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,
ist_korrupt INTEGER DEFAULT 0,
hinzugefuegt_am TEXT NOT NULL,
download_quelle TEXT DEFAULT 'local',
stream_url TEXT,
yt_url TEXT,
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
)
''');
},
onUpgrade: (db, oldVersion, newVersion) async {
if (oldVersion < 2) {
try {
await db.execute('ALTER TABLE songs ADD COLUMN stream_url TEXT');
} catch (_) {
// Spalte existiert bereits ignorieren
}
}
if (oldVersion < 3) {
try {
await db.execute('ALTER TABLE songs ADD COLUMN ist_korrupt INTEGER DEFAULT 0');
} catch (_) {
// Spalte existiert bereits ignorieren
}
}
if (oldVersion < 4) {
try {
await db.execute('ALTER TABLE songs ADD COLUMN yt_url TEXT');
} catch (_) {
// Spalte existiert bereits ignorieren
}
}
},
);
}
/// Löscht fehlerhafte Einträge ohne Dateipfad
Future<void> alteDummiesLoeschen() async {
final d = await db;
await d.delete('songs', where: "datei_pfad = '' OR datei_pfad IS NULL");
}
/// Einzelnen Song löschen
Future<void> loeschSong(int id) async {
final d = await db;
await d.delete('songs', where: 'id = ?', whereArgs: [id]);
}
// ─── Songs ──────────────────────────────────────
Future<int> songEinfuegen(Song song) async {
final d = await db;
return d.insert('songs', song.toMap(),
conflictAlgorithm: ConflictAlgorithm.ignore);
}
Future<void> songsEinfuegen(List<Song> 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<List<Song>> 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<Song?> 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<Song?> 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<void> 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(),
});
}
}
/// Die letzten [anzahl] abgespielten Songs
Future<List<Song>> letzteWiedergaben({int anzahl = 5}) async {
final d = await db;
final rows = await d.rawQuery('''
SELECT s.* FROM songs s
JOIN wiedergabe_verlauf w ON s.id = w.song_id
ORDER BY w.zuletzt_abgespielt DESC
LIMIT ?
''', [anzahl]);
return rows.map((r) => Song.fromMap(r)).toList();
}
/// Markiert einen Song als korrupt
Future<void> alsKorruptMarkieren(int songId) async {
final d = await db;
await d.update('songs', {'ist_korrupt': 1},
where: 'id = ?', whereArgs: [songId]);
}
/// Markiert einen Song als nicht-korrupt (Reparatur)
Future<void> korruptZuruecksetzen(int songId) async {
final d = await db;
await d.update('songs', {'ist_korrupt': 0},
where: 'id = ?', whereArgs: [songId]);
}
/// Alle als korrupt markierten Songs abrufen
Future<List<Song>> korrupteSongs() async {
final d = await db;
final rows = await d.query('songs',
where: 'ist_korrupt = 1',
orderBy: 'hinzugefuegt_am DESC');
return rows.map((r) => Song.fromMap(r)).toList();
}
/// YouTube-Quell-URL für einen Song speichern
Future<void> ytUrlAktualisieren(int songId, String ytUrl) async {
final d = await db;
await d.update('songs', {'yt_url': ytUrl},
where: 'id = ?', whereArgs: [songId]);
}
/// Alle Songs ohne YouTube-Quell-URL abrufen (für Scanner-Suche)
Future<List<Song>> songsOhneYtUrl({int limit = 50}) async {
final d = await db;
final rows = await d.query('songs',
where: 'yt_url IS NULL OR yt_url = ""',
orderBy: 'hinzugefuegt_am DESC',
limit: limit);
return rows.map((r) => Song.fromMap(r)).toList();
}
// ─── Tags ────────────────────────────────────────
Future<int> 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<List<Tag>> alleTags() async {
final d = await db;
final rows = await d.query('tags', orderBy: 'name');
return rows.map((r) => Tag.fromMap(r)).toList();
}
Future<void> 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<List<Tag>> 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();
}
Future<void> songTagEntfernen(int songId, int tagId) async {
final d = await db;
await d.delete('song_tags',
where: 'song_id = ? AND tag_id = ?',
whereArgs: [songId, tagId]);
}
Future<List<int>> songIdsFuerTag(String tagName) async {
final d = await db;
final rows = await d.rawQuery('''
SELECT st.song_id FROM song_tags st
JOIN tags t ON t.id = st.tag_id
WHERE t.name = ?
''', [tagName]);
return rows.map((r) => r['song_id'] as int).toList();
}
Future<Map<String, int>> tagCountsBerechnen() async {
final d = await db;
final rows = await d.rawQuery('''
SELECT t.name, COUNT(st.song_id) as cnt
FROM tags t
LEFT JOIN song_tags st ON t.id = st.tag_id
GROUP BY t.name
''');
return {for (final r in rows) r['name'] as String: r['cnt'] as int};
}
// ─── Playlists ───────────────────────────────────
Future<int> playlistErstellen(String name) async {
final d = await db;
return d.insert('playlists', {
'name': name,
'erstellt_am': DateTime.now().toIso8601String(),
});
}
Future<void> 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<List<Map<String, dynamic>>> allePlaylists() async {
final d = await db;
return d.query('playlists', orderBy: 'erstellt_am DESC');
}
Future<void> songAusPlaylistEntfernen(int playlistId, int songId) async {
final d = await db;
await d.delete('playlist_songs',
where: 'playlist_id = ? AND song_id = ?',
whereArgs: [playlistId, songId]);
}
/// Aktualisiert die Position aller Songs in einer Playlist (nach Drag & Drop)
Future<void> playlistReihenfolgeAktualisieren(int playlistId, List<int> songIds) async {
final d = await db;
await d.transaction((txn) async {
for (int i = 0; i < songIds.length; i++) {
await txn.update(
'playlist_songs',
{'position': i},
where: 'playlist_id = ? AND song_id = ?',
whereArgs: [playlistId, songIds[i]],
);
}
});
}
Future<void> metadatenAktualisieren(int songId, {String? titel, String? kuenstler, String? album}) async {
final d = await db;
final update = <String, dynamic>{};
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<List<Song>> 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<void> 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');
});
}
}