mirror of
https://github.com/zarzet/SpotiFLAC-Mobile.git
synced 2026-09-30 13:19:44 +02:00
refactor: migrate persistence to SQLite, add strict provider mode, and optimize collection lookups
- Replace SharedPreferences with SQLite (AppStateDatabase, LibraryCollectionsDatabase) for download queue, library collections, and recent access history - Add Set-based O(1) track containment checks for wishlist, loved, and playlist tracks - Add batch addTracksToPlaylist with PlaylistAddBatchResult - Go backend: strict mode locks download to selected provider when auto fallback is off - Go backend: fix extension progress normalization (percent/100) and lifecycle tracking - Go backend: case-insensitive provider ID matching throughout fallback chain - Lyrics embedding now respects lyricsMode setting (embed/both/off) - Debounced queue persistence to reduce write frequency - Fix shouldUseFallback logic to not be gated by useExtensions
This commit is contained in:
1 parent
8e794e1ef1
commit
e39756fa3f
15 files changed
+1581
-624
No files matched your search
@@ -0,0 +1,309 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('AppStateDb');
|
||||
|
||||
const _dbFileName = 'app_state.db';
|
||||
const _dbVersion = 1;
|
||||
|
||||
const _queueTable = 'download_queue_items';
|
||||
const _recentTable = 'recent_access_items';
|
||||
const _hiddenRecentTable = 'hidden_recent_downloads';
|
||||
|
||||
const _legacyQueueKey = 'download_queue';
|
||||
const _legacyRecentAccessKey = 'recent_access_history';
|
||||
const _legacyHiddenDownloadsKey = 'hidden_downloads_in_recents';
|
||||
|
||||
const _queueMigrationKey = 'app_state_migrated_queue_to_sqlite_v1';
|
||||
const _recentMigrationKey = 'app_state_migrated_recent_to_sqlite_v1';
|
||||
|
||||
class AppStateDatabase {
|
||||
static final AppStateDatabase instance = AppStateDatabase._init();
|
||||
static Database? _database;
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
|
||||
AppStateDatabase._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDb();
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<Database> _initDb() async {
|
||||
final dbPath = await getApplicationDocumentsDirectory();
|
||||
final path = join(dbPath.path, _dbFileName);
|
||||
|
||||
_log.i('Initializing app state database at: $path');
|
||||
|
||||
return openDatabase(
|
||||
path,
|
||||
version: _dbVersion,
|
||||
onCreate: _createDb,
|
||||
onUpgrade: _upgradeDb,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createDb(Database db, int version) async {
|
||||
_log.i('Creating app state database schema v$version');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_queueTable (
|
||||
id TEXT PRIMARY KEY,
|
||||
item_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_queueTable}_status ON $_queueTable(status)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_queueTable}_created ON $_queueTable(created_at ASC)',
|
||||
);
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_recentTable (
|
||||
unique_key TEXT PRIMARY KEY,
|
||||
item_json TEXT NOT NULL,
|
||||
accessed_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_recentTable}_accessed ON $_recentTable(accessed_at DESC)',
|
||||
);
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_hiddenRecentTable (
|
||||
download_id TEXT PRIMARY KEY,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
}
|
||||
|
||||
Future<void> _upgradeDb(Database db, int oldVersion, int newVersion) async {
|
||||
_log.i('Upgrading app state database from v$oldVersion to v$newVersion');
|
||||
}
|
||||
|
||||
Future<bool> migrateQueueFromSharedPreferences() async {
|
||||
final prefs = await _prefs;
|
||||
if (prefs.getBool(_queueMigrationKey) == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final raw = prefs.getString(_legacyQueueKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
await prefs.setBool(_queueMigrationKey, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! List) {
|
||||
await prefs.setBool(_queueMigrationKey, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
final nowIso = DateTime.now().toIso8601String();
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
final batch = txn.batch();
|
||||
for (final entry in decoded.whereType<Map>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final id = map['id'] as String?;
|
||||
if (id == null || id.isEmpty) continue;
|
||||
|
||||
final status = map['status'] as String? ?? 'queued';
|
||||
if (status != 'queued' && status != 'downloading') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status == 'downloading') {
|
||||
map['status'] = 'queued';
|
||||
map['progress'] = 0.0;
|
||||
map['speedMBps'] = 0.0;
|
||||
map['bytesReceived'] = 0;
|
||||
}
|
||||
|
||||
final createdAt = map['createdAt'] as String? ?? nowIso;
|
||||
batch.insert(_queueTable, {
|
||||
'id': id,
|
||||
'item_json': jsonEncode(map),
|
||||
'status': 'queued',
|
||||
'created_at': createdAt,
|
||||
'updated_at': nowIso,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
|
||||
await prefs.setBool(_queueMigrationKey, true);
|
||||
_log.i('Migrated legacy queue data to SQLite');
|
||||
return true;
|
||||
} catch (e, stack) {
|
||||
_log.e('Failed queue migration to SQLite: $e', e, stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<bool> migrateRecentAccessFromSharedPreferences() async {
|
||||
final prefs = await _prefs;
|
||||
if (prefs.getBool(_recentMigrationKey) == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final rawRecent = prefs.getString(_legacyRecentAccessKey);
|
||||
final hiddenIds = prefs.getStringList(_legacyHiddenDownloadsKey);
|
||||
if ((rawRecent == null || rawRecent.isEmpty) &&
|
||||
(hiddenIds == null || hiddenIds.isEmpty)) {
|
||||
await prefs.setBool(_recentMigrationKey, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final nowIso = DateTime.now().toIso8601String();
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
if (rawRecent != null && rawRecent.isNotEmpty) {
|
||||
final decoded = jsonDecode(rawRecent);
|
||||
if (decoded is List) {
|
||||
final batch = txn.batch();
|
||||
for (final entry in decoded.whereType<Map>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final type = map['type'] as String?;
|
||||
final id = map['id'] as String?;
|
||||
final providerId = map['providerId'] as String?;
|
||||
if (type == null || id == null || type.isEmpty || id.isEmpty) {
|
||||
continue;
|
||||
}
|
||||
final uniqueKey = '$type:${providerId ?? 'default'}:$id';
|
||||
final accessedAt = map['accessedAt'] as String? ?? nowIso;
|
||||
batch.insert(_recentTable, {
|
||||
'unique_key': uniqueKey,
|
||||
'item_json': jsonEncode(map),
|
||||
'accessed_at': accessedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (hiddenIds != null && hiddenIds.isNotEmpty) {
|
||||
final batch = txn.batch();
|
||||
for (final id in hiddenIds) {
|
||||
if (id.isEmpty) continue;
|
||||
batch.insert(_hiddenRecentTable, {
|
||||
'download_id': id,
|
||||
'updated_at': nowIso,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
}
|
||||
});
|
||||
|
||||
await prefs.setBool(_recentMigrationKey, true);
|
||||
_log.i('Migrated legacy recent-access data to SQLite');
|
||||
return true;
|
||||
} catch (e, stack) {
|
||||
_log.e('Failed recent-access migration to SQLite: $e', e, stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getPendingDownloadQueueRows() async {
|
||||
final db = await database;
|
||||
return db.query(
|
||||
_queueTable,
|
||||
where: 'status = ? OR status = ?',
|
||||
whereArgs: ['queued', 'downloading'],
|
||||
orderBy: 'created_at ASC, rowid ASC',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> replacePendingDownloadQueueRows(
|
||||
List<Map<String, dynamic>> rows,
|
||||
) async {
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.delete(_queueTable);
|
||||
if (rows.isEmpty) return;
|
||||
|
||||
final batch = txn.batch();
|
||||
for (final row in rows) {
|
||||
batch.insert(
|
||||
_queueTable,
|
||||
row,
|
||||
conflictAlgorithm: ConflictAlgorithm.replace,
|
||||
);
|
||||
}
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
}
|
||||
|
||||
Future<List<Map<String, dynamic>>> getRecentAccessRows({int? limit}) async {
|
||||
final db = await database;
|
||||
return db.query(
|
||||
_recentTable,
|
||||
orderBy: 'accessed_at DESC, rowid DESC',
|
||||
limit: limit,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> upsertRecentAccessRow({
|
||||
required String uniqueKey,
|
||||
required String itemJson,
|
||||
required String accessedAt,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.insert(_recentTable, {
|
||||
'unique_key': uniqueKey,
|
||||
'item_json': itemJson,
|
||||
'accessed_at': accessedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> deleteRecentAccessRow(String uniqueKey) async {
|
||||
final db = await database;
|
||||
await db.delete(
|
||||
_recentTable,
|
||||
where: 'unique_key = ?',
|
||||
whereArgs: [uniqueKey],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> clearRecentAccessRows() async {
|
||||
final db = await database;
|
||||
await db.delete(_recentTable);
|
||||
}
|
||||
|
||||
Future<Set<String>> getHiddenRecentDownloadIds() async {
|
||||
final db = await database;
|
||||
final rows = await db.query(_hiddenRecentTable, columns: ['download_id']);
|
||||
return rows
|
||||
.map((row) => row['download_id'] as String?)
|
||||
.whereType<String>()
|
||||
.toSet();
|
||||
}
|
||||
|
||||
Future<void> addHiddenRecentDownloadId(String downloadId) async {
|
||||
final id = downloadId.trim();
|
||||
if (id.isEmpty) return;
|
||||
final db = await database;
|
||||
await db.insert(_hiddenRecentTable, {
|
||||
'download_id': id,
|
||||
'updated_at': DateTime.now().toIso8601String(),
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> clearHiddenRecentDownloadIds() async {
|
||||
final db = await database;
|
||||
await db.delete(_hiddenRecentTable);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,411 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:sqflite/sqflite.dart';
|
||||
import 'package:spotiflac_android/utils/logger.dart';
|
||||
|
||||
final _log = AppLogger('LibraryCollectionsDb');
|
||||
|
||||
const _dbFileName = 'library_collections.db';
|
||||
const _dbVersion = 1;
|
||||
|
||||
const _tableWishlist = 'wishlist_tracks';
|
||||
const _tableLoved = 'loved_tracks';
|
||||
const _tablePlaylists = 'playlists';
|
||||
const _tablePlaylistTracks = 'playlist_tracks';
|
||||
|
||||
const _legacyCollectionsStorageKey = 'library_collections_v1';
|
||||
const _migrationDoneKey = 'library_collections_migrated_to_sqlite_v1';
|
||||
|
||||
class LibraryCollectionsSnapshot {
|
||||
final List<Map<String, dynamic>> wishlistRows;
|
||||
final List<Map<String, dynamic>> lovedRows;
|
||||
final List<Map<String, dynamic>> playlistRows;
|
||||
final List<Map<String, dynamic>> playlistTrackRows;
|
||||
|
||||
const LibraryCollectionsSnapshot({
|
||||
required this.wishlistRows,
|
||||
required this.lovedRows,
|
||||
required this.playlistRows,
|
||||
required this.playlistTrackRows,
|
||||
});
|
||||
}
|
||||
|
||||
class LibraryCollectionsDatabase {
|
||||
static final LibraryCollectionsDatabase instance =
|
||||
LibraryCollectionsDatabase._init();
|
||||
static Database? _database;
|
||||
|
||||
final Future<SharedPreferences> _prefs = SharedPreferences.getInstance();
|
||||
|
||||
LibraryCollectionsDatabase._init();
|
||||
|
||||
Future<Database> get database async {
|
||||
if (_database != null) return _database!;
|
||||
_database = await _initDb();
|
||||
return _database!;
|
||||
}
|
||||
|
||||
Future<Database> _initDb() async {
|
||||
final dbPath = await getApplicationDocumentsDirectory();
|
||||
final path = join(dbPath.path, _dbFileName);
|
||||
|
||||
_log.i('Initializing collections database at: $path');
|
||||
|
||||
return openDatabase(
|
||||
path,
|
||||
version: _dbVersion,
|
||||
onConfigure: (db) async {
|
||||
await db.execute('PRAGMA foreign_keys = ON');
|
||||
},
|
||||
onCreate: _createDb,
|
||||
onUpgrade: _upgradeDb,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _createDb(Database db, int version) async {
|
||||
_log.i('Creating collections database schema v$version');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_tableWishlist (
|
||||
track_key TEXT PRIMARY KEY,
|
||||
track_json TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_tableLoved (
|
||||
track_key TEXT PRIMARY KEY,
|
||||
track_json TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_tablePlaylists (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
cover_image_path TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute('''
|
||||
CREATE TABLE $_tablePlaylistTracks (
|
||||
playlist_id TEXT NOT NULL,
|
||||
track_key TEXT NOT NULL,
|
||||
track_json TEXT NOT NULL,
|
||||
added_at TEXT NOT NULL,
|
||||
PRIMARY KEY (playlist_id, track_key),
|
||||
FOREIGN KEY (playlist_id) REFERENCES $_tablePlaylists(id) ON DELETE CASCADE
|
||||
)
|
||||
''');
|
||||
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_tableWishlist}_added_at ON $_tableWishlist(added_at DESC)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_tableLoved}_added_at ON $_tableLoved(added_at DESC)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_tablePlaylists}_created_at ON $_tablePlaylists(created_at DESC)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_tablePlaylistTracks}_playlist_id ON $_tablePlaylistTracks(playlist_id)',
|
||||
);
|
||||
await db.execute(
|
||||
'CREATE INDEX idx_${_tablePlaylistTracks}_added_at ON $_tablePlaylistTracks(added_at DESC)',
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _upgradeDb(Database db, int oldVersion, int newVersion) async {
|
||||
_log.i('Upgrading collections database from v$oldVersion to v$newVersion');
|
||||
}
|
||||
|
||||
Future<bool> migrateFromSharedPreferences() async {
|
||||
final prefs = await _prefs;
|
||||
if (prefs.getBool(_migrationDoneKey) == true) {
|
||||
return false;
|
||||
}
|
||||
|
||||
final raw = prefs.getString(_legacyCollectionsStorageKey);
|
||||
if (raw == null || raw.isEmpty) {
|
||||
await prefs.setBool(_migrationDoneKey, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
final decoded = jsonDecode(raw);
|
||||
if (decoded is! Map) {
|
||||
await prefs.setBool(_migrationDoneKey, true);
|
||||
return false;
|
||||
}
|
||||
|
||||
final root = Map<String, dynamic>.from(decoded);
|
||||
final wishlistRaw = (root['wishlist'] as List?) ?? const [];
|
||||
final lovedRaw = (root['loved'] as List?) ?? const [];
|
||||
final playlistsRaw = (root['playlists'] as List?) ?? const [];
|
||||
final nowIso = DateTime.now().toIso8601String();
|
||||
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
for (final entry in wishlistRaw.whereType<Map>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final trackKey = map['key'] as String?;
|
||||
final track = map['track'];
|
||||
if (trackKey == null || track is! Map) continue;
|
||||
final addedAt = (map['addedAt'] as String?) ?? nowIso;
|
||||
await txn.insert(_tableWishlist, {
|
||||
'track_key': trackKey,
|
||||
'track_json': jsonEncode(track),
|
||||
'added_at': addedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
for (final entry in lovedRaw.whereType<Map>()) {
|
||||
final map = Map<String, dynamic>.from(entry);
|
||||
final trackKey = map['key'] as String?;
|
||||
final track = map['track'];
|
||||
if (trackKey == null || track is! Map) continue;
|
||||
final addedAt = (map['addedAt'] as String?) ?? nowIso;
|
||||
await txn.insert(_tableLoved, {
|
||||
'track_key': trackKey,
|
||||
'track_json': jsonEncode(track),
|
||||
'added_at': addedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
for (final playlistEntry in playlistsRaw.whereType<Map>()) {
|
||||
final playlist = Map<String, dynamic>.from(playlistEntry);
|
||||
final playlistId = playlist['id'] as String?;
|
||||
if (playlistId == null || playlistId.isEmpty) continue;
|
||||
|
||||
final createdAt = (playlist['createdAt'] as String?) ?? nowIso;
|
||||
final updatedAt = (playlist['updatedAt'] as String?) ?? createdAt;
|
||||
await txn.insert(_tablePlaylists, {
|
||||
'id': playlistId,
|
||||
'name': (playlist['name'] as String?) ?? '',
|
||||
'cover_image_path': playlist['coverImagePath'] as String?,
|
||||
'created_at': createdAt,
|
||||
'updated_at': updatedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
|
||||
final tracksRaw = (playlist['tracks'] as List?) ?? const [];
|
||||
for (final trackEntry in tracksRaw.whereType<Map>()) {
|
||||
final trackMap = Map<String, dynamic>.from(trackEntry);
|
||||
final trackKey = trackMap['key'] as String?;
|
||||
final track = trackMap['track'];
|
||||
if (trackKey == null || track is! Map) continue;
|
||||
final addedAt = (trackMap['addedAt'] as String?) ?? nowIso;
|
||||
await txn.insert(_tablePlaylistTracks, {
|
||||
'playlist_id': playlistId,
|
||||
'track_key': trackKey,
|
||||
'track_json': jsonEncode(track),
|
||||
'added_at': addedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await prefs.setBool(_migrationDoneKey, true);
|
||||
_log.i('Migrated legacy collections data to SQLite');
|
||||
return true;
|
||||
} catch (e, stack) {
|
||||
_log.e('Failed migrating collections to SQLite: $e', e, stack);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
Future<LibraryCollectionsSnapshot> loadSnapshot() async {
|
||||
final db = await database;
|
||||
final wishlistRows = await db.query(
|
||||
_tableWishlist,
|
||||
orderBy: 'added_at DESC, rowid DESC',
|
||||
);
|
||||
final lovedRows = await db.query(
|
||||
_tableLoved,
|
||||
orderBy: 'added_at DESC, rowid DESC',
|
||||
);
|
||||
final playlistRows = await db.query(
|
||||
_tablePlaylists,
|
||||
orderBy: 'created_at DESC, rowid DESC',
|
||||
);
|
||||
final playlistTrackRows = await db.query(
|
||||
_tablePlaylistTracks,
|
||||
orderBy: 'playlist_id ASC, added_at DESC, rowid DESC',
|
||||
);
|
||||
|
||||
return LibraryCollectionsSnapshot(
|
||||
wishlistRows: wishlistRows,
|
||||
lovedRows: lovedRows,
|
||||
playlistRows: playlistRows,
|
||||
playlistTrackRows: playlistTrackRows,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> upsertWishlistEntry({
|
||||
required String trackKey,
|
||||
required String trackJson,
|
||||
required String addedAt,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.insert(_tableWishlist, {
|
||||
'track_key': trackKey,
|
||||
'track_json': trackJson,
|
||||
'added_at': addedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> deleteWishlistEntry(String trackKey) async {
|
||||
final db = await database;
|
||||
await db.delete(
|
||||
_tableWishlist,
|
||||
where: 'track_key = ?',
|
||||
whereArgs: [trackKey],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> upsertLovedEntry({
|
||||
required String trackKey,
|
||||
required String trackJson,
|
||||
required String addedAt,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.insert(_tableLoved, {
|
||||
'track_key': trackKey,
|
||||
'track_json': trackJson,
|
||||
'added_at': addedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> deleteLovedEntry(String trackKey) async {
|
||||
final db = await database;
|
||||
await db.delete(_tableLoved, where: 'track_key = ?', whereArgs: [trackKey]);
|
||||
}
|
||||
|
||||
Future<void> upsertPlaylist({
|
||||
required String id,
|
||||
required String name,
|
||||
required String createdAt,
|
||||
required String updatedAt,
|
||||
String? coverImagePath,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.insert(_tablePlaylists, {
|
||||
'id': id,
|
||||
'name': name,
|
||||
'cover_image_path': coverImagePath,
|
||||
'created_at': createdAt,
|
||||
'updated_at': updatedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
|
||||
Future<void> renamePlaylist({
|
||||
required String playlistId,
|
||||
required String name,
|
||||
required String updatedAt,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.update(
|
||||
_tablePlaylists,
|
||||
{'name': name, 'updated_at': updatedAt},
|
||||
where: 'id = ?',
|
||||
whereArgs: [playlistId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> updatePlaylistCover({
|
||||
required String playlistId,
|
||||
required String updatedAt,
|
||||
String? coverImagePath,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.update(
|
||||
_tablePlaylists,
|
||||
{'cover_image_path': coverImagePath, 'updated_at': updatedAt},
|
||||
where: 'id = ?',
|
||||
whereArgs: [playlistId],
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> deletePlaylist(String playlistId) async {
|
||||
final db = await database;
|
||||
await db.delete(_tablePlaylists, where: 'id = ?', whereArgs: [playlistId]);
|
||||
}
|
||||
|
||||
Future<void> upsertPlaylistTrack({
|
||||
required String playlistId,
|
||||
required String trackKey,
|
||||
required String trackJson,
|
||||
required String addedAt,
|
||||
required String playlistUpdatedAt,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.insert(_tablePlaylistTracks, {
|
||||
'playlist_id': playlistId,
|
||||
'track_key': trackKey,
|
||||
'track_json': trackJson,
|
||||
'added_at': addedAt,
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
await txn.update(
|
||||
_tablePlaylists,
|
||||
{'updated_at': playlistUpdatedAt},
|
||||
where: 'id = ?',
|
||||
whereArgs: [playlistId],
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> upsertPlaylistTracksBatch({
|
||||
required String playlistId,
|
||||
required String playlistUpdatedAt,
|
||||
required List<Map<String, String>> tracks,
|
||||
}) async {
|
||||
if (tracks.isEmpty) return;
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
final batch = txn.batch();
|
||||
for (final track in tracks) {
|
||||
batch.insert(_tablePlaylistTracks, {
|
||||
'playlist_id': playlistId,
|
||||
'track_key': track['track_key'],
|
||||
'track_json': track['track_json'],
|
||||
'added_at': track['added_at'],
|
||||
}, conflictAlgorithm: ConflictAlgorithm.replace);
|
||||
}
|
||||
batch.update(
|
||||
_tablePlaylists,
|
||||
{'updated_at': playlistUpdatedAt},
|
||||
where: 'id = ?',
|
||||
whereArgs: [playlistId],
|
||||
);
|
||||
await batch.commit(noResult: true);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deletePlaylistTrack({
|
||||
required String playlistId,
|
||||
required String trackKey,
|
||||
required String playlistUpdatedAt,
|
||||
}) async {
|
||||
final db = await database;
|
||||
await db.transaction((txn) async {
|
||||
await txn.delete(
|
||||
_tablePlaylistTracks,
|
||||
where: 'playlist_id = ? AND track_key = ?',
|
||||
whereArgs: [playlistId, trackKey],
|
||||
);
|
||||
await txn.update(
|
||||
_tablePlaylists,
|
||||
{'updated_at': playlistUpdatedAt},
|
||||
where: 'id = ?',
|
||||
whereArgs: [playlistId],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user