mirror of
https://github.com/austinried/subtracks.git
synced 2026-02-10 06:52:43 +01:00
v2
This commit is contained in:
72
lib/database/converters.dart
Normal file
72
lib/database/converters.dart
Normal file
@@ -0,0 +1,72 @@
|
||||
import 'dart:convert';
|
||||
|
||||
import 'package:drift/drift.dart';
|
||||
import 'package:fast_immutable_collections/fast_immutable_collections.dart';
|
||||
|
||||
import '../models/query.dart';
|
||||
import '../models/settings.dart';
|
||||
|
||||
class DurationSecondsConverter extends TypeConverter<Duration, int> {
|
||||
const DurationSecondsConverter();
|
||||
|
||||
@override
|
||||
Duration fromSql(int fromDb) => Duration(seconds: fromDb);
|
||||
|
||||
@override
|
||||
int toSql(Duration value) => value.inSeconds;
|
||||
}
|
||||
|
||||
class UriConverter extends TypeConverter<Uri, String> {
|
||||
const UriConverter();
|
||||
|
||||
@override
|
||||
Uri fromSql(String fromDb) => Uri.parse(fromDb);
|
||||
|
||||
@override
|
||||
String toSql(Uri value) => value.toString();
|
||||
}
|
||||
|
||||
class ListQueryConverter extends TypeConverter<ListQuery, String> {
|
||||
const ListQueryConverter();
|
||||
|
||||
@override
|
||||
ListQuery fromSql(String fromDb) => ListQuery.fromJson(jsonDecode(fromDb));
|
||||
|
||||
@override
|
||||
String toSql(ListQuery value) => jsonEncode(value.toJson());
|
||||
}
|
||||
|
||||
class SubsonicFeatureListConverter
|
||||
extends TypeConverter<IList<SubsonicFeature>, String> {
|
||||
const SubsonicFeatureListConverter();
|
||||
|
||||
@override
|
||||
IList<SubsonicFeature> fromSql(String fromDb) {
|
||||
return IList<SubsonicFeature>.fromJson(
|
||||
jsonDecode(fromDb),
|
||||
(item) => SubsonicFeature.values.byName(item as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(IList<SubsonicFeature> value) {
|
||||
return jsonEncode(value.toJson((e) => e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
class IListIntConverter extends TypeConverter<IList<int>, String> {
|
||||
const IListIntConverter();
|
||||
|
||||
@override
|
||||
IList<int> fromSql(String fromDb) {
|
||||
return IList<int>.fromJson(
|
||||
jsonDecode(fromDb),
|
||||
(item) => int.parse(item as String),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
String toSql(IList<int> value) {
|
||||
return jsonEncode(value.toJson((e) => jsonEncode(e)));
|
||||
}
|
||||
}
|
||||
627
lib/database/database.dart
Normal file
627
lib/database/database.dart
Normal file
@@ -0,0 +1,627 @@
|
||||
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 '../models/music.dart';
|
||||
import '../models/query.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../models/support.dart';
|
||||
import 'converters.dart';
|
||||
|
||||
part 'database.g.dart';
|
||||
|
||||
@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, Iterable<String> ids) async {
|
||||
await (delete(artists)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(ids),
|
||||
))
|
||||
.go();
|
||||
}
|
||||
|
||||
Future<void> saveAlbums(Iterable<AlbumsCompanion> albums) async {
|
||||
await batch((batch) {
|
||||
batch.insertAllOnConflictUpdate(this.albums, albums);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAlbumsNotIn(int sourceId, Iterable<String> ids) async {
|
||||
final alsoKeep = (await albumIdsWithDownloaded(sourceId).get()).toSet();
|
||||
|
||||
ids = ids.toList()..addAll(alsoKeep);
|
||||
await (delete(albums)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(ids),
|
||||
))
|
||||
.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, Iterable<String> ids) async {
|
||||
await (delete(playlists)
|
||||
..where(
|
||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(ids),
|
||||
))
|
||||
.go();
|
||||
await (delete(playlistSongs)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.sourceId.equals(sourceId) & tbl.playlistId.isNotIn(ids),
|
||||
))
|
||||
.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, Iterable<String> ids) async {
|
||||
await (delete(songs)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.sourceId.equals(sourceId) &
|
||||
tbl.id.isNotIn(ids) &
|
||||
tbl.downloadFilePath.isNull() &
|
||||
tbl.downloadTaskId.isNull(),
|
||||
))
|
||||
.go();
|
||||
final remainingIds = (await (selectOnly(songs)
|
||||
..addColumns([songs.id])
|
||||
..where(songs.sourceId.equals(sourceId)))
|
||||
.map((row) => row.read(songs.id))
|
||||
.get())
|
||||
.whereNotNull();
|
||||
await (delete(playlistSongs)
|
||||
..where(
|
||||
(tbl) =>
|
||||
tbl.sourceId.equals(sourceId) &
|
||||
tbl.songId.isNotIn(remainingIds),
|
||||
))
|
||||
.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 NativeDatabase.createInBackground(file);
|
||||
});
|
||||
}
|
||||
|
||||
@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),
|
||||
// );
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
5547
lib/database/database.g.dart
Normal file
5547
lib/database/database.g.dart
Normal file
File diff suppressed because it is too large
Load Diff
547
lib/database/tables.drift
Normal file
547
lib/database/tables.drift
Normal file
@@ -0,0 +1,547 @@
|
||||
import '../models/music.dart';
|
||||
import '../models/settings.dart';
|
||||
import '../models/support.dart';
|
||||
import 'converters.dart';
|
||||
|
||||
--
|
||||
-- SCHEMA
|
||||
--
|
||||
|
||||
CREATE TABLE queue(
|
||||
"index" INT NOT NULL PRIMARY KEY UNIQUE,
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
context ENUM(QueueContextType) NOT NULL,
|
||||
context_id TEXT,
|
||||
current_track BOOLEAN UNIQUE
|
||||
);
|
||||
CREATE INDEX queue_index ON queue ("index");
|
||||
CREATE INDEX queue_current_track ON queue ("current_track");
|
||||
|
||||
CREATE TABLE last_audio_state(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
queue_mode ENUM(QueueMode) NOT NULL,
|
||||
shuffle_indicies TEXT MAPPED BY `const IListIntConverter()`,
|
||||
repeat ENUM(RepeatMode) NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE last_bottom_nav_state(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
tab TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE last_library_state(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
tab TEXT NOT NULL,
|
||||
albums_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
||||
artists_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
||||
playlists_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`,
|
||||
songs_list TEXT NOT NULL MAPPED BY `const ListQueryConverter()`
|
||||
);
|
||||
|
||||
CREATE TABLE app_settings(
|
||||
id INT NOT NULL PRIMARY KEY,
|
||||
max_bitrate_wifi INT NOT NULL,
|
||||
max_bitrate_mobile INT NOT NULL,
|
||||
stream_format TEXT
|
||||
) WITH AppSettings;
|
||||
|
||||
CREATE TABLE sources(
|
||||
id INT NOT NULL PRIMARY KEY AUTOINCREMENT,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
address TEXT NOT NULL MAPPED BY `const UriConverter()`,
|
||||
is_active BOOLEAN UNIQUE,
|
||||
created_at DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP))
|
||||
);
|
||||
|
||||
CREATE TABLE subsonic_sources(
|
||||
source_id INT NOT NULL PRIMARY KEY,
|
||||
features TEXT NOT NULL MAPPED BY `const SubsonicFeatureListConverter()`,
|
||||
username TEXT NOT NULL,
|
||||
password TEXT NOT NULL,
|
||||
use_token_auth BOOLEAN NOT NULL DEFAULT 1,
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE artists(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
album_count INT NOT NULL,
|
||||
starred DATETIME,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Artist;
|
||||
CREATE INDEX artists_source_id ON artists (source_id);
|
||||
|
||||
CREATE VIRTUAL TABLE artists_fts USING fts5(source_id, name, content=artists, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER artists_ai AFTER INSERT ON artists BEGIN
|
||||
INSERT INTO artists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER artists_ad AFTER DELETE ON artists BEGIN
|
||||
INSERT INTO artists_fts(artists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER artists_au AFTER UPDATE ON artists BEGIN
|
||||
INSERT INTO artists_fts(artists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
INSERT INTO artists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TABLE albums(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
artist_id TEXT,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
album_artist TEXT COLLATE NOCASE,
|
||||
created DATETIME NOT NULL,
|
||||
cover_art TEXT,
|
||||
genre TEXT,
|
||||
year INT,
|
||||
starred DATETIME,
|
||||
song_count INT NOT NULL,
|
||||
frequent_rank INT,
|
||||
recent_rank INT,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Album;
|
||||
CREATE INDEX albums_source_id ON albums (source_id);
|
||||
CREATE INDEX albums_source_id_artist_id_idx ON albums (source_id, artist_id);
|
||||
|
||||
CREATE VIRTUAL TABLE albums_fts USING fts5(source_id, name, content=albums, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER albums_ai AFTER INSERT ON albums BEGIN
|
||||
INSERT INTO albums_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER albums_ad AFTER DELETE ON albums BEGIN
|
||||
INSERT INTO albums_fts(albums_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER albums_au AFTER UPDATE ON albums BEGIN
|
||||
INSERT INTO albums_fts(albums_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
INSERT INTO albums_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TABLE playlists(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
name TEXT NOT NULL COLLATE NOCASE,
|
||||
comment TEXT COLLATE NOCASE,
|
||||
cover_art TEXT,
|
||||
song_count INT NOT NULL,
|
||||
created DATETIME NOT NULL,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Playlist;
|
||||
CREATE INDEX playlists_source_id ON playlists (source_id);
|
||||
CREATE TABLE playlist_songs(
|
||||
source_id INT NOT NULL,
|
||||
playlist_id TEXT NOT NULL,
|
||||
song_id TEXT NOT NULL,
|
||||
position INT NOT NULL,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, playlist_id, position),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX playlist_songs_source_id_playlist_id_idx ON playlist_songs (source_id, playlist_id);
|
||||
CREATE INDEX playlist_songs_source_id_song_id_idx ON playlist_songs (source_id, song_id);
|
||||
|
||||
CREATE VIRTUAL TABLE playlists_fts USING fts5(source_id, name, content=playlists, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER playlists_ai AFTER INSERT ON playlists BEGIN
|
||||
INSERT INTO playlists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER playlists_ad AFTER DELETE ON playlists BEGIN
|
||||
INSERT INTO playlists_fts(playlists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER playlists_au AFTER UPDATE ON playlists BEGIN
|
||||
INSERT INTO playlists_fts(playlists_fts, rowid, source_id, name)
|
||||
VALUES('delete', old.rowid, old.source_id, old.name);
|
||||
INSERT INTO playlists_fts(rowid, source_id, name)
|
||||
VALUES (new.rowid, new.source_id, new.name);
|
||||
END;
|
||||
|
||||
CREATE TABLE songs(
|
||||
source_id INT NOT NULL,
|
||||
id TEXT NOT NULL,
|
||||
album_id TEXT,
|
||||
artist_id TEXT,
|
||||
title TEXT NOT NULL COLLATE NOCASE,
|
||||
album TEXT COLLATE NOCASE,
|
||||
artist TEXT COLLATE NOCASE,
|
||||
duration INT MAPPED BY `const DurationSecondsConverter()`,
|
||||
track INT,
|
||||
disc INT,
|
||||
starred DATETIME,
|
||||
genre TEXT,
|
||||
download_task_id TEXT UNIQUE,
|
||||
download_file_path TEXT UNIQUE,
|
||||
is_deleted BOOLEAN NOT NULL DEFAULT 0,
|
||||
updated DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP)),
|
||||
PRIMARY KEY (source_id, id),
|
||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
||||
) WITH Song;
|
||||
CREATE INDEX songs_source_id_album_id_idx ON songs (source_id, album_id);
|
||||
CREATE INDEX songs_source_id_artist_id_idx ON songs (source_id, artist_id);
|
||||
CREATE INDEX songs_download_task_id_idx ON songs (download_task_id);
|
||||
|
||||
CREATE VIRTUAL TABLE songs_fts USING fts5(source_id, title, content=songs, content_rowid=rowid);
|
||||
|
||||
CREATE TRIGGER songs_ai AFTER INSERT ON songs BEGIN
|
||||
INSERT INTO songs_fts(rowid, source_id, title)
|
||||
VALUES (new.rowid, new.source_id, new.title);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER songs_ad AFTER DELETE ON songs BEGIN
|
||||
INSERT INTO songs_fts(songs_fts, rowid, source_id, title)
|
||||
VALUES('delete', old.rowid, old.source_id, old.title);
|
||||
END;
|
||||
|
||||
CREATE TRIGGER songs_au AFTER UPDATE ON songs BEGIN
|
||||
INSERT INTO songs_fts(songs_fts, rowid, source_id, title)
|
||||
VALUES('delete', old.rowid, old.source_id, old.title);
|
||||
INSERT INTO songs_fts(rowid, source_id, title)
|
||||
VALUES (new.rowid, new.source_id, new.title);
|
||||
END;
|
||||
|
||||
--
|
||||
-- QUERIES
|
||||
--
|
||||
|
||||
sourcesCount:
|
||||
SELECT COUNT(*)
|
||||
FROM sources;
|
||||
|
||||
allSubsonicSources WITH SubsonicSettings:
|
||||
SELECT
|
||||
sources.id,
|
||||
sources.name,
|
||||
sources.address,
|
||||
sources.is_active,
|
||||
sources.created_at,
|
||||
subsonic_sources.features,
|
||||
subsonic_sources.username,
|
||||
subsonic_sources.password,
|
||||
subsonic_sources.use_token_auth
|
||||
FROM sources
|
||||
JOIN subsonic_sources ON subsonic_sources.source_id = sources.id;
|
||||
|
||||
albumIdsWithDownloaded:
|
||||
SELECT albums.id
|
||||
FROM albums
|
||||
JOIN songs on songs.source_id = albums.source_id AND songs.album_id = albums.id
|
||||
WHERE
|
||||
albums.source_id = :source_id
|
||||
AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
||||
GROUP BY albums.id;
|
||||
|
||||
searchArtists:
|
||||
SELECT rowid
|
||||
FROM artists_fts
|
||||
WHERE artists_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
searchAlbums:
|
||||
SELECT rowid
|
||||
FROM albums_fts
|
||||
WHERE albums_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
searchPlaylists:
|
||||
SELECT rowid
|
||||
FROM playlists_fts
|
||||
WHERE playlists_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
searchSongs:
|
||||
SELECT rowid
|
||||
FROM songs_fts
|
||||
WHERE songs_fts MATCH :query
|
||||
ORDER BY rank
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
artistById:
|
||||
SELECT * FROM artists
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumById:
|
||||
SELECT * FROM albums
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumsByArtistId:
|
||||
SELECT * FROM albums
|
||||
WHERE source_id = :source_id AND artist_id = :artist_id;
|
||||
|
||||
albumsInIds:
|
||||
SELECT * FROM albums
|
||||
WHERE source_id = :source_id AND id IN :ids;
|
||||
|
||||
playlistById:
|
||||
SELECT * FROM playlists
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
songById:
|
||||
SELECT * FROM songs
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumGenres:
|
||||
SELECT
|
||||
genre
|
||||
FROM albums
|
||||
WHERE genre IS NOT NULL AND source_id = :source_id
|
||||
GROUP BY genre
|
||||
ORDER BY COUNT(genre) DESC
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
albumsByGenre:
|
||||
SELECT
|
||||
albums.*
|
||||
FROM albums
|
||||
JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE songs.source_id = :source_id AND songs.genre = :genre
|
||||
GROUP BY albums.id
|
||||
ORDER BY albums.created DESC, albums.name
|
||||
LIMIT :limit OFFSET :offset;
|
||||
|
||||
filterSongsByGenre:
|
||||
SELECT
|
||||
songs.*
|
||||
FROM songs
|
||||
JOIN albums ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
songsByGenreCount:
|
||||
SELECT
|
||||
COUNT(*)
|
||||
FROM songs
|
||||
WHERE songs.source_id = :source_id AND songs.genre = :genre;
|
||||
|
||||
songsWithDownloadTasks:
|
||||
SELECT * FROM songs
|
||||
WHERE download_task_id IS NOT NULL;
|
||||
|
||||
songByDownloadTask:
|
||||
SELECT * FROM songs
|
||||
WHERE download_task_id = :task_id;
|
||||
|
||||
clearSongDownloadTaskBySong:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
completeSongDownload:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL,
|
||||
download_file_path = :file_path
|
||||
WHERE download_task_id = :task_id;
|
||||
|
||||
clearSongDownloadTask:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL,
|
||||
download_file_path = NULL
|
||||
WHERE download_task_id = :task_id;
|
||||
|
||||
updateSongDownloadTask:
|
||||
UPDATE songs SET
|
||||
download_task_id = :task_id
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
deleteSongDownloadFile:
|
||||
UPDATE songs SET
|
||||
download_task_id = NULL,
|
||||
download_file_path = NULL
|
||||
WHERE source_id = :source_id AND id = :id;
|
||||
|
||||
albumDownloadStatus WITH ListDownloadStatus:
|
||||
SELECT
|
||||
COUNT(*) as total,
|
||||
COUNT(CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS downloaded,
|
||||
COUNT(CASE WHEN songs.download_task_id IS NOT NULL THEN songs.id ELSE NULL END) AS downloading
|
||||
FROM albums
|
||||
JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE albums.source_id = :source_id AND albums.id = :id;
|
||||
|
||||
playlistDownloadStatus WITH ListDownloadStatus:
|
||||
SELECT
|
||||
COUNT(DISTINCT songs.id) as total,
|
||||
COUNT(DISTINCT CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS downloaded,
|
||||
COUNT(DISTINCT CASE WHEN songs.download_task_id IS NOT NULL THEN songs.id ELSE NULL END) AS downloading
|
||||
FROM playlists
|
||||
JOIN playlist_songs ON
|
||||
playlist_songs.source_id = playlists.source_id
|
||||
AND playlist_songs.playlist_id = playlists.id
|
||||
JOIN songs ON
|
||||
songs.source_id = playlist_songs.source_id
|
||||
AND songs.id = playlist_songs.song_id
|
||||
WHERE
|
||||
playlists.source_id = :source_id AND playlists.id = :id;
|
||||
|
||||
filterAlbums:
|
||||
SELECT
|
||||
albums.*
|
||||
FROM albums
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterAlbumsDownloaded:
|
||||
SELECT
|
||||
albums.*
|
||||
FROM albums
|
||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE $predicate
|
||||
GROUP BY albums.source_id, albums.id
|
||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterArtists:
|
||||
SELECT
|
||||
artists.*
|
||||
FROM artists
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterArtistsDownloaded WITH Artist:
|
||||
SELECT
|
||||
artists.*,
|
||||
COUNT(DISTINCT CASE WHEN songs.download_file_path IS NOT NULL THEN songs.album_id ELSE NULL END) AS album_count
|
||||
FROM artists
|
||||
LEFT JOIN albums ON artists.source_id = albums.source_id AND artists.id = albums.artist_id
|
||||
LEFT JOIN songs ON albums.source_id = songs.source_id AND albums.id = songs.album_id
|
||||
WHERE $predicate
|
||||
GROUP BY artists.source_id, artists.id
|
||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterPlaylists:
|
||||
SELECT
|
||||
playlists.*
|
||||
FROM playlists
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterPlaylistsDownloaded WITH Playlist:
|
||||
SELECT
|
||||
playlists.*,
|
||||
COUNT(CASE WHEN songs.download_file_path IS NOT NULL THEN songs.id ELSE NULL END) AS song_count
|
||||
FROM playlists
|
||||
LEFT JOIN playlist_songs ON playlist_songs.source_id = playlists.source_id AND playlist_songs.playlist_id = playlists.id
|
||||
LEFT JOIN songs ON playlist_songs.source_id = songs.source_id AND playlist_songs.song_id = songs.id
|
||||
WHERE $predicate
|
||||
GROUP BY playlists.source_id, playlists.id
|
||||
HAVING SUM(CASE WHEN songs.download_file_path IS NOT NULL THEN 1 ELSE 0 END) > 0
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterSongs:
|
||||
SELECT
|
||||
songs.*
|
||||
FROM songs
|
||||
WHERE $predicate
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
filterSongsDownloaded:
|
||||
SELECT
|
||||
songs.*
|
||||
FROM songs
|
||||
WHERE $predicate AND songs.download_file_path IS NOT NULL
|
||||
ORDER BY $order
|
||||
LIMIT $limit;
|
||||
|
||||
playlistIsDownloaded:
|
||||
SELECT
|
||||
COUNT(*) = 0
|
||||
FROM playlists
|
||||
JOIN playlist_songs ON
|
||||
playlist_songs.source_id = playlists.source_id
|
||||
AND playlist_songs.playlist_id = playlists.id
|
||||
JOIN songs ON
|
||||
songs.source_id = playlist_songs.source_id
|
||||
AND songs.id = playlist_songs.song_id
|
||||
WHERE
|
||||
playlists.source_id = :source_id AND playlists.id = :id
|
||||
AND songs.download_file_path IS NULL;
|
||||
|
||||
playlistHasDownloadsInProgress:
|
||||
SELECT
|
||||
COUNT(*) > 0
|
||||
FROM playlists
|
||||
JOIN playlist_songs ON
|
||||
playlist_songs.source_id = playlists.source_id
|
||||
AND playlist_songs.playlist_id = playlists.id
|
||||
JOIN songs ON
|
||||
songs.source_id = playlist_songs.source_id
|
||||
AND songs.id = playlist_songs.song_id
|
||||
WHERE playlists.source_id = :source_id AND playlists.id = :id
|
||||
AND songs.download_task_id IS NOT NULL;
|
||||
|
||||
songsInIds:
|
||||
SELECT *
|
||||
FROM songs
|
||||
WHERE source_id = :source_id AND id IN :ids;
|
||||
|
||||
songsInRowIds:
|
||||
SELECT *
|
||||
FROM songs
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
albumsInRowIds:
|
||||
SELECT *
|
||||
FROM albums
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
artistsInRowIds:
|
||||
SELECT *
|
||||
FROM artists
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
playlistsInRowIds:
|
||||
SELECT *
|
||||
FROM playlists
|
||||
WHERE ROWID IN :row_ids;
|
||||
|
||||
currentTrackIndex:
|
||||
SELECT
|
||||
queue."index"
|
||||
FROM queue
|
||||
WHERE queue.current_track = 1;
|
||||
|
||||
queueLength:
|
||||
SELECT COUNT(*) FROM queue;
|
||||
|
||||
queueInIndicies:
|
||||
SELECT *
|
||||
FROM queue
|
||||
WHERE queue."index" IN :indicies;
|
||||
|
||||
getAppSettings:
|
||||
SELECT * FROM app_settings
|
||||
WHERE id = 1;
|
||||
23
lib/database/util.dart
Normal file
23
lib/database/util.dart
Normal file
@@ -0,0 +1,23 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart';
|
||||
import 'package:path_provider/path_provider.dart';
|
||||
|
||||
Future<String> datapasePath(String database) async =>
|
||||
join((await getApplicationSupportDirectory()).path, 'databases', database);
|
||||
|
||||
extension DateTimeExt on DateTime {
|
||||
String toDb() => DateFormat('yyyy-MM-dd hh:mm:ss').format(toUtc());
|
||||
|
||||
static DateTime parseUtc(Object? obj) {
|
||||
final str = obj.toString();
|
||||
return DateTime.parse(hasTimeZone(str) ? str : '${obj}Z').toLocal();
|
||||
}
|
||||
|
||||
static DateTime? tryParseUtc(Object? obj) {
|
||||
final str = obj.toString();
|
||||
return DateTime.tryParse(hasTimeZone(str) ? str : '${obj}Z')?.toLocal();
|
||||
}
|
||||
|
||||
static bool hasTimeZone(String str) =>
|
||||
RegExp(r'(Z|[+-](?:2[0-3]|[01][0-9]):[0-5][0-9])').hasMatch(str);
|
||||
}
|
||||
Reference in New Issue
Block a user