mirror of
https://github.com/austinried/subtracks.git
synced 2025-12-29 17:39:27 +01:00
Compare commits
No commits in common. "0c80dbdba53d22bc5a9c2b1ab93a01267a7ad7ce" and "2df86f4faab1431139ac822838b8cb68b75867a8" have entirely different histories.
0c80dbdba5
...
2df86f4faa
3
.gitignore
vendored
3
.gitignore
vendored
@ -46,6 +46,3 @@ app.*.map.json
|
|||||||
|
|
||||||
# VSCode
|
# VSCode
|
||||||
.vscode/settings.json
|
.vscode/settings.json
|
||||||
|
|
||||||
# subtracks
|
|
||||||
/music
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ services:
|
|||||||
build: ./docker/library-manager
|
build: ./docker/library-manager
|
||||||
volumes:
|
volumes:
|
||||||
- deno-dir:/deno-dir
|
- deno-dir:/deno-dir
|
||||||
- ./music:/music
|
- music:/music
|
||||||
|
|
||||||
navidrome:
|
navidrome:
|
||||||
image: deluan/navidrome:latest
|
image: deluan/navidrome:latest
|
||||||
@ -14,7 +14,7 @@ services:
|
|||||||
ND_LOGLEVEL: debug
|
ND_LOGLEVEL: debug
|
||||||
volumes:
|
volumes:
|
||||||
- navidrome-data:/data
|
- navidrome-data:/data
|
||||||
- ./music:/music:ro
|
- music:/music:ro
|
||||||
|
|
||||||
gonic:
|
gonic:
|
||||||
image: sentriz/gonic:latest
|
image: sentriz/gonic:latest
|
||||||
@ -26,7 +26,7 @@ services:
|
|||||||
- 4747:80
|
- 4747:80
|
||||||
volumes:
|
volumes:
|
||||||
- gonic-data:/data
|
- gonic-data:/data
|
||||||
- ./music:/music:ro
|
- music:/music:ro
|
||||||
- gonic-podcasts:/podcasts
|
- gonic-podcasts:/podcasts
|
||||||
- gonic-playlists:/playlists
|
- gonic-playlists:/playlists
|
||||||
- gonic-cache:/cache
|
- gonic-cache:/cache
|
||||||
|
|||||||
@ -3,6 +3,9 @@ import * as path from "jsr:@std/path@1.1.2";
|
|||||||
import { MUSIC_DIR } from "./util/env.ts";
|
import { MUSIC_DIR } from "./util/env.ts";
|
||||||
import { SubsonicClient } from "./util/subsonic.ts";
|
import { SubsonicClient } from "./util/subsonic.ts";
|
||||||
|
|
||||||
|
await new Deno.Command("rm", { args: ["-rf", path.join(MUSIC_DIR, "*")] })
|
||||||
|
.output();
|
||||||
|
|
||||||
const client = new SubsonicClient(
|
const client = new SubsonicClient(
|
||||||
"http://demo.subsonic.org",
|
"http://demo.subsonic.org",
|
||||||
"guest1",
|
"guest1",
|
||||||
@ -10,7 +13,7 @@ const client = new SubsonicClient(
|
|||||||
);
|
);
|
||||||
|
|
||||||
for (const id of ["197", "199", "321"]) {
|
for (const id of ["197", "199", "321"]) {
|
||||||
const { res } = await client.get("download", [["id", id]]);
|
const { res } = await client.get("download", { id });
|
||||||
|
|
||||||
let filename = res.headers.get("Content-Disposition")
|
let filename = res.headers.get("Content-Disposition")
|
||||||
?.split(";")[1];
|
?.split(";")[1];
|
||||||
|
|||||||
@ -2,69 +2,31 @@
|
|||||||
import { SubsonicClient } from "./util/subsonic.ts";
|
import { SubsonicClient } from "./util/subsonic.ts";
|
||||||
import { sleep } from "./util/util.ts";
|
import { sleep } from "./util/util.ts";
|
||||||
|
|
||||||
async function getSongId(
|
async function scrobbleTrack(
|
||||||
client: SubsonicClient,
|
client: SubsonicClient,
|
||||||
album: string,
|
album: string,
|
||||||
track: number,
|
track: number,
|
||||||
): Promise<string> {
|
|
||||||
const { xml: albumsXml } = await client.get("getAlbumList2", [
|
|
||||||
["type", "newest"],
|
|
||||||
]);
|
|
||||||
const albumId = albumsXml.querySelector(
|
|
||||||
`album[name='${album.replaceAll("'", "\\'")}']`,
|
|
||||||
)?.id;
|
|
||||||
|
|
||||||
const { xml: songsXml } = await client.get("getAlbum", [["id", albumId!]]);
|
|
||||||
return songsXml.querySelector(`song[track='${track}']`)?.id!;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function scrobbleTrack(
|
|
||||||
client: SubsonicClient,
|
|
||||||
songId: string,
|
|
||||||
) {
|
) {
|
||||||
await client.get("scrobble", [
|
const { xml: albumsXml } = await client.get("getAlbumList2", {
|
||||||
["id", songId!],
|
type: "newest",
|
||||||
["submission", "true"],
|
});
|
||||||
]);
|
const albumId = albumsXml.querySelector(`album[name='${album}']`)?.id;
|
||||||
}
|
|
||||||
|
|
||||||
async function createPlaylist(
|
const { xml: songsXml } = await client.get("getAlbum", { id: albumId! });
|
||||||
client: SubsonicClient,
|
const songId = songsXml.querySelector(`song[track='${track}']`)?.id;
|
||||||
name: string,
|
|
||||||
songs: { album: string; track: number }[],
|
|
||||||
) {
|
|
||||||
const songIds = await Promise.all(songs.map(({ album, track }) => {
|
|
||||||
return getSongId(client, album, track);
|
|
||||||
}));
|
|
||||||
|
|
||||||
await client.get("createPlaylist", [
|
await client.get("scrobble", {
|
||||||
["name", name],
|
id: songId!,
|
||||||
...songIds.map((songId) => ["songId", songId] as [string, string]),
|
submission: "true",
|
||||||
]);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupTestData(client: SubsonicClient) {
|
async function setupTestData(client: SubsonicClient) {
|
||||||
await scrobbleTrack(
|
await scrobbleTrack(client, "Retroconnaissance EP", 1);
|
||||||
client,
|
|
||||||
await getSongId(client, "Retroconnaissance EP", 1),
|
|
||||||
);
|
|
||||||
await sleep(1_000);
|
await sleep(1_000);
|
||||||
await scrobbleTrack(
|
await scrobbleTrack(client, "Retroconnaissance EP", 2);
|
||||||
client,
|
|
||||||
await getSongId(client, "Retroconnaissance EP", 2),
|
|
||||||
);
|
|
||||||
await sleep(1_000);
|
await sleep(1_000);
|
||||||
await scrobbleTrack(client, await getSongId(client, "Kosmonaut", 1));
|
await scrobbleTrack(client, "Kosmonaut", 1);
|
||||||
|
|
||||||
await createPlaylist(client, "Playlist 1", [
|
|
||||||
{ album: "Retroconnaissance EP", track: 2 },
|
|
||||||
{ album: "Retroconnaissance EP", track: 1 },
|
|
||||||
{ album: "Kosmonaut", track: 2 },
|
|
||||||
{ album: "Kosmonaut", track: 4 },
|
|
||||||
{ album: "I Don't Know What I'm Doing", track: 9 },
|
|
||||||
{ album: "I Don't Know What I'm Doing", track: 10 },
|
|
||||||
{ album: "I Don't Know What I'm Doing", track: 11 },
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function setupNavidrome() {
|
async function setupNavidrome() {
|
||||||
|
|||||||
@ -10,15 +10,15 @@ export class SubsonicClient {
|
|||||||
|
|
||||||
async get(
|
async get(
|
||||||
method: "download",
|
method: "download",
|
||||||
params?: [string, string][],
|
params?: Record<string, string>,
|
||||||
): Promise<{ res: Response; xml: undefined }>;
|
): Promise<{ res: Response; xml: undefined }>;
|
||||||
async get(
|
async get(
|
||||||
method: string,
|
method: string,
|
||||||
params?: [string, string][],
|
params?: Record<string, string>,
|
||||||
): Promise<{ res: Response; xml: Document }>;
|
): Promise<{ res: Response; xml: Document }>;
|
||||||
async get(
|
async get(
|
||||||
method: string,
|
method: string,
|
||||||
params?: [string, string][],
|
params?: Record<string, string>,
|
||||||
): Promise<{ res: Response; xml: Document | undefined }> {
|
): Promise<{ res: Response; xml: Document | undefined }> {
|
||||||
const url = new URL(`rest/${method}.view`, this.baseUrl);
|
const url = new URL(`rest/${method}.view`, this.baseUrl);
|
||||||
|
|
||||||
@ -28,9 +28,9 @@ export class SubsonicClient {
|
|||||||
url.searchParams.set("c", "subtracks-test-fixture");
|
url.searchParams.set("c", "subtracks-test-fixture");
|
||||||
|
|
||||||
if (params) {
|
if (params) {
|
||||||
for (const [key, value] of params) {
|
Object.entries(params).forEach(([key, value]) =>
|
||||||
url.searchParams.append(key, value);
|
url.searchParams.append(key, value)
|
||||||
}
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const res = await fetch(url);
|
const res = await fetch(url);
|
||||||
|
|||||||
@ -1,51 +0,0 @@
|
|||||||
import 'dart:convert';
|
|
||||||
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:fast_immutable_collections/fast_immutable_collections.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 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)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,745 +0,0 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:drift_flutter/drift_flutter.dart';
|
|
||||||
import 'package:path/path.dart' as path;
|
|
||||||
import 'package:path_provider/path_provider.dart';
|
|
||||||
|
|
||||||
import '../sources/models.dart' as models;
|
|
||||||
import 'converters.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([QueryExecutor? executor])
|
|
||||||
: super(executor ?? _openConnection());
|
|
||||||
|
|
||||||
static QueryExecutor _openConnection() {
|
|
||||||
return driftDatabase(
|
|
||||||
name: 'my_database',
|
|
||||||
native: DriftNativeOptions(
|
|
||||||
databasePath: () async {
|
|
||||||
final directory = await getApplicationSupportDirectory();
|
|
||||||
return path.join(directory.absolute.path, 'subtracks.sqlite');
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
int get schemaVersion => 1;
|
|
||||||
|
|
||||||
@override
|
|
||||||
MigrationStrategy get migration {
|
|
||||||
return MigrationStrategy(
|
|
||||||
beforeOpen: (details) async {
|
|
||||||
await customStatement('PRAGMA foreign_keys = ON');
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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);
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
extension ArtistToDb on models.Artist {
|
|
||||||
ArtistsCompanion toDb(int sourceId) => ArtistsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
starred: Value(starred),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension AlbumToDb on models.Album {
|
|
||||||
AlbumsCompanion toDb(int sourceId) => AlbumsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: id,
|
|
||||||
artistId: Value(artistId),
|
|
||||||
name: name,
|
|
||||||
albumArtist: Value(albumArtist),
|
|
||||||
created: created,
|
|
||||||
coverArt: Value(coverArt),
|
|
||||||
genre: Value(genre),
|
|
||||||
year: Value(year),
|
|
||||||
starred: Value(starred),
|
|
||||||
frequentRank: Value(frequentRank),
|
|
||||||
recentRank: Value(recentRank),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension SongToDb on models.Song {
|
|
||||||
SongsCompanion toDb(int sourceId) => SongsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: id,
|
|
||||||
albumId: Value(albumId),
|
|
||||||
artistId: Value(artistId),
|
|
||||||
title: title,
|
|
||||||
album: Value(album),
|
|
||||||
artist: Value(artist),
|
|
||||||
duration: Value(duration),
|
|
||||||
track: Value(track),
|
|
||||||
disc: Value(disc),
|
|
||||||
starred: Value(starred),
|
|
||||||
genre: Value(genre),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PlaylistToDb on models.Playlist {
|
|
||||||
PlaylistsCompanion toDb(int sourceId) => PlaylistsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: id,
|
|
||||||
name: name,
|
|
||||||
comment: Value(comment),
|
|
||||||
coverArt: Value(coverArt),
|
|
||||||
created: created,
|
|
||||||
changed: changed,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
extension PlaylistSongToDb on models.PlaylistSong {
|
|
||||||
PlaylistSongsCompanion toDb(int sourceId) => PlaylistSongsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
playlistId: playlistId,
|
|
||||||
songId: songId,
|
|
||||||
position: position,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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),
|
|
||||||
// );
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// }
|
|
||||||
File diff suppressed because it is too large
Load Diff
@ -1,553 +0,0 @@
|
|||||||
import 'converters.dart';
|
|
||||||
import '../sources/models.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,
|
|
||||||
is_active BOOLEAN UNIQUE,
|
|
||||||
created_at DATETIME NOT NULL DEFAULT (strftime('%s', CURRENT_TIMESTAMP))
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE subsonic_source_options(
|
|
||||||
source_id INT NOT NULL PRIMARY KEY,
|
|
||||||
address TEXT NOT NULL MAPPED BY `const UriConverter()`,
|
|
||||||
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,
|
|
||||||
starred DATETIME,
|
|
||||||
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,
|
|
||||||
frequent_rank INT,
|
|
||||||
recent_rank INT,
|
|
||||||
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,
|
|
||||||
created DATETIME NOT NULL,
|
|
||||||
changed DATETIME NOT NULL,
|
|
||||||
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,
|
|
||||||
PRIMARY KEY (source_id, playlist_id, position),
|
|
||||||
FOREIGN KEY (source_id) REFERENCES sources (id) ON DELETE CASCADE
|
|
||||||
) WITH PlaylistSong;
|
|
||||||
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,
|
|
||||||
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 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;
|
|
||||||
|
|
||||||
-- albumIdsWithDownloadStatus:
|
|
||||||
-- 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;
|
|
||||||
|
|
||||||
-- artistIdsWithDownloadStatus:
|
|
||||||
-- SELECT artists.id
|
|
||||||
-- 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
|
|
||||||
-- artists.source_id = :source_id
|
|
||||||
-- AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
|
||||||
-- GROUP BY artists.id;
|
|
||||||
|
|
||||||
-- playlistIdsWithDownloadStatus:
|
|
||||||
-- SELECT playlists.id
|
|
||||||
-- 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
|
|
||||||
-- playlists.source_id = :source_id
|
|
||||||
-- AND (songs.download_file_path IS NOT NULL OR songs.download_task_id IS NOT NULL)
|
|
||||||
-- GROUP BY playlists.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;
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'app/router.dart';
|
import 'router.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MainApp());
|
runApp(const MainApp());
|
||||||
|
|||||||
@ -1,140 +0,0 @@
|
|||||||
import 'package:async/async.dart';
|
|
||||||
import 'package:collection/collection.dart';
|
|
||||||
import 'package:drift/drift.dart';
|
|
||||||
|
|
||||||
import '../database/database.dart';
|
|
||||||
import '../sources/music_source.dart';
|
|
||||||
|
|
||||||
const kSliceSize = 200;
|
|
||||||
|
|
||||||
class SyncService {
|
|
||||||
SyncService({
|
|
||||||
required this.source,
|
|
||||||
required this.db,
|
|
||||||
required this.sourceId,
|
|
||||||
});
|
|
||||||
|
|
||||||
final MusicSource source;
|
|
||||||
final SubtracksDatabase db;
|
|
||||||
final int sourceId;
|
|
||||||
|
|
||||||
Future<void> sync() async {
|
|
||||||
await db.transaction(() async {
|
|
||||||
await Future.wait([
|
|
||||||
syncArtists(),
|
|
||||||
syncAlbums(),
|
|
||||||
syncSongs(),
|
|
||||||
syncPlaylists(),
|
|
||||||
syncPlaylistSongs(),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> syncArtists() async {
|
|
||||||
final sourceIds = <String>{};
|
|
||||||
|
|
||||||
await for (final slice in source.allArtists().slices(kSliceSize)) {
|
|
||||||
sourceIds.addAll(slice.map((e) => e.id));
|
|
||||||
|
|
||||||
await db.batch((batch) async {
|
|
||||||
batch.insertAllOnConflictUpdate(
|
|
||||||
db.artists,
|
|
||||||
slice.map((artist) => artist.toDb(sourceId)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var slice in sourceIds.slices(kSqliteMaxVariableNumber - 1)) {
|
|
||||||
await db.artists.deleteWhere(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(slice),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> syncAlbums() async {
|
|
||||||
final sourceIds = <String>{};
|
|
||||||
|
|
||||||
await for (final slice in source.allAlbums().slices(kSliceSize)) {
|
|
||||||
sourceIds.addAll(slice.map((e) => e.id));
|
|
||||||
|
|
||||||
await db.batch((batch) async {
|
|
||||||
batch.insertAllOnConflictUpdate(
|
|
||||||
db.albums,
|
|
||||||
slice.map((e) => e.toDb(sourceId)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var slice in sourceIds.slices(kSqliteMaxVariableNumber - 1)) {
|
|
||||||
await db.albums.deleteWhere(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(slice),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> syncSongs() async {
|
|
||||||
final sourceIds = <String>{};
|
|
||||||
|
|
||||||
await for (final slice in source.allSongs().slices(kSliceSize)) {
|
|
||||||
sourceIds.addAll(slice.map((e) => e.id));
|
|
||||||
|
|
||||||
await db.batch((batch) async {
|
|
||||||
batch.insertAllOnConflictUpdate(
|
|
||||||
db.songs,
|
|
||||||
slice.map((e) => e.toDb(sourceId)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var slice in sourceIds.slices(kSqliteMaxVariableNumber - 1)) {
|
|
||||||
await db.songs.deleteWhere(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(slice),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> syncPlaylists() async {
|
|
||||||
final sourceIds = <String>{};
|
|
||||||
|
|
||||||
await for (final slice in source.allPlaylists().slices(kSliceSize)) {
|
|
||||||
sourceIds.addAll(slice.map((e) => e.id));
|
|
||||||
|
|
||||||
await db.batch((batch) async {
|
|
||||||
batch.insertAllOnConflictUpdate(
|
|
||||||
db.playlists,
|
|
||||||
slice.map((e) => e.toDb(sourceId)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var slice in sourceIds.slices(kSqliteMaxVariableNumber - 1)) {
|
|
||||||
await db.playlists.deleteWhere(
|
|
||||||
(tbl) => tbl.sourceId.equals(sourceId) & tbl.id.isNotIn(slice),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Future<void> syncPlaylistSongs() async {
|
|
||||||
final sourceIds = <(String, String)>{};
|
|
||||||
|
|
||||||
await for (final slice in source.allPlaylistSongs().slices(kSliceSize)) {
|
|
||||||
sourceIds.addAll(slice.map((e) => (e.playlistId, e.songId)));
|
|
||||||
|
|
||||||
await db.batch((batch) async {
|
|
||||||
batch.insertAllOnConflictUpdate(
|
|
||||||
db.playlistSongs,
|
|
||||||
slice.map((e) => e.toDb(sourceId)),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var slice in sourceIds.slices((kSqliteMaxVariableNumber ~/ 2) - 1)) {
|
|
||||||
await db.playlistSongs.deleteWhere(
|
|
||||||
(tbl) =>
|
|
||||||
tbl.sourceId.equals(sourceId) &
|
|
||||||
tbl.playlistId.isNotIn(slice.map((e) => e.$1)) &
|
|
||||||
tbl.songId.isNotIn(slice.map((e) => e.$2)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,21 +1,32 @@
|
|||||||
|
// ignore_for_file: annotate_overrides
|
||||||
|
|
||||||
import 'package:freezed_annotation/freezed_annotation.dart';
|
import 'package:freezed_annotation/freezed_annotation.dart';
|
||||||
|
|
||||||
part 'models.freezed.dart';
|
part 'models.freezed.dart';
|
||||||
|
part 'models.g.dart';
|
||||||
|
|
||||||
|
mixin Starred {
|
||||||
|
DateTime? get starred;
|
||||||
|
}
|
||||||
|
|
||||||
|
mixin CoverArt {
|
||||||
|
String? get coverArt;
|
||||||
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class Artist with _$Artist {
|
abstract class SourceItem with _$SourceItem {
|
||||||
const factory Artist({
|
@With<Starred>()
|
||||||
|
const factory SourceItem.artist({
|
||||||
required String id,
|
required String id,
|
||||||
required String name,
|
required String name,
|
||||||
DateTime? starred,
|
DateTime? starred,
|
||||||
Uri? smallImage,
|
Uri? smallImage,
|
||||||
Uri? largeImage,
|
Uri? largeImage,
|
||||||
}) = _Artist;
|
}) = SourceArtist;
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@With<Starred>()
|
||||||
abstract class Album with _$Album {
|
@With<CoverArt>()
|
||||||
const factory Album({
|
const factory SourceItem.album({
|
||||||
required String id,
|
required String id,
|
||||||
String? artistId,
|
String? artistId,
|
||||||
required String name,
|
required String name,
|
||||||
@ -27,12 +38,10 @@ abstract class Album with _$Album {
|
|||||||
String? genre,
|
String? genre,
|
||||||
int? frequentRank,
|
int? frequentRank,
|
||||||
int? recentRank,
|
int? recentRank,
|
||||||
}) = _Album;
|
}) = SourceAlbum;
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@With<CoverArt>()
|
||||||
abstract class Playlist with _$Playlist {
|
const factory SourceItem.playlist({
|
||||||
const factory Playlist({
|
|
||||||
required String id,
|
required String id,
|
||||||
required String name,
|
required String name,
|
||||||
String? comment,
|
String? comment,
|
||||||
@ -41,12 +50,11 @@ abstract class Playlist with _$Playlist {
|
|||||||
String? coverArt,
|
String? coverArt,
|
||||||
String? owner,
|
String? owner,
|
||||||
bool? public,
|
bool? public,
|
||||||
}) = _Playlist;
|
}) = SourcePlaylist;
|
||||||
}
|
|
||||||
|
|
||||||
@freezed
|
@With<Starred>()
|
||||||
abstract class Song with _$Song {
|
@With<CoverArt>()
|
||||||
const factory Song({
|
const factory SourceItem.song({
|
||||||
required String id,
|
required String id,
|
||||||
String? albumId,
|
String? albumId,
|
||||||
String? artistId,
|
String? artistId,
|
||||||
@ -59,14 +67,20 @@ abstract class Song with _$Song {
|
|||||||
DateTime? starred,
|
DateTime? starred,
|
||||||
String? genre,
|
String? genre,
|
||||||
String? coverArt,
|
String? coverArt,
|
||||||
}) = _Song;
|
}) = SourceSong;
|
||||||
|
|
||||||
|
factory SourceItem.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SourceItemFromJson(json);
|
||||||
}
|
}
|
||||||
|
|
||||||
@freezed
|
@freezed
|
||||||
abstract class PlaylistSong with _$PlaylistSong {
|
abstract class SourcePlaylistSong with _$SourcePlaylistSong {
|
||||||
const factory PlaylistSong({
|
const factory SourcePlaylistSong({
|
||||||
required String playlistId,
|
required String playlistId,
|
||||||
required String songId,
|
required String songId,
|
||||||
required int position,
|
required int position,
|
||||||
}) = _PlaylistSong;
|
}) = _SourcePlaylistSong;
|
||||||
|
|
||||||
|
factory SourcePlaylistSong.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$SourcePlaylistSongFromJson(json);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
142
lib/sources/models.g.dart
Normal file
142
lib/sources/models.g.dart
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'models.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
SourceArtist _$SourceArtistFromJson(Map<String, dynamic> json) => SourceArtist(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
starred: json['starred'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['starred'] as String),
|
||||||
|
smallImage: json['smallImage'] == null
|
||||||
|
? null
|
||||||
|
: Uri.parse(json['smallImage'] as String),
|
||||||
|
largeImage: json['largeImage'] == null
|
||||||
|
? null
|
||||||
|
: Uri.parse(json['largeImage'] as String),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SourceArtistToJson(SourceArtist instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'starred': instance.starred?.toIso8601String(),
|
||||||
|
'smallImage': instance.smallImage?.toString(),
|
||||||
|
'largeImage': instance.largeImage?.toString(),
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
SourceAlbum _$SourceAlbumFromJson(Map<String, dynamic> json) => SourceAlbum(
|
||||||
|
id: json['id'] as String,
|
||||||
|
artistId: json['artistId'] as String?,
|
||||||
|
name: json['name'] as String,
|
||||||
|
albumArtist: json['albumArtist'] as String?,
|
||||||
|
created: DateTime.parse(json['created'] as String),
|
||||||
|
coverArt: json['coverArt'] as String?,
|
||||||
|
year: (json['year'] as num?)?.toInt(),
|
||||||
|
starred: json['starred'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['starred'] as String),
|
||||||
|
genre: json['genre'] as String?,
|
||||||
|
frequentRank: (json['frequentRank'] as num?)?.toInt(),
|
||||||
|
recentRank: (json['recentRank'] as num?)?.toInt(),
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SourceAlbumToJson(SourceAlbum instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'artistId': instance.artistId,
|
||||||
|
'name': instance.name,
|
||||||
|
'albumArtist': instance.albumArtist,
|
||||||
|
'created': instance.created.toIso8601String(),
|
||||||
|
'coverArt': instance.coverArt,
|
||||||
|
'year': instance.year,
|
||||||
|
'starred': instance.starred?.toIso8601String(),
|
||||||
|
'genre': instance.genre,
|
||||||
|
'frequentRank': instance.frequentRank,
|
||||||
|
'recentRank': instance.recentRank,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
SourcePlaylist _$SourcePlaylistFromJson(Map<String, dynamic> json) =>
|
||||||
|
SourcePlaylist(
|
||||||
|
id: json['id'] as String,
|
||||||
|
name: json['name'] as String,
|
||||||
|
comment: json['comment'] as String?,
|
||||||
|
created: DateTime.parse(json['created'] as String),
|
||||||
|
changed: DateTime.parse(json['changed'] as String),
|
||||||
|
coverArt: json['coverArt'] as String?,
|
||||||
|
owner: json['owner'] as String?,
|
||||||
|
public: json['public'] as bool?,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SourcePlaylistToJson(SourcePlaylist instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'name': instance.name,
|
||||||
|
'comment': instance.comment,
|
||||||
|
'created': instance.created.toIso8601String(),
|
||||||
|
'changed': instance.changed.toIso8601String(),
|
||||||
|
'coverArt': instance.coverArt,
|
||||||
|
'owner': instance.owner,
|
||||||
|
'public': instance.public,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
SourceSong _$SourceSongFromJson(Map<String, dynamic> json) => SourceSong(
|
||||||
|
id: json['id'] as String,
|
||||||
|
albumId: json['albumId'] as String?,
|
||||||
|
artistId: json['artistId'] as String?,
|
||||||
|
title: json['title'] as String,
|
||||||
|
artist: json['artist'] as String?,
|
||||||
|
album: json['album'] as String?,
|
||||||
|
duration: json['duration'] == null
|
||||||
|
? null
|
||||||
|
: Duration(microseconds: (json['duration'] as num).toInt()),
|
||||||
|
track: (json['track'] as num?)?.toInt(),
|
||||||
|
disc: (json['disc'] as num?)?.toInt(),
|
||||||
|
starred: json['starred'] == null
|
||||||
|
? null
|
||||||
|
: DateTime.parse(json['starred'] as String),
|
||||||
|
genre: json['genre'] as String?,
|
||||||
|
coverArt: json['coverArt'] as String?,
|
||||||
|
$type: json['runtimeType'] as String?,
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SourceSongToJson(SourceSong instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'albumId': instance.albumId,
|
||||||
|
'artistId': instance.artistId,
|
||||||
|
'title': instance.title,
|
||||||
|
'artist': instance.artist,
|
||||||
|
'album': instance.album,
|
||||||
|
'duration': instance.duration?.inMicroseconds,
|
||||||
|
'track': instance.track,
|
||||||
|
'disc': instance.disc,
|
||||||
|
'starred': instance.starred?.toIso8601String(),
|
||||||
|
'genre': instance.genre,
|
||||||
|
'coverArt': instance.coverArt,
|
||||||
|
'runtimeType': instance.$type,
|
||||||
|
};
|
||||||
|
|
||||||
|
_SourcePlaylistSong _$SourcePlaylistSongFromJson(Map<String, dynamic> json) =>
|
||||||
|
_SourcePlaylistSong(
|
||||||
|
playlistId: json['playlistId'] as String,
|
||||||
|
songId: json['songId'] as String,
|
||||||
|
position: (json['position'] as num).toInt(),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$SourcePlaylistSongToJson(_SourcePlaylistSong instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'playlistId': instance.playlistId,
|
||||||
|
'songId': instance.songId,
|
||||||
|
'position': instance.position,
|
||||||
|
};
|
||||||
@ -3,11 +3,11 @@ import 'models.dart';
|
|||||||
abstract class MusicSource {
|
abstract class MusicSource {
|
||||||
Future<void> ping();
|
Future<void> ping();
|
||||||
|
|
||||||
Stream<Album> allAlbums();
|
Stream<SourceAlbum> allAlbums();
|
||||||
Stream<Artist> allArtists();
|
Stream<SourceArtist> allArtists();
|
||||||
Stream<Playlist> allPlaylists();
|
Stream<SourcePlaylist> allPlaylists();
|
||||||
Stream<Song> allSongs();
|
Stream<SourceSong> allSongs();
|
||||||
Stream<PlaylistSong> allPlaylistSongs();
|
Stream<SourcePlaylistSong> allPlaylistSongs();
|
||||||
|
|
||||||
Uri streamUri(String songId);
|
Uri streamUri(String songId);
|
||||||
Uri downloadUri(String songId);
|
Uri downloadUri(String songId);
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import 'package:xml/xml.dart';
|
|||||||
|
|
||||||
import '../models.dart';
|
import '../models.dart';
|
||||||
|
|
||||||
Artist mapArtist(XmlElement e, XmlElement? info) => Artist(
|
SourceArtist mapArtist(XmlElement e, XmlElement? info) => SourceArtist(
|
||||||
id: e.getAttribute('id')!,
|
id: e.getAttribute('id')!,
|
||||||
name: e.getAttribute('name')!,
|
name: e.getAttribute('name')!,
|
||||||
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
starred: DateTime.tryParse(e.getAttribute('starred').toString()),
|
||||||
@ -10,11 +10,11 @@ Artist mapArtist(XmlElement e, XmlElement? info) => Artist(
|
|||||||
largeImage: Uri.tryParse(info?.getElement('largeImageUrl')?.innerText ?? ''),
|
largeImage: Uri.tryParse(info?.getElement('largeImageUrl')?.innerText ?? ''),
|
||||||
);
|
);
|
||||||
|
|
||||||
Album mapAlbum(
|
SourceAlbum mapAlbum(
|
||||||
XmlElement e, {
|
XmlElement e, {
|
||||||
int? frequentRank,
|
int? frequentRank,
|
||||||
int? recentRank,
|
int? recentRank,
|
||||||
}) => Album(
|
}) => SourceAlbum(
|
||||||
id: e.getAttribute('id')!,
|
id: e.getAttribute('id')!,
|
||||||
artistId: e.getAttribute('artistId'),
|
artistId: e.getAttribute('artistId'),
|
||||||
name: e.getAttribute('name')!,
|
name: e.getAttribute('name')!,
|
||||||
@ -28,7 +28,7 @@ Album mapAlbum(
|
|||||||
recentRank: recentRank,
|
recentRank: recentRank,
|
||||||
);
|
);
|
||||||
|
|
||||||
Playlist mapPlaylist(XmlElement e) => Playlist(
|
SourcePlaylist mapPlaylist(XmlElement e) => SourcePlaylist(
|
||||||
id: e.getAttribute('id')!,
|
id: e.getAttribute('id')!,
|
||||||
name: e.getAttribute('name')!,
|
name: e.getAttribute('name')!,
|
||||||
comment: e.getAttribute('comment'),
|
comment: e.getAttribute('comment'),
|
||||||
@ -36,9 +36,10 @@ Playlist mapPlaylist(XmlElement e) => Playlist(
|
|||||||
created: DateTime.parse(e.getAttribute('created')!),
|
created: DateTime.parse(e.getAttribute('created')!),
|
||||||
changed: DateTime.parse(e.getAttribute('changed')!),
|
changed: DateTime.parse(e.getAttribute('changed')!),
|
||||||
owner: e.getAttribute('owner'),
|
owner: e.getAttribute('owner'),
|
||||||
|
public: bool.tryParse(e.getAttribute('public').toString()),
|
||||||
);
|
);
|
||||||
|
|
||||||
Song mapSong(XmlElement e) => Song(
|
SourceSong mapSong(XmlElement e) => SourceSong(
|
||||||
id: e.getAttribute('id')!,
|
id: e.getAttribute('id')!,
|
||||||
albumId: e.getAttribute('albumId'),
|
albumId: e.getAttribute('albumId'),
|
||||||
artistId: e.getAttribute('artistId'),
|
artistId: e.getAttribute('artistId'),
|
||||||
@ -56,10 +57,10 @@ Song mapSong(XmlElement e) => Song(
|
|||||||
genre: e.getAttribute('genre'),
|
genre: e.getAttribute('genre'),
|
||||||
);
|
);
|
||||||
|
|
||||||
PlaylistSong mapPlaylistSong(
|
SourcePlaylistSong mapPlaylistSong(
|
||||||
int index,
|
int index,
|
||||||
XmlElement e,
|
XmlElement e,
|
||||||
) => PlaylistSong(
|
) => SourcePlaylistSong(
|
||||||
playlistId: e.parentElement!.getAttribute('id')!,
|
playlistId: e.parentElement!.getAttribute('id')!,
|
||||||
songId: e.getAttribute('id')!,
|
songId: e.getAttribute('id')!,
|
||||||
position: index,
|
position: index,
|
||||||
|
|||||||
@ -41,7 +41,7 @@ class SubsonicSource implements MusicSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Artist> allArtists() async* {
|
Stream<SourceArtist> allArtists() async* {
|
||||||
final getArtistsRes = await _pool.withResource(
|
final getArtistsRes = await _pool.withResource(
|
||||||
() => client.get('getArtists'),
|
() => client.get('getArtists'),
|
||||||
);
|
);
|
||||||
@ -58,7 +58,7 @@ class SubsonicSource implements MusicSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Album> allAlbums() async* {
|
Stream<SourceAlbum> allAlbums() async* {
|
||||||
final extras = await Future.wait([
|
final extras = await Future.wait([
|
||||||
_albumList(
|
_albumList(
|
||||||
'frequent',
|
'frequent',
|
||||||
@ -89,7 +89,7 @@ class SubsonicSource implements MusicSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Playlist> allPlaylists() async* {
|
Stream<SourcePlaylist> allPlaylists() async* {
|
||||||
final res = await _pool.withResource(() => client.get('getPlaylists'));
|
final res = await _pool.withResource(() => client.get('getPlaylists'));
|
||||||
|
|
||||||
yield* Stream.fromIterable(
|
yield* Stream.fromIterable(
|
||||||
@ -98,7 +98,7 @@ class SubsonicSource implements MusicSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<PlaylistSong> allPlaylistSongs() async* {
|
Stream<SourcePlaylistSong> allPlaylistSongs() async* {
|
||||||
final allPlaylists = await _pool.withResource(
|
final allPlaylists = await _pool.withResource(
|
||||||
() => client.get('getPlaylists'),
|
() => client.get('getPlaylists'),
|
||||||
);
|
);
|
||||||
@ -116,7 +116,7 @@ class SubsonicSource implements MusicSource {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Stream<Song> allSongs() async* {
|
Stream<SourceSong> allSongs() async* {
|
||||||
if (await supportsFastSongSync) {
|
if (await supportsFastSongSync) {
|
||||||
await for (var songs in _songSearch()) {
|
await for (var songs in _songSearch()) {
|
||||||
yield* Stream.fromIterable(songs.map(mapSong));
|
yield* Stream.fromIterable(songs.map(mapSong));
|
||||||
|
|||||||
@ -1,7 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -e
|
|
||||||
|
|
||||||
rm -rf ./music
|
|
||||||
|
|
||||||
docker compose build
|
|
||||||
docker compose run --rm library-manager music-download.ts
|
|
||||||
@ -5,6 +5,8 @@ docker compose build
|
|||||||
docker compose down
|
docker compose down
|
||||||
docker volume rm $(docker compose volumes -q) || true
|
docker volume rm $(docker compose volumes -q) || true
|
||||||
|
|
||||||
|
docker compose run --rm library-manager music-download.ts
|
||||||
|
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
echo "waiting for library scans..."
|
echo "waiting for library scans..."
|
||||||
sleep 10
|
sleep 10
|
||||||
|
|||||||
78
pubspec.lock
78
pubspec.lock
@ -42,7 +42,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.7.0"
|
version: "2.7.0"
|
||||||
async:
|
async:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
||||||
@ -161,14 +161,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
charcode:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: charcode
|
|
||||||
sha256: fb0f1107cac15a5ea6ef0a6ef71a807b9e4267c713bb93e00e92d737cc8dbd8a
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "1.4.0"
|
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -289,30 +281,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
version: "3.1.1"
|
||||||
drift:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: drift
|
|
||||||
sha256: "83290a32ae006a7535c5ecf300722cb77177250d9df4ee2becc5fa8a36095114"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.29.0"
|
|
||||||
drift_dev:
|
|
||||||
dependency: "direct dev"
|
|
||||||
description:
|
|
||||||
name: drift_dev
|
|
||||||
sha256: "6019f827544e77524ffd5134ae0cb75dfd92ef5ef3e269872af92840c929cd43"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.29.0"
|
|
||||||
drift_flutter:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: drift_flutter
|
|
||||||
sha256: b7534bf320aac5213259aac120670ba67b63a1fd010505babc436ff86083818f
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.2.7"
|
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -321,14 +289,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.3.3"
|
version: "1.3.3"
|
||||||
fast_immutable_collections:
|
|
||||||
dependency: "direct main"
|
|
||||||
description:
|
|
||||||
name: fast_immutable_collections
|
|
||||||
sha256: "19f70498af299cbce5ff919dbbecd5abfd9d0c28139004f68d3810ce23dedfb3"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "11.1.0"
|
|
||||||
ffi:
|
ffi:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -641,7 +601,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.0"
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path
|
name: path
|
||||||
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
@ -649,7 +609,7 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "1.9.1"
|
version: "1.9.1"
|
||||||
path_provider:
|
path_provider:
|
||||||
dependency: "direct main"
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
||||||
@ -744,14 +704,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
recase:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: recase
|
|
||||||
sha256: e4eb4ec2dcdee52dcf99cb4ceabaffc631d7424ee55e56f280bc039737f89213
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "4.1.0"
|
|
||||||
riverpod:
|
riverpod:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@ -917,30 +869,6 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.0"
|
version: "2.4.0"
|
||||||
sqlite3:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sqlite3
|
|
||||||
sha256: "3145bd74dcdb4fd6f5c6dda4d4e4490a8087d7f286a14dee5d37087290f0f8a2"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "2.9.4"
|
|
||||||
sqlite3_flutter_libs:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sqlite3_flutter_libs
|
|
||||||
sha256: "69c80d812ef2500202ebd22002cbfc1b6565e9ff56b2f971e757fac5d42294df"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.5.40"
|
|
||||||
sqlparser:
|
|
||||||
dependency: transitive
|
|
||||||
description:
|
|
||||||
name: sqlparser
|
|
||||||
sha256: "54eea43e36dd3769274c3108625f9ea1a382f8d2ac8b16f3e4589d9bd9b0e16c"
|
|
||||||
url: "https://pub.dev"
|
|
||||||
source: hosted
|
|
||||||
version: "0.42.0"
|
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|||||||
@ -7,13 +7,9 @@ environment:
|
|||||||
sdk: ^3.9.2
|
sdk: ^3.9.2
|
||||||
|
|
||||||
dependencies:
|
dependencies:
|
||||||
async: ^2.13.0
|
|
||||||
cached_network_image: ^3.4.1
|
cached_network_image: ^3.4.1
|
||||||
collection: ^1.19.1
|
collection: ^1.19.1
|
||||||
crypto: ^3.0.6
|
crypto: ^3.0.6
|
||||||
drift: ^2.29.0
|
|
||||||
drift_flutter: ^0.2.7
|
|
||||||
fast_immutable_collections: ^11.1.0
|
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_hooks: ^0.21.3+1
|
flutter_hooks: ^0.21.3+1
|
||||||
@ -24,8 +20,6 @@ dependencies:
|
|||||||
infinite_scroll_pagination: ^5.1.1
|
infinite_scroll_pagination: ^5.1.1
|
||||||
json_annotation: ^4.9.0
|
json_annotation: ^4.9.0
|
||||||
material_symbols_icons: ^4.2874.0
|
material_symbols_icons: ^4.2874.0
|
||||||
path: ^1.9.1
|
|
||||||
path_provider: ^2.1.5
|
|
||||||
pool: ^1.5.2
|
pool: ^1.5.2
|
||||||
xml: ^6.6.1
|
xml: ^6.6.1
|
||||||
|
|
||||||
@ -39,7 +33,6 @@ dev_dependencies:
|
|||||||
freezed: ^3.2.3
|
freezed: ^3.2.3
|
||||||
json_serializable: ^6.11.1
|
json_serializable: ^6.11.1
|
||||||
test: ^1.26.2
|
test: ^1.26.2
|
||||||
drift_dev: ^2.29.0
|
|
||||||
|
|
||||||
flutter:
|
flutter:
|
||||||
uses-material-design: true
|
uses-material-design: true
|
||||||
|
|||||||
@ -1,290 +0,0 @@
|
|||||||
import 'package:subtracks/database/database.dart';
|
|
||||||
import 'package:subtracks/services/sync_services.dart';
|
|
||||||
import 'package:subtracks/sources/subsonic/source.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
import '../util/database.dart';
|
|
||||||
import '../util/subsonic.dart';
|
|
||||||
|
|
||||||
void main() {
|
|
||||||
late SubtracksDatabase db;
|
|
||||||
late SubsonicSource source;
|
|
||||||
late int sourceId;
|
|
||||||
late int sourceIdOther;
|
|
||||||
|
|
||||||
late SyncService sync;
|
|
||||||
|
|
||||||
setUp(() async {
|
|
||||||
db = testDatabase();
|
|
||||||
source = SubsonicSource(testServerClients()[Servers.navidrome]!);
|
|
||||||
sourceId = await db
|
|
||||||
.into(db.sources)
|
|
||||||
.insert(SourcesCompanion.insert(name: 'navidrome'));
|
|
||||||
sourceIdOther = await db
|
|
||||||
.into(db.sources)
|
|
||||||
.insert(SourcesCompanion.insert(name: 'other'));
|
|
||||||
|
|
||||||
sync = SyncService(
|
|
||||||
db: db,
|
|
||||||
source: source,
|
|
||||||
sourceId: sourceId,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
tearDown(() async {
|
|
||||||
await db.close();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncArtists', () async {
|
|
||||||
await db
|
|
||||||
.into(db.artists)
|
|
||||||
.insert(
|
|
||||||
ArtistsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: 'shouldBeDeleted',
|
|
||||||
name: 'shouldBeDeleted',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.artists)
|
|
||||||
.insert(
|
|
||||||
ArtistsCompanion.insert(
|
|
||||||
sourceId: sourceIdOther,
|
|
||||||
id: 'shouldBeKept',
|
|
||||||
name: 'shouldBeKept',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sync.syncArtists();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await db.managers.artists
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(2),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.artists
|
|
||||||
.filter((f) => f.id.equals('shouldBeDeleted'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNull,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.artists
|
|
||||||
.filter((f) => f.id.equals('shouldBeKept'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNotNull,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncAlbums', () async {
|
|
||||||
await db
|
|
||||||
.into(db.albums)
|
|
||||||
.insert(
|
|
||||||
AlbumsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: 'shouldBeDeleted',
|
|
||||||
name: 'shouldBeDeleted',
|
|
||||||
created: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.albums)
|
|
||||||
.insert(
|
|
||||||
AlbumsCompanion.insert(
|
|
||||||
sourceId: sourceIdOther,
|
|
||||||
id: 'shouldBeKept',
|
|
||||||
name: 'shouldBeKept',
|
|
||||||
created: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sync.syncAlbums();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await db.managers.albums
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(3),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.albums
|
|
||||||
.filter((f) => f.id.equals('shouldBeDeleted'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNull,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.albums
|
|
||||||
.filter((f) => f.id.equals('shouldBeKept'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNotNull,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncSongs', () async {
|
|
||||||
await db
|
|
||||||
.into(db.songs)
|
|
||||||
.insert(
|
|
||||||
SongsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: 'shouldBeDeleted',
|
|
||||||
title: 'shouldBeDeleted',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.songs)
|
|
||||||
.insert(
|
|
||||||
SongsCompanion.insert(
|
|
||||||
sourceId: sourceIdOther,
|
|
||||||
id: 'shouldBeKept',
|
|
||||||
title: 'shouldBeKept',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sync.syncSongs();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await db.managers.songs
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(20),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.songs
|
|
||||||
.filter((f) => f.id.equals('shouldBeDeleted'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNull,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.songs
|
|
||||||
.filter((f) => f.id.equals('shouldBeKept'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNotNull,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncPlaylists', () async {
|
|
||||||
await db
|
|
||||||
.into(db.playlists)
|
|
||||||
.insert(
|
|
||||||
PlaylistsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
id: 'shouldBeDeleted',
|
|
||||||
name: 'shouldBeDeleted',
|
|
||||||
created: DateTime.now(),
|
|
||||||
changed: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.playlists)
|
|
||||||
.insert(
|
|
||||||
PlaylistsCompanion.insert(
|
|
||||||
sourceId: sourceIdOther,
|
|
||||||
id: 'shouldBeKept',
|
|
||||||
name: 'shouldBeKept',
|
|
||||||
created: DateTime.now(),
|
|
||||||
changed: DateTime.now(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sync.syncPlaylists();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await db.managers.playlists
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(1),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.playlists
|
|
||||||
.filter((f) => f.id.equals('shouldBeDeleted'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNull,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.playlists
|
|
||||||
.filter((f) => f.id.equals('shouldBeKept'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNotNull,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncPlaylistSongs', () async {
|
|
||||||
await db
|
|
||||||
.into(db.playlistSongs)
|
|
||||||
.insert(
|
|
||||||
PlaylistSongsCompanion.insert(
|
|
||||||
sourceId: sourceId,
|
|
||||||
playlistId: 'shouldBeDeleted',
|
|
||||||
songId: 'shouldBeDeleted',
|
|
||||||
position: 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
await db
|
|
||||||
.into(db.playlistSongs)
|
|
||||||
.insert(
|
|
||||||
PlaylistSongsCompanion.insert(
|
|
||||||
sourceId: sourceIdOther,
|
|
||||||
playlistId: 'shouldBeKept',
|
|
||||||
songId: 'shouldBeKept',
|
|
||||||
position: 1,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sync.syncPlaylistSongs();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await db.managers.playlistSongs
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(7),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.playlistSongs
|
|
||||||
.filter((f) => f.playlistId.equals('shouldBeDeleted'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNull,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.playlistSongs
|
|
||||||
.filter((f) => f.playlistId.equals('shouldBeKept'))
|
|
||||||
.getSingleOrNull(),
|
|
||||||
isNotNull,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('syncPlaylistSongs', () async {
|
|
||||||
await sync.sync();
|
|
||||||
|
|
||||||
expect(
|
|
||||||
await db.managers.artists
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(2),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.albums
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(3),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.songs
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(20),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.playlists
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(1),
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
await db.managers.playlistSongs
|
|
||||||
.filter((f) => f.sourceId.equals(sourceId))
|
|
||||||
.count(),
|
|
||||||
equals(7),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
@ -1,13 +1,50 @@
|
|||||||
import 'package:collection/collection.dart';
|
import 'package:collection/collection.dart';
|
||||||
|
import 'package:http/http.dart';
|
||||||
|
import 'package:subtracks/sources/subsonic/client.dart';
|
||||||
import 'package:subtracks/sources/subsonic/source.dart';
|
import 'package:subtracks/sources/subsonic/source.dart';
|
||||||
import 'package:test/test.dart';
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
import '../util/subsonic.dart';
|
class TestHttpClient extends BaseClient {
|
||||||
|
@override
|
||||||
|
Future<StreamedResponse> send(BaseRequest request) => request.send();
|
||||||
|
}
|
||||||
|
|
||||||
|
class Server {
|
||||||
|
Server({
|
||||||
|
required this.name,
|
||||||
|
required this.client,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String name;
|
||||||
|
final SubsonicClient client;
|
||||||
|
}
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
groupByTestServer((client) {
|
|
||||||
late SubsonicSource source;
|
late SubsonicSource source;
|
||||||
|
|
||||||
|
final clients = [
|
||||||
|
Server(
|
||||||
|
name: 'navidrome',
|
||||||
|
client: SubsonicClient(
|
||||||
|
http: TestHttpClient(),
|
||||||
|
address: Uri.parse('http://localhost:4533/'),
|
||||||
|
username: 'admin',
|
||||||
|
password: 'password',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Server(
|
||||||
|
name: 'gonic',
|
||||||
|
client: SubsonicClient(
|
||||||
|
http: TestHttpClient(),
|
||||||
|
address: Uri.parse('http://localhost:4747/'),
|
||||||
|
username: 'admin',
|
||||||
|
password: 'admin',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
for (final Server(:name, :client) in clients) {
|
||||||
|
group(name, () {
|
||||||
setUp(() async {
|
setUp(() async {
|
||||||
source = SubsonicSource(client);
|
source = SubsonicSource(client);
|
||||||
});
|
});
|
||||||
@ -64,13 +101,13 @@ void main() {
|
|||||||
test('allPlaylists', () async {
|
test('allPlaylists', () async {
|
||||||
final items = await source.allPlaylists().toList();
|
final items = await source.allPlaylists().toList();
|
||||||
|
|
||||||
expect(items.length, equals(1));
|
expect(items.length, equals(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('allPlaylistSongs', () async {
|
test('allPlaylistSongs', () async {
|
||||||
final items = await source.allPlaylistSongs().toList();
|
final items = await source.allPlaylistSongs().toList();
|
||||||
|
|
||||||
expect(items.length, equals(7));
|
expect(items.length, equals(0));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('album-artist relation', () async {
|
test('album-artist relation', () async {
|
||||||
@ -106,4 +143,5 @@ void main() {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import 'package:drift/drift.dart';
|
|
||||||
import 'package:drift/native.dart';
|
|
||||||
import 'package:subtracks/database/database.dart';
|
|
||||||
|
|
||||||
SubtracksDatabase testDatabase() => SubtracksDatabase(
|
|
||||||
DatabaseConnection(
|
|
||||||
NativeDatabase.memory(),
|
|
||||||
closeStreamsSynchronously: true,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
@ -1,6 +0,0 @@
|
|||||||
import 'package:http/http.dart';
|
|
||||||
|
|
||||||
class TestHttpClient extends BaseClient {
|
|
||||||
@override
|
|
||||||
Future<StreamedResponse> send(BaseRequest request) => request.send();
|
|
||||||
}
|
|
||||||
@ -1,34 +0,0 @@
|
|||||||
import 'package:subtracks/sources/subsonic/client.dart';
|
|
||||||
import 'package:test/test.dart';
|
|
||||||
|
|
||||||
import 'http.dart';
|
|
||||||
|
|
||||||
enum Servers {
|
|
||||||
navidrome,
|
|
||||||
gonic,
|
|
||||||
}
|
|
||||||
|
|
||||||
Map<Servers, SubsonicClient> testServerClients() => {
|
|
||||||
Servers.navidrome: SubsonicClient(
|
|
||||||
http: TestHttpClient(),
|
|
||||||
address: Uri.parse('http://localhost:4533/'),
|
|
||||||
username: 'admin',
|
|
||||||
password: 'password',
|
|
||||||
),
|
|
||||||
Servers.gonic: SubsonicClient(
|
|
||||||
http: TestHttpClient(),
|
|
||||||
address: Uri.parse('http://localhost:4747/'),
|
|
||||||
username: 'admin',
|
|
||||||
password: 'admin',
|
|
||||||
),
|
|
||||||
};
|
|
||||||
|
|
||||||
void groupByTestServer(void Function(SubsonicClient client) callback) {
|
|
||||||
final clients = testServerClients();
|
|
||||||
|
|
||||||
for (final MapEntry(key: server, value: client) in clients.entries) {
|
|
||||||
group(server.name, () {
|
|
||||||
callback(client);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
x
Reference in New Issue
Block a user