mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-27 00:59:28 +01:00
682 lines
21 KiB
Dart
682 lines
21 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:collection/collection.dart';
|
|
import 'package:drift/drift.dart';
|
|
import 'package:drift/isolate.dart';
|
|
import 'package:drift/native.dart';
|
|
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
|
|
|
import '../log.dart';
|
|
import '../models/music.dart';
|
|
import '../models/query.dart';
|
|
import '../models/settings.dart';
|
|
import '../models/support.dart';
|
|
import 'converters.dart';
|
|
import 'error_logging_database.dart';
|
|
|
|
part 'database.g.dart';
|
|
|
|
// don't exceed SQLITE_MAX_VARIABLE_NUMBER (32766 for version >= 3.32.0)
|
|
// https://www.sqlite.org/limits.html
|
|
const kSqliteMaxVariableNumber = 32766;
|
|
|
|
@DriftDatabase(include: {'tables.drift'})
|
|
class SubtracksDatabase extends _$SubtracksDatabase {
|
|
SubtracksDatabase() : super(_openConnection());
|
|
SubtracksDatabase.connection(QueryExecutor e) : super(e);
|
|
|
|
@override
|
|
int get schemaVersion => 1;
|
|
|
|
@override
|
|
MigrationStrategy get migration {
|
|
return MigrationStrategy(
|
|
beforeOpen: (details) async {
|
|
await customStatement('PRAGMA foreign_keys = ON');
|
|
},
|
|
);
|
|
}
|
|
|
|
/// Runs a database opertion in a background isolate.
|
|
///
|
|
/// **Only pass top-level functions to [computation]!**
|
|
///
|
|
/// **Do not use non-serializable data inside [computation]!**
|
|
Future<Ret> background<Ret>(
|
|
FutureOr<Ret> Function(SubtracksDatabase) computation,
|
|
) async {
|
|
return computeWithDatabase(
|
|
connect: SubtracksDatabase.connection,
|
|
computation: computation,
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Album> albumsList(int sourceId, ListQuery opt) {
|
|
return filterAlbums(
|
|
(_) => _filterPredicate('albums', sourceId, opt),
|
|
(_) => _filterOrderBy(opt),
|
|
(_) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Album> albumsListDownloaded(int sourceId, ListQuery opt) {
|
|
return filterAlbumsDownloaded(
|
|
(_, __) => _filterPredicate('albums', sourceId, opt),
|
|
(_, __) => _filterOrderBy(opt),
|
|
(_, __) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Artist> artistsList(int sourceId, ListQuery opt) {
|
|
return filterArtists(
|
|
(_) => _filterPredicate('artists', sourceId, opt),
|
|
(_) => _filterOrderBy(opt),
|
|
(_) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Artist> artistsListDownloaded(int sourceId, ListQuery opt) {
|
|
return filterArtistsDownloaded(
|
|
(_, __, ___) => _filterPredicate('artists', sourceId, opt),
|
|
(_, __, ___) => _filterOrderBy(opt),
|
|
(_, __, ___) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Playlist> playlistsList(int sourceId, ListQuery opt) {
|
|
return filterPlaylists(
|
|
(_) => _filterPredicate('playlists', sourceId, opt),
|
|
(_) => _filterOrderBy(opt),
|
|
(_) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Playlist> playlistsListDownloaded(
|
|
int sourceId, ListQuery opt) {
|
|
return filterPlaylistsDownloaded(
|
|
(_, __, ___) => _filterPredicate('playlists', sourceId, opt),
|
|
(_, __, ___) => _filterOrderBy(opt),
|
|
(_, __, ___) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Song> songsList(int sourceId, ListQuery opt) {
|
|
return filterSongs(
|
|
(_) => _filterPredicate('songs', sourceId, opt),
|
|
(_) => _filterOrderBy(opt),
|
|
(_) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Song> songsListDownloaded(int sourceId, ListQuery opt) {
|
|
return filterSongsDownloaded(
|
|
(_) => _filterPredicate('songs', sourceId, opt),
|
|
(_) => _filterOrderBy(opt),
|
|
(_) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
Expression<bool> _filterPredicate(String table, int sourceId, ListQuery opt) {
|
|
return opt.filters.map((filter) => buildFilter<bool>(filter)).fold(
|
|
CustomExpression('$table.source_id = $sourceId'),
|
|
(previousValue, element) => previousValue & element,
|
|
);
|
|
}
|
|
|
|
OrderBy _filterOrderBy(ListQuery opt) {
|
|
return opt.sort != null
|
|
? OrderBy([_buildOrder(opt.sort!)])
|
|
: const OrderBy.nothing();
|
|
}
|
|
|
|
Limit _filterLimit(ListQuery opt) {
|
|
return Limit(opt.page.limit, opt.page.offset);
|
|
}
|
|
|
|
MultiSelectable<Song> albumSongsList(SourceId sid, ListQuery opt) {
|
|
return listQuery(
|
|
select(songs)
|
|
..where((tbl) =>
|
|
tbl.sourceId.equals(sid.sourceId) & tbl.albumId.equals(sid.id)),
|
|
opt,
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Song> songsByAlbumList(int sourceId, ListQuery opt) {
|
|
return filterSongsByGenre(
|
|
(_, __) => _filterPredicate('songs', sourceId, opt),
|
|
(_, __) => _filterOrderBy(opt),
|
|
(_, __) => _filterLimit(opt),
|
|
);
|
|
}
|
|
|
|
MultiSelectable<Song> playlistSongsList(SourceId sid, ListQuery opt) {
|
|
return listQueryJoined(
|
|
select(songs).join([
|
|
innerJoin(
|
|
playlistSongs,
|
|
playlistSongs.sourceId.equalsExp(songs.sourceId) &
|
|
playlistSongs.songId.equalsExp(songs.id),
|
|
useColumns: false,
|
|
),
|
|
])
|
|
..where(playlistSongs.sourceId.equals(sid.sourceId) &
|
|
playlistSongs.playlistId.equals(sid.id)),
|
|
opt,
|
|
).map((row) => row.readTable(songs));
|
|
}
|
|
|
|
Future<void> saveArtists(Iterable<ArtistsCompanion> artists) async {
|
|
await batch((batch) {
|
|
batch.insertAllOnConflictUpdate(this.artists, artists);
|
|
});
|
|
}
|
|
|
|
Future<void> deleteArtistsNotIn(int sourceId, Set<String> ids) {
|
|
return transaction(() async {
|
|
final allIds = (await (selectOnly(artists)
|
|
..addColumns([artists.id])
|
|
..where(artists.sourceId.equals(sourceId)))
|
|
.map((row) => row.read(artists.id))
|
|
.get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
final downloadIds = (await artistIdsWithDownloadStatus(sourceId).get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
|
|
final diff = allIds.difference(downloadIds).difference(ids);
|
|
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
await (delete(artists)
|
|
..where(
|
|
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
.go();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> saveAlbums(Iterable<AlbumsCompanion> albums) async {
|
|
await batch((batch) {
|
|
batch.insertAllOnConflictUpdate(this.albums, albums);
|
|
});
|
|
}
|
|
|
|
Future<void> deleteAlbumsNotIn(int sourceId, Set<String> ids) {
|
|
return transaction(() async {
|
|
final allIds = (await (selectOnly(albums)
|
|
..addColumns([albums.id])
|
|
..where(albums.sourceId.equals(sourceId)))
|
|
.map((row) => row.read(albums.id))
|
|
.get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
final downloadIds = (await albumIdsWithDownloadStatus(sourceId).get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
|
|
final diff = allIds.difference(downloadIds).difference(ids);
|
|
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
await (delete(albums)
|
|
..where(
|
|
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
.go();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> savePlaylists(
|
|
Iterable<PlaylistWithSongsCompanion> playlistsWithSongs,
|
|
) async {
|
|
final playlists = playlistsWithSongs.map((e) => e.playist);
|
|
final playlistSongs = playlistsWithSongs.expand((e) => e.songs);
|
|
final sourceId = playlists.first.sourceId.value;
|
|
|
|
await (delete(this.playlistSongs)
|
|
..where(
|
|
(tbl) =>
|
|
tbl.sourceId.equals(sourceId) &
|
|
tbl.playlistId.isIn(playlists.map((e) => e.id.value)),
|
|
))
|
|
.go();
|
|
|
|
await batch((batch) {
|
|
batch.insertAllOnConflictUpdate(this.playlists, playlists);
|
|
batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs);
|
|
});
|
|
}
|
|
|
|
Future<void> deletePlaylistsNotIn(int sourceId, Set<String> ids) {
|
|
return transaction(() async {
|
|
final allIds = (await (selectOnly(playlists)
|
|
..addColumns([playlists.id])
|
|
..where(playlists.sourceId.equals(sourceId)))
|
|
.map((row) => row.read(playlists.id))
|
|
.get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
final downloadIds = (await playlistIdsWithDownloadStatus(sourceId).get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
|
|
final diff = allIds.difference(downloadIds).difference(ids);
|
|
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
await (delete(playlists)
|
|
..where(
|
|
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
.go();
|
|
await (delete(playlistSongs)
|
|
..where((tbl) =>
|
|
tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(slice)))
|
|
.go();
|
|
}
|
|
});
|
|
}
|
|
|
|
Future<void> savePlaylistSongs(
|
|
int sourceId,
|
|
List<String> ids,
|
|
Iterable<PlaylistSongsCompanion> playlistSongs,
|
|
) async {
|
|
await (delete(this.playlistSongs)
|
|
..where(
|
|
(tbl) => tbl.sourceId.equals(sourceId) & tbl.playlistId.isIn(ids),
|
|
))
|
|
.go();
|
|
await batch((batch) {
|
|
batch.insertAllOnConflictUpdate(this.playlistSongs, playlistSongs);
|
|
});
|
|
}
|
|
|
|
Future<void> saveSongs(Iterable<SongsCompanion> songs) async {
|
|
await batch((batch) {
|
|
batch.insertAllOnConflictUpdate(this.songs, songs);
|
|
});
|
|
}
|
|
|
|
Future<void> deleteSongsNotIn(int sourceId, Set<String> ids) {
|
|
return transaction(() async {
|
|
final allIds = (await (selectOnly(songs)
|
|
..addColumns([songs.id])
|
|
..where(
|
|
songs.sourceId.equals(sourceId) &
|
|
songs.downloadFilePath.isNull() &
|
|
songs.downloadTaskId.isNull(),
|
|
))
|
|
.map((row) => row.read(songs.id))
|
|
.get())
|
|
.whereNotNull()
|
|
.toSet();
|
|
|
|
final diff = allIds.difference(ids);
|
|
for (var slice in diff.slices(kSqliteMaxVariableNumber)) {
|
|
await (delete(songs)
|
|
..where(
|
|
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isIn(slice)))
|
|
.go();
|
|
await (delete(playlistSongs)
|
|
..where(
|
|
(tbl) => tbl.sourceId.equals(sourceId) & tbl.songId.isIn(slice),
|
|
))
|
|
.go();
|
|
}
|
|
});
|
|
}
|
|
|
|
Selectable<LastBottomNavStateData> getLastBottomNavState() {
|
|
return select(lastBottomNavState)..where((tbl) => tbl.id.equals(1));
|
|
}
|
|
|
|
Future<void> saveLastBottomNavState(LastBottomNavStateData update) {
|
|
return into(lastBottomNavState).insertOnConflictUpdate(update);
|
|
}
|
|
|
|
Selectable<LastLibraryStateData> getLastLibraryState() {
|
|
return select(lastLibraryState)..where((tbl) => tbl.id.equals(1));
|
|
}
|
|
|
|
Future<void> saveLastLibraryState(LastLibraryStateData update) {
|
|
return into(lastLibraryState).insertOnConflictUpdate(update);
|
|
}
|
|
|
|
Selectable<LastAudioStateData> getLastAudioState() {
|
|
return select(lastAudioState)..where((tbl) => tbl.id.equals(1));
|
|
}
|
|
|
|
Future<void> saveLastAudioState(LastAudioStateCompanion update) {
|
|
return into(lastAudioState).insertOnConflictUpdate(update);
|
|
}
|
|
|
|
Future<void> insertQueue(Iterable<QueueCompanion> songs) async {
|
|
await batch((batch) {
|
|
batch.insertAll(queue, songs);
|
|
});
|
|
}
|
|
|
|
Future<void> clearQueue() async {
|
|
await delete(queue).go();
|
|
}
|
|
|
|
Future<void> setCurrentTrack(int index) async {
|
|
await transaction(() async {
|
|
await (update(queue)..where((tbl) => tbl.index.equals(index).not()))
|
|
.write(const QueueCompanion(currentTrack: Value(null)));
|
|
await (update(queue)..where((tbl) => tbl.index.equals(index)))
|
|
.write(const QueueCompanion(currentTrack: Value(true)));
|
|
});
|
|
}
|
|
|
|
Future<void> createSource(
|
|
SourcesCompanion source,
|
|
SubsonicSourcesCompanion subsonic,
|
|
) async {
|
|
await transaction(() async {
|
|
final count = await sourcesCount().getSingle();
|
|
if (count == 0) {
|
|
source = source.copyWith(isActive: const Value(true));
|
|
}
|
|
|
|
final id = await into(sources).insert(source);
|
|
subsonic = subsonic.copyWith(sourceId: Value(id));
|
|
await into(subsonicSources).insert(subsonic);
|
|
});
|
|
}
|
|
|
|
Future<void> updateSource(SubsonicSettings source) async {
|
|
await transaction(() async {
|
|
await into(sources).insertOnConflictUpdate(source.toSourceInsertable());
|
|
await into(subsonicSources)
|
|
.insertOnConflictUpdate(source.toSubsonicInsertable());
|
|
});
|
|
}
|
|
|
|
Future<void> deleteSource(int sourceId) async {
|
|
await transaction(() async {
|
|
await (delete(subsonicSources)
|
|
..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
.go();
|
|
await (delete(sources)..where((tbl) => tbl.id.equals(sourceId))).go();
|
|
|
|
await (delete(songs)..where((tbl) => tbl.sourceId.equals(sourceId))).go();
|
|
await (delete(albums)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
.go();
|
|
await (delete(artists)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
.go();
|
|
await (delete(playlistSongs)
|
|
..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
.go();
|
|
await (delete(playlists)..where((tbl) => tbl.sourceId.equals(sourceId)))
|
|
.go();
|
|
});
|
|
}
|
|
|
|
Future<void> setActiveSource(int id) async {
|
|
await batch((batch) {
|
|
batch.update(
|
|
sources,
|
|
const SourcesCompanion(isActive: Value(null)),
|
|
where: (t) => t.id.isNotValue(id),
|
|
);
|
|
batch.update(
|
|
sources,
|
|
const SourcesCompanion(isActive: Value(true)),
|
|
where: (t) => t.id.equals(id),
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> updateSettings(AppSettingsCompanion settings) async {
|
|
await into(appSettings).insertOnConflictUpdate(settings);
|
|
}
|
|
}
|
|
|
|
LazyDatabase _openConnection() {
|
|
return LazyDatabase(() async {
|
|
final dbFolder = await getApplicationDocumentsDirectory();
|
|
final file = File(p.join(dbFolder.path, 'subtracks.sqlite'));
|
|
// return NativeDatabase.createInBackground(file, logStatements: true);
|
|
|
|
return ErrorLoggingDatabase(
|
|
NativeDatabase.createInBackground(file),
|
|
(e, s) => log.severe('SQL error', e, s),
|
|
);
|
|
});
|
|
}
|
|
|
|
@Riverpod(keepAlive: true)
|
|
SubtracksDatabase database(DatabaseRef ref) {
|
|
return SubtracksDatabase();
|
|
}
|
|
|
|
OrderingTerm _buildOrder(SortBy sort) {
|
|
OrderingMode? mode =
|
|
sort.dir == SortDirection.asc ? OrderingMode.asc : OrderingMode.desc;
|
|
return OrderingTerm(
|
|
expression: CustomExpression(sort.column),
|
|
mode: mode,
|
|
);
|
|
}
|
|
|
|
SimpleSelectStatement<T, R> listQuery<T extends HasResultSet, R>(
|
|
SimpleSelectStatement<T, R> query,
|
|
ListQuery opt,
|
|
) {
|
|
if (opt.page.limit > 0) {
|
|
query.limit(opt.page.limit, offset: opt.page.offset);
|
|
}
|
|
|
|
if (opt.sort != null) {
|
|
OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc
|
|
? OrderingMode.asc
|
|
: OrderingMode.desc;
|
|
query.orderBy([
|
|
(t) => OrderingTerm(
|
|
expression: CustomExpression(opt.sort!.column),
|
|
mode: mode,
|
|
)
|
|
]);
|
|
}
|
|
|
|
for (var filter in opt.filters) {
|
|
query.where((tbl) => buildFilter(filter));
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
JoinedSelectStatement<T, R> listQueryJoined<T extends HasResultSet, R>(
|
|
JoinedSelectStatement<T, R> query,
|
|
ListQuery opt,
|
|
) {
|
|
if (opt.page.limit > 0) {
|
|
query.limit(opt.page.limit, offset: opt.page.offset);
|
|
}
|
|
|
|
if (opt.sort != null) {
|
|
OrderingMode? mode = opt.sort != null && opt.sort!.dir == SortDirection.asc
|
|
? OrderingMode.asc
|
|
: OrderingMode.desc;
|
|
query.orderBy([
|
|
OrderingTerm(
|
|
expression: CustomExpression(opt.sort!.column),
|
|
mode: mode,
|
|
)
|
|
]);
|
|
}
|
|
|
|
for (var filter in opt.filters) {
|
|
query.where(buildFilter(filter));
|
|
}
|
|
|
|
return query;
|
|
}
|
|
|
|
CustomExpression<T> buildFilter<T extends Object>(
|
|
FilterWith filter,
|
|
) {
|
|
return filter.when(
|
|
equals: (column, value, invert) => CustomExpression<T>(
|
|
'$column ${invert ? '<>' : '='} \'$value\'',
|
|
),
|
|
greaterThan: (column, value, orEquals) => CustomExpression<T>(
|
|
'$column ${orEquals ? '>=' : '>'} $value',
|
|
),
|
|
isNull: (column, invert) => CustomExpression<T>(
|
|
'$column ${invert ? 'IS NOT' : 'IS'} NULL',
|
|
),
|
|
betweenInt: (column, from, to) => CustomExpression<T>(
|
|
'$column BETWEEN $from AND $to',
|
|
),
|
|
isIn: (column, invert, values) => CustomExpression<T>(
|
|
'$column ${invert ? 'NOT IN' : 'IN'} (${values.join(',')})',
|
|
),
|
|
);
|
|
}
|
|
|
|
class AlbumSongsCompanion {
|
|
final AlbumsCompanion album;
|
|
final Iterable<SongsCompanion> songs;
|
|
|
|
AlbumSongsCompanion(this.album, this.songs);
|
|
}
|
|
|
|
class ArtistAlbumsCompanion {
|
|
final ArtistsCompanion artist;
|
|
final Iterable<AlbumsCompanion> albums;
|
|
|
|
ArtistAlbumsCompanion(this.artist, this.albums);
|
|
}
|
|
|
|
class PlaylistWithSongsCompanion {
|
|
final PlaylistsCompanion playist;
|
|
final Iterable<PlaylistSongsCompanion> songs;
|
|
|
|
PlaylistWithSongsCompanion(this.playist, this.songs);
|
|
}
|
|
|
|
// Future<void> saveArtist(
|
|
// SubtracksDatabase db,
|
|
// ArtistAlbumsCompanion artistAlbums,
|
|
// ) async {
|
|
// return db.background((db) async {
|
|
// final artist = artistAlbums.artist;
|
|
// final albums = artistAlbums.albums;
|
|
|
|
// await db.batch((batch) {
|
|
// batch.insertAllOnConflictUpdate(db.artists, [artist]);
|
|
// batch.insertAllOnConflictUpdate(db.albums, albums);
|
|
|
|
// // remove this artistId from albums not found in source
|
|
// // don't delete them since they coud have been moved to another artist
|
|
// // that we haven't synced yet
|
|
// final albumIds = {for (var a in albums) a.id.value};
|
|
// batch.update(
|
|
// db.albums,
|
|
// const AlbumsCompanion(artistId: Value(null)),
|
|
// where: (tbl) =>
|
|
// tbl.sourceId.equals(artist.sourceId.value) &
|
|
// tbl.artistId.equals(artist.id.value) &
|
|
// tbl.id.isNotIn(albumIds),
|
|
// );
|
|
// });
|
|
// });
|
|
// }
|
|
|
|
// Future<void> saveAlbum(
|
|
// SubtracksDatabase db,
|
|
// AlbumSongsCompanion albumSongs,
|
|
// ) async {
|
|
// return db.background((db) async {
|
|
// final album = albumSongs.album.copyWith(synced: Value(DateTime.now()));
|
|
// final songs = albumSongs.songs;
|
|
|
|
// final songIds = {for (var a in songs) a.id.value};
|
|
// final hardDeletedSongIds = (await (db.selectOnly(db.songs)
|
|
// ..addColumns([db.songs.id])
|
|
// ..where(
|
|
// db.songs.sourceId.equals(album.sourceId.value) &
|
|
// db.songs.albumId.equals(album.id.value) &
|
|
// db.songs.id.isNotIn(songIds) &
|
|
// db.songs.downloadFilePath.isNull() &
|
|
// db.songs.downloadTaskId.isNull(),
|
|
// ))
|
|
// .map((row) => row.read(db.songs.id))
|
|
// .get())
|
|
// .whereNotNull();
|
|
|
|
// await db.batch((batch) {
|
|
// batch.insertAllOnConflictUpdate(db.albums, [album]);
|
|
// batch.insertAllOnConflictUpdate(db.songs, songs);
|
|
|
|
// // soft delete songs that have been downloaded so that the user
|
|
// // can decide to keep or remove them later
|
|
// // TODO: add a setting to skip soft delete and just remove download too
|
|
// batch.update(
|
|
// db.songs,
|
|
// const SongsCompanion(isDeleted: Value(true)),
|
|
// where: (tbl) =>
|
|
// tbl.sourceId.equals(album.sourceId.value) &
|
|
// tbl.albumId.equals(album.id.value) &
|
|
// tbl.id.isNotIn(songIds) &
|
|
// (tbl.downloadFilePath.isNotNull() | tbl.downloadTaskId.isNotNull()),
|
|
// );
|
|
|
|
// // safe to hard delete songs that have not been downloaded
|
|
// batch.deleteWhere(
|
|
// db.songs,
|
|
// (tbl) =>
|
|
// tbl.sourceId.equals(album.sourceId.value) &
|
|
// tbl.id.isIn(hardDeletedSongIds),
|
|
// );
|
|
|
|
// // also need to remove these songs from any playlists that contain them
|
|
// batch.deleteWhere(
|
|
// db.playlistSongs,
|
|
// (tbl) =>
|
|
// tbl.sourceId.equals(album.sourceId.value) &
|
|
// tbl.songId.isIn(hardDeletedSongIds),
|
|
// );
|
|
// });
|
|
// });
|
|
// }
|
|
|
|
// Future<void> savePlaylist(
|
|
// SubtracksDatabase db,
|
|
// PlaylistWithSongsCompanion playlistWithSongs,
|
|
// ) async {
|
|
// return db.background((db) async {
|
|
// final playlist =
|
|
// playlistWithSongs.playist.copyWith(synced: Value(DateTime.now()));
|
|
// final songs = playlistWithSongs.songs;
|
|
|
|
// await db.batch((batch) {
|
|
// batch.insertAllOnConflictUpdate(db.playlists, [playlist]);
|
|
// batch.insertAllOnConflictUpdate(db.songs, songs);
|
|
|
|
// batch.insertAllOnConflictUpdate(
|
|
// db.playlistSongs,
|
|
// songs.mapIndexed(
|
|
// (index, song) => PlaylistSongsCompanion.insert(
|
|
// sourceId: playlist.sourceId.value,
|
|
// playlistId: playlist.id.value,
|
|
// songId: song.id.value,
|
|
// position: index,
|
|
// ),
|
|
// ),
|
|
// );
|
|
|
|
// // the new playlist could be shorter than the old one, so we delete
|
|
// // playlist songs above our new playlist's length
|
|
// batch.deleteWhere(
|
|
// db.playlistSongs,
|
|
// (tbl) =>
|
|
// tbl.sourceId.equals(playlist.sourceId.value) &
|
|
// tbl.playlistId.equals(playlist.id.value) &
|
|
// tbl.position.isBiggerOrEqualValue(songs.length),
|
|
// );
|
|
// });
|
|
// });
|
|
// }
|