2020-03-24 22:17:03 +01:00
|
|
|
import 'dart:io';
|
2020-06-01 10:49:33 +02:00
|
|
|
import 'dart:isolate';
|
2020-03-24 22:17:03 +01:00
|
|
|
|
2020-06-01 10:49:33 +02:00
|
|
|
import 'package:moor/isolate.dart';
|
2020-03-24 22:17:03 +01:00
|
|
|
import 'package:moor/moor.dart';
|
|
|
|
import 'package:moor_ffi/moor_ffi.dart';
|
|
|
|
import 'package:path/path.dart' as p;
|
|
|
|
import 'package:path_provider/path_provider.dart';
|
|
|
|
|
|
|
|
import '../models/album_model.dart';
|
2020-04-11 14:27:49 +02:00
|
|
|
import '../models/song_model.dart';
|
2020-03-24 22:17:03 +01:00
|
|
|
import 'music_data_source_contract.dart';
|
|
|
|
|
|
|
|
part 'moor_music_data_source.g.dart';
|
|
|
|
|
2020-06-07 18:22:10 +02:00
|
|
|
const String MOOR_ISOLATE = 'MOOR_ISOLATE';
|
|
|
|
|
2020-03-28 09:38:52 +01:00
|
|
|
@DataClassName('MoorAlbum')
|
2020-03-24 22:17:03 +01:00
|
|
|
class Albums extends Table {
|
2020-06-28 10:40:53 +02:00
|
|
|
IntColumn get id => integer().autoIncrement()();
|
2020-03-24 22:17:03 +01:00
|
|
|
TextColumn get title => text()();
|
|
|
|
TextColumn get artist => text()();
|
|
|
|
TextColumn get albumArtPath => text().nullable()();
|
|
|
|
IntColumn get year => integer().nullable()();
|
2020-06-28 10:40:53 +02:00
|
|
|
BoolColumn get present => boolean().withDefault(const Constant(true))();
|
2020-03-24 22:17:03 +01:00
|
|
|
|
|
|
|
@override
|
2020-06-28 10:40:53 +02:00
|
|
|
Set<Column> get primaryKey => {id};
|
2020-03-24 22:17:03 +01:00
|
|
|
}
|
|
|
|
|
2020-03-30 22:25:57 +02:00
|
|
|
@DataClassName('MoorSong')
|
|
|
|
class Songs extends Table {
|
|
|
|
TextColumn get title => text()();
|
2020-06-28 10:40:53 +02:00
|
|
|
TextColumn get albumTitle => text()();
|
|
|
|
IntColumn get albumId => integer()();
|
2020-03-30 22:25:57 +02:00
|
|
|
TextColumn get artist => text()();
|
|
|
|
TextColumn get path => text()();
|
2020-04-11 14:27:49 +02:00
|
|
|
IntColumn get duration => integer().nullable()();
|
2020-03-30 22:25:57 +02:00
|
|
|
TextColumn get albumArtPath => text().nullable()();
|
|
|
|
IntColumn get trackNumber => integer().nullable()();
|
2020-06-28 10:40:53 +02:00
|
|
|
BoolColumn get present => boolean().withDefault(const Constant(true))();
|
|
|
|
|
|
|
|
@override
|
|
|
|
Set<Column> get primaryKey => {path};
|
2020-03-30 22:25:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
@UseMoor(tables: [Albums, Songs])
|
2020-03-24 22:17:03 +01:00
|
|
|
class MoorMusicDataSource extends _$MoorMusicDataSource
|
|
|
|
implements MusicDataSource {
|
2020-06-01 10:49:33 +02:00
|
|
|
/// Use MoorMusicDataSource in main isolate only.
|
2020-03-24 22:17:03 +01:00
|
|
|
MoorMusicDataSource() : super(_openConnection());
|
2020-06-28 10:40:53 +02:00
|
|
|
|
2020-06-01 10:49:33 +02:00
|
|
|
/// Used for testing with in-memory database.
|
2020-03-28 09:38:52 +01:00
|
|
|
MoorMusicDataSource.withQueryExecutor(QueryExecutor e) : super(e);
|
2020-06-28 10:40:53 +02:00
|
|
|
|
2020-06-01 10:49:33 +02:00
|
|
|
/// Used to connect to a database on another isolate.
|
2020-06-28 10:40:53 +02:00
|
|
|
MoorMusicDataSource.connect(DatabaseConnection connection)
|
|
|
|
: super.connect(connection);
|
2020-03-24 22:17:03 +01:00
|
|
|
|
|
|
|
@override
|
|
|
|
int get schemaVersion => 1;
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<List<AlbumModel>> getAlbums() async {
|
|
|
|
return select(albums).get().then((moorAlbumList) => moorAlbumList
|
2020-03-30 22:25:57 +02:00
|
|
|
.map((moorAlbum) => AlbumModel.fromMoorAlbum(moorAlbum))
|
2020-03-24 22:17:03 +01:00
|
|
|
.toList());
|
|
|
|
}
|
|
|
|
|
2020-03-28 09:38:52 +01:00
|
|
|
// TODO: insert can throw exception -> implications?
|
2020-03-24 22:17:03 +01:00
|
|
|
@override
|
2020-06-28 10:40:53 +02:00
|
|
|
Future<int> insertAlbum(AlbumModel albumModel) async {
|
|
|
|
return await into(albums).insert(albumModel.toAlbumsCompanion());
|
2020-03-24 22:17:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
2020-03-28 09:38:52 +01:00
|
|
|
Future<bool> albumExists(AlbumModel albumModel) async {
|
|
|
|
final List<AlbumModel> albumList = await getAlbums();
|
|
|
|
return albumList.contains(albumModel);
|
2020-03-24 22:17:03 +01:00
|
|
|
}
|
2020-03-30 22:25:57 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
Future<List<SongModel>> getSongs() {
|
2020-04-03 13:55:15 +02:00
|
|
|
return select(songs).get().then((moorSongList) => moorSongList
|
|
|
|
.map((moorSong) => SongModel.fromMoorSong(moorSong))
|
|
|
|
.toList());
|
|
|
|
}
|
|
|
|
|
2020-06-28 10:40:53 +02:00
|
|
|
@override
|
2020-04-16 08:04:34 +02:00
|
|
|
Future<List<SongModel>> getSongsFromAlbum(AlbumModel album) {
|
2020-06-28 10:40:53 +02:00
|
|
|
return (select(songs)..where((tbl) => tbl.albumTitle.equals(album.title)))
|
|
|
|
.get()
|
|
|
|
.then((moorSongList) => moorSongList
|
|
|
|
.map((moorSong) => SongModel.fromMoorSong(moorSong))
|
|
|
|
.toList());
|
2020-04-16 08:04:34 +02:00
|
|
|
}
|
|
|
|
|
2020-04-03 13:55:15 +02:00
|
|
|
@override
|
|
|
|
Future<void> insertSong(SongModel songModel) async {
|
|
|
|
await into(songs).insert(songModel.toSongsCompanion());
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<bool> songExists(SongModel songModel) async {
|
|
|
|
final List<SongModel> songList = await getSongs();
|
|
|
|
return songList.contains(songModel);
|
2020-03-30 22:25:57 +02:00
|
|
|
}
|
2020-06-28 10:40:53 +02:00
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> flagAlbumPresent(AlbumModel albumModel) async {
|
|
|
|
if (albumModel.id != null) {
|
|
|
|
(update(albums)..where((t) => t.id.equals(albumModel.id)))
|
|
|
|
.write(const AlbumsCompanion(present: Value(true)));
|
|
|
|
} else {
|
|
|
|
throw UnimplementedError();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<AlbumModel> getAlbumByTitleArtist(String title, String artist) {
|
|
|
|
return (select(albums)
|
|
|
|
..where((t) => t.title.equals(title) & t.artist.equals(artist)))
|
|
|
|
.getSingle()
|
|
|
|
.then(
|
|
|
|
(moorAlbum) {
|
|
|
|
if (moorAlbum == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return AlbumModel.fromMoorAlbum(moorAlbum);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> removeNonpresentAlbums() async {
|
|
|
|
(delete(albums)..where((t) => t.present.not())).go();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> resetAlbumsPresentFlag() async {
|
|
|
|
update(albums).write(const AlbumsCompanion(present: Value(false)));
|
|
|
|
// return;
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> flagSongPresent(SongModel songModel) async {
|
|
|
|
(update(songs)..where((t) => t.path.equals(songModel.path)))
|
|
|
|
.write(const SongsCompanion(present: Value(true)));
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<SongModel> getSongByPath(String path) async {
|
|
|
|
return (select(songs)..where((t) => t.path.equals(path))).getSingle().then(
|
|
|
|
(moorSong) {
|
|
|
|
if (moorSong == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return SongModel.fromMoorSong(moorSong);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<SongModel> getSongByTitleAlbumArtist(
|
|
|
|
String title, String album, String artist) async {
|
|
|
|
return (select(songs)
|
|
|
|
..where((t) =>
|
|
|
|
t.title.equals(title) &
|
|
|
|
t.albumTitle.equals(album) &
|
|
|
|
t.artist.equals(artist)))
|
|
|
|
.getSingle()
|
|
|
|
.then(
|
|
|
|
(moorSong) {
|
|
|
|
if (moorSong == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return SongModel.fromMoorSong(moorSong);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> removeNonpresentSongs() async {
|
|
|
|
(delete(songs)..where((t) => t.present.not())).go();
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Future<void> resetSongsPresentFlag() async {
|
|
|
|
update(songs).write(const SongsCompanion(present: Value(false)));
|
|
|
|
}
|
2020-03-24 22:17:03 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
LazyDatabase _openConnection() {
|
|
|
|
// the LazyDatabase util lets us find the right location for the file async.
|
|
|
|
return LazyDatabase(() async {
|
|
|
|
// put the database file, called db.sqlite here, into the documents folder
|
|
|
|
// for your app.
|
2020-03-28 09:38:52 +01:00
|
|
|
final Directory dbFolder = await getApplicationDocumentsDirectory();
|
|
|
|
final File file = File(p.join(dbFolder.path, 'db.sqlite'));
|
2020-03-24 22:17:03 +01:00
|
|
|
return VmDatabase(file);
|
|
|
|
});
|
|
|
|
}
|
2020-06-01 10:49:33 +02:00
|
|
|
|
|
|
|
Future<MoorIsolate> createMoorIsolate() async {
|
|
|
|
// this method is called from the main isolate. Since we can't use
|
|
|
|
// getApplicationDocumentsDirectory on a background isolate, we calculate
|
|
|
|
// the database path in the foreground isolate and then inform the
|
|
|
|
// background isolate about the path.
|
|
|
|
final dir = await getApplicationDocumentsDirectory();
|
|
|
|
final path = p.join(dir.path, 'db.sqlite');
|
|
|
|
final receivePort = ReceivePort();
|
|
|
|
|
|
|
|
await Isolate.spawn(
|
|
|
|
_startBackground,
|
|
|
|
_IsolateStartRequest(receivePort.sendPort, path),
|
|
|
|
);
|
|
|
|
|
|
|
|
// _startBackground will send the MoorIsolate to this ReceivePort
|
|
|
|
return await receivePort.first as MoorIsolate;
|
|
|
|
}
|
|
|
|
|
|
|
|
void _startBackground(_IsolateStartRequest request) {
|
|
|
|
// this is the entry point from the background isolate! Let's create
|
|
|
|
// the database from the path we received
|
|
|
|
final executor = VmDatabase(File(request.targetPath));
|
|
|
|
// we're using MoorIsolate.inCurrent here as this method already runs on a
|
|
|
|
// background isolate. If we used MoorIsolate.spawn, a third isolate would be
|
|
|
|
// started which is not what we want!
|
|
|
|
final moorIsolate = MoorIsolate.inCurrent(
|
|
|
|
() => DatabaseConnection.fromExecutor(executor),
|
|
|
|
);
|
|
|
|
// inform the starting isolate about this, so that it can call .connect()
|
|
|
|
request.sendMoorIsolate.send(moorIsolate);
|
|
|
|
}
|
|
|
|
|
|
|
|
// used to bundle the SendPort and the target path, since isolate entry point
|
|
|
|
// functions can only take one parameter.
|
|
|
|
class _IsolateStartRequest {
|
|
|
|
_IsolateStartRequest(this.sendMoorIsolate, this.targetPath);
|
2020-06-28 10:40:53 +02:00
|
|
|
|
2020-06-01 10:49:33 +02:00
|
|
|
final SendPort sendMoorIsolate;
|
|
|
|
final String targetPath;
|
|
|
|
}
|