From 84b700236b660a04b063a0a7c6ebd14a96f02f76 Mon Sep 17 00:00:00 2001 From: Elias Projahn Date: Fri, 6 May 2022 15:13:49 +0200 Subject: [PATCH] common: Adapt to database changes --- common/lib/src/app.dart | 1 - common/lib/src/backend.dart | 52 +---- common/lib/src/library.dart | 210 +++++---------------- common/lib/src/playback.dart | 9 +- common/lib/src/screens/program.dart | 38 ++-- common/lib/src/screens/work.dart | 5 +- common/lib/src/widgets/player_bar.dart | 34 ++-- common/lib/src/widgets/recording_tile.dart | 5 +- common/pubspec.yaml | 1 + 9 files changed, 85 insertions(+), 270 deletions(-) diff --git a/common/lib/src/app.dart b/common/lib/src/app.dart index 5e49322..e91ecbf 100644 --- a/common/lib/src/app.dart +++ b/common/lib/src/app.dart @@ -37,7 +37,6 @@ class MusicusApp extends StatelessWidget { @override Widget build(BuildContext context) { return MusicusBackend( - dbPath: dbPath, settingsStorage: settingsStorage, playback: playback, platform: platform, diff --git a/common/lib/src/backend.dart b/common/lib/src/backend.dart index 0f5edef..4aeb96a 100644 --- a/common/lib/src/backend.dart +++ b/common/lib/src/backend.dart @@ -1,10 +1,3 @@ -import 'dart:io'; -import 'dart:isolate'; -import 'dart:ui'; - -import 'package:drift/drift.dart'; -import 'package:drift/isolate.dart'; -import 'package:drift/native.dart'; import 'package:flutter/widgets.dart'; import 'package:musicus_database/musicus_database.dart'; @@ -44,9 +37,6 @@ enum MusicusBackendStatus { /// The backend maintains a Musicus database within a Moor isolate. The connect /// port will be registered as 'moor' in the [IsolateNameServer]. class MusicusBackend extends StatefulWidget { - /// Path to the database file. - final String dbPath; - /// An object to persist the settings. final MusicusSettingsStorage settingsStorage; @@ -64,7 +54,6 @@ class MusicusBackend extends StatefulWidget { final Widget child; MusicusBackend({ - @required this.dbPath, @required this.settingsStorage, @required this.playback, @required this.platform, @@ -79,31 +68,19 @@ class MusicusBackend extends StatefulWidget { } class MusicusBackendState extends State { - /// Starts the database isolate. - /// - /// It will create a database connection for [request.path] and will send the - /// drift send port through [request.sendPort]. - static void _dbIsolateEntrypoint(_IsolateStartRequest request) { - final executor = NativeDatabase(File(request.path)); - - final driftIsolate = - DriftIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor)); - - request.sendPort.send(driftIsolate.connectPort); - } - /// The current backend status. /// /// If this is not [MusicusBackendStatus.ready], the [child] widget should /// prevent all access to the backend. MusicusBackendStatus status = MusicusBackendStatus.loading; - MusicusClientDatabase db; MusicusPlayback playback; MusicusSettings settings; MusicusPlatform platform; MusicusLibrary library; + MusicusClientDatabase get db => library.db; + @override void initState() { super.initState(); @@ -112,23 +89,6 @@ class MusicusBackendState extends State { /// Initialize resources. Future _load() async { - SendPort driftPort = IsolateNameServer.lookupPortByName('moor'); - - if (driftPort == null) { - final receivePort = ReceivePort(); - - await Isolate.spawn(_dbIsolateEntrypoint, - _IsolateStartRequest(receivePort.sendPort, widget.dbPath)); - - driftPort = await receivePort.first; - IsolateNameServer.registerPortWithName(driftPort, 'drift'); - } - - final driftIsolate = DriftIsolate.fromConnectPort(driftPort); - db = MusicusClientDatabase.connect( - connection: await driftIsolate.connect(), - ); - playback = widget.playback; await playback.setup(); @@ -186,14 +146,6 @@ class MusicusBackendState extends State { } } -/// Bundles arguments for the database isolate. -class _IsolateStartRequest { - final SendPort sendPort; - final String path; - - _IsolateStartRequest(this.sendPort, this.path); -} - /// Helper widget passing the current backend state down the widget tree. class _InheritedBackend extends InheritedWidget { final Widget child; diff --git a/common/lib/src/library.dart b/common/lib/src/library.dart index 9b2e117..23781fe 100644 --- a/common/lib/src/library.dart +++ b/common/lib/src/library.dart @@ -1,123 +1,39 @@ -import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'dart:ui'; + +import 'package:path/path.dart' as p; +import 'package:drift/drift.dart'; +import 'package:drift/isolate.dart'; +import 'package:drift/native.dart'; +import 'package:musicus_database/musicus_database.dart'; import 'platform.dart'; -/// Bundles a [Track] with information on how to find the corresponding file. -class InternalTrack { - /// The represented track. - final Track track; - - /// A string identifying the track for playback. - /// - /// This will be the result of calling the platform objects getIdentifier() - /// function with the file name of the track. - final String identifier; - - InternalTrack({ - this.track, - this.identifier, - }); - - factory InternalTrack.fromJson(Map json) => InternalTrack( - track: Track.fromJson(json['track']), - identifier: json['identifier'], - ); - - Map toJson() => { - 'track': track.toJson(), - 'identifier': identifier, - }; -} - -/// Description of a concrete audio file. -/// -/// This gets stored in the folder of the audio file and links the audio file -/// to a recording in the database. -class Track { - /// The name of the file that contains the track's audio. - /// - /// This corresponds to a document ID in terms of the Android Storage Access - /// Framework. - final String fileName; - - /// Index within the list of tracks for the corresponding recording. - final int index; - - /// Of which recording this track is a part of. - final int recordingId; - - /// Which work parts of the recorded work are contained in this track. - final List partIds; - - Track({ - this.fileName, - this.index, - this.recordingId, - this.partIds, - }); - - factory Track.fromJson(Map json) => Track( - fileName: json['fileName'], - index: json['index'], - recordingId: json['recording'], - partIds: List.from(json['parts']), - ); - - Map toJson() => { - 'fileName': fileName, - 'index': index, - 'recording': recordingId, - 'parts': partIds, - }; -} - -/// Representation of all tracked audio files in one folder. -class MusicusFile { - /// Current version of the Musicus file format. - /// - /// If incompatible changes are made, this will be increased by one. - static const currentVersion = 0; - - /// Musicus file format version in use. - /// - /// This will be used in the future, if incompatible changes are made. - final int version; - - /// List of [Track] objects. - final List tracks; - - MusicusFile({ - this.version = currentVersion, - List tracks, - }) : tracks = tracks ?? []; - - factory MusicusFile.fromJson(Map json) => MusicusFile( - version: json['version'], - tracks: json['tracks'] - .map((trackJson) => Track.fromJson(trackJson)) - .toList(growable: true), - ); - - Map toJson() => { - 'version': version, - 'tracks': tracks.map((t) => t.toJson()).toList(), - }; -} - /// Manager for all available tracks and their representation on disk. class MusicusLibrary { + /// Starts the database isolate. + /// + /// It will create a database connection for [request.path] and will send the + /// drift send port through [request.sendPort]. + static void _dbIsolateEntrypoint(_IsolateStartRequest request) { + final executor = NativeDatabase(File(request.path)); + + final driftIsolate = + DriftIsolate.inCurrent(() => DatabaseConnection.fromExecutor(executor)); + + request.sendPort.send(driftIsolate.connectPort); + } + /// String representing the music library base path. final String basePath; + /// The actual music library database. + MusicusClientDatabase db; + /// Access to platform dependent functionality. final MusicusPlatform platform; - /// Map of all available tracks by recording ID. - /// - /// These are [InternalTrack] objects to store the URI of the corresponding - /// audio file alongside the real [Track] object. - final Map> tracks = {}; - MusicusLibrary(this.basePath, this.platform); /// Load all available tracks. @@ -126,62 +42,32 @@ class MusicusLibrary { /// content of all files called musicus.json and stores all track information /// that it found. Future load() async { - // TODO: Consider capping the recursion somewhere. - Future recurse([String parentId]) async { - final children = await platform.getChildren(parentId); + SendPort driftPort = IsolateNameServer.lookupPortByName('drift'); - for (final child in children) { - if (child.isDirectory) { - recurse(child.id); - } else if (child.name == 'musicus.json') { - final content = await platform.readDocument(child.id); - final musicusFile = MusicusFile.fromJson(jsonDecode(content)); - for (final track in musicusFile.tracks) { - _indexTrack(parentId, track); - } - } - } + if (driftPort == null) { + final receivePort = ReceivePort(); + + await Isolate.spawn( + _dbIsolateEntrypoint, + _IsolateStartRequest( + receivePort.sendPort, p.join(basePath, 'musicus.db')), + ); + + driftPort = await receivePort.first; + IsolateNameServer.registerPortWithName(driftPort, 'drift'); } - await recurse(); - } - - /// Add a list of new tracks to the music library. - /// - /// They are stored in this instance and on disk in the directory denoted by - /// [parentId]. - Future addTracks(String parentId, List newTracks) async { - MusicusFile musicusFile; - - final oldContent = - await platform.readDocumentByName(parentId, 'musicus.json'); - - if (oldContent != null) { - musicusFile = MusicusFile.fromJson(jsonDecode(oldContent)); - } else { - musicusFile = MusicusFile(); - } - - for (final track in newTracks) { - _indexTrack(parentId, track); - musicusFile.tracks.add(track); - } - - await platform.writeDocumentByName( - parentId, 'musicus.json', jsonEncode(musicusFile.toJson())); - } - - /// Add a track to the map of available tracks. - Future _indexTrack(String parentId, Track track) async { - final iTrack = InternalTrack( - track: track, - identifier: await platform.getIdentifier(parentId, track.fileName), + final driftIsolate = DriftIsolate.fromConnectPort(driftPort); + db = MusicusClientDatabase.connect( + connection: await driftIsolate.connect(), ); - - if (tracks.containsKey(track.recordingId)) { - tracks[track.recordingId].add(iTrack); - } else { - tracks[track.recordingId] = [iTrack]; - } } } + +/// Bundles arguments for the database isolate. +class _IsolateStartRequest { + final SendPort sendPort; + final String path; + + _IsolateStartRequest(this.sendPort, this.path); +} diff --git a/common/lib/src/playback.dart b/common/lib/src/playback.dart index ac81eaa..da5ca5e 100644 --- a/common/lib/src/playback.dart +++ b/common/lib/src/playback.dart @@ -1,8 +1,7 @@ import 'package:meta/meta.dart'; +import 'package:musicus_database/musicus_database.dart'; import 'package:rxdart/rxdart.dart'; -import 'library.dart'; - /// Base class for Musicus playback. abstract class MusicusPlayback { /// Whether the player is active. @@ -14,7 +13,7 @@ abstract class MusicusPlayback { /// The current playlist. /// /// If the player is not active, this will be an empty list. - final playlist = BehaviorSubject.seeded([]); + final playlist = BehaviorSubject.seeded([]); /// Index of the currently played (or paused) track within the playlist. /// @@ -24,7 +23,7 @@ abstract class MusicusPlayback { /// The currently played track. /// /// This will be null, if there is no current track. - final currentTrack = BehaviorSubject.seeded(null); + final currentTrack = BehaviorSubject.seeded(null); /// Whether we are currently playing or not. /// @@ -50,7 +49,7 @@ abstract class MusicusPlayback { Future setup(); /// Add a list of tracks to the players playlist. - Future addTracks(List tracks); + Future addTracks(List tracks); /// Remove the track at [index] from the playlist. Future removeTrack(int index); diff --git a/common/lib/src/screens/program.dart b/common/lib/src/screens/program.dart index cbf614c..d8c8507 100644 --- a/common/lib/src/screens/program.dart +++ b/common/lib/src/screens/program.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:musicus_database/musicus_database.dart'; import '../backend.dart'; -import '../library.dart'; import '../widgets/play_pause_button.dart'; import '../widgets/recording_tile.dart'; @@ -18,7 +17,7 @@ class _ProgramScreenState extends State { StreamSubscription playerActiveSubscription; - StreamSubscription> playlistSubscription; + StreamSubscription> playlistSubscription; List widgets = []; StreamSubscription positionSubscription; @@ -64,7 +63,7 @@ class _ProgramScreenState extends State { } /// Go through the tracks of [playlist] and preprocess them for displaying. - Future updateProgram(List playlist) async { + Future updateProgram(List playlist) async { List newWidgets = []; // The following variables exist to adapt the resulting ProgramItem to its @@ -72,25 +71,22 @@ class _ProgramScreenState extends State { // If the previous recording was the same, we won't need to include the // recording data again. - int lastRecordingId; + String lastRecordingId; // If the previous work was the same, we won't need to retrieve its parts // from the database again. - int lastWorkId; + String lastWorkId; // This will contain information on the last new work. WorkInfo workInfo; - // The index of the last displayed section. - int lastSectionIndex; - for (var i = 0; i < playlist.length; i++) { // The widgets displayed for this track. List children = []; final track = playlist[i]; - final recordingId = track.track.recordingId; - final partIds = track.track.partIds; + final recordingId = track.recording; + final partIds = track.workParts; // If the recording is the same, the work will also be the same, so // workInfo doesn't have to be updated either. @@ -102,7 +98,6 @@ class _ProgramScreenState extends State { if (recordingInfo.recording.work != lastWorkId) { lastWorkId = recordingInfo.recording.work; workInfo = await backend.db.getWork(lastWorkId); - lastSectionIndex = null; } children.addAll([ @@ -116,27 +111,20 @@ class _ProgramScreenState extends State { ]); } - for (final partId in partIds) { - final partInfo = workInfo.parts[partId]; - - final sectionIndex = workInfo.sections - .lastIndexWhere((s) => s.beforePartIndex <= partId); - if (sectionIndex != lastSectionIndex && sectionIndex >= 0) { - lastSectionIndex = sectionIndex; - children.add(Padding( - padding: const EdgeInsets.only( - bottom: 8.0, - ), - child: Text(workInfo.sections[sectionIndex].title), - )); + for (final part_id_unparsed in partIds.split(',')) { + if (part_id_unparsed.isEmpty) { + continue; } + final partId = int.parse(part_id_unparsed); + final partInfo = workInfo.parts[partId]; + children.add(Padding( padding: const EdgeInsets.only( left: 8.0, ), child: Text( - partInfo.part.title, + partInfo.title, style: TextStyle( fontStyle: FontStyle.italic, ), diff --git a/common/lib/src/screens/work.dart b/common/lib/src/screens/work.dart index 553b2cf..9dd3976 100644 --- a/common/lib/src/screens/work.dart +++ b/common/lib/src/screens/work.dart @@ -31,9 +31,8 @@ class WorkScreen extends StatelessWidget { title: PerformancesText( performanceInfos: recordingInfo.performances, ), - onTap: () { - final tracks = backend.library.tracks[recordingId]; - tracks.sort((t1, t2) => t1.track.index.compareTo(t2.track.index)); + onTap: () async { + final tracks = await backend.db.tracksByRecording(recordingId).get(); backend.playback.addTracks(tracks); }, ); diff --git a/common/lib/src/widgets/player_bar.dart b/common/lib/src/widgets/player_bar.dart index 6ad5420..7c5c687 100644 --- a/common/lib/src/widgets/player_bar.dart +++ b/common/lib/src/widgets/player_bar.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:musicus_database/musicus_database.dart'; import '../backend.dart'; -import '../library.dart'; import '../screens/program.dart'; import 'play_pause_button.dart'; @@ -16,7 +15,7 @@ class PlayerBar extends StatefulWidget { class _PlayerBarState extends State { MusicusBackendState _backend; - StreamSubscription _currentTrackSubscribtion; + StreamSubscription _currentTrackSubscribtion; WorkInfo _workInfo; List _partIds; @@ -29,16 +28,22 @@ class _PlayerBarState extends State { _currentTrackSubscribtion?.cancel(); _currentTrackSubscribtion = _backend.playback.currentTrack.listen((track) { if (track != null) { - _setTrack(track.track); + _setTrack(track); } }); } Future _setTrack(Track track) async { final recording = - await _backend.db.recordingById(track.recordingId).getSingle(); + await _backend.db.recordingById(track.recording).getSingle(); + final workInfo = await _backend.db.getWork(recording.work); - final partIds = track.partIds; + + final partIds = track.workParts + .split(',') + .where((p) => p.isNotEmpty) + .map((p) => int.parse(p)) + .toList(); if (mounted) { setState(() { @@ -54,27 +59,14 @@ class _PlayerBarState extends State { String subtitle; if (_workInfo != null) { - title = _workInfo.composers - .map((p) => '${p.firstName} ${p.lastName}') - .join(', '); + title = '${_workInfo.composer.firstName} ${_workInfo.composer.lastName}'; final subtitleBuffer = StringBuffer(_workInfo.work.title); if (_partIds.isNotEmpty) { subtitleBuffer.write(': '); - - final section = _workInfo.sections.lastWhere( - (s) => s.beforePartIndex <= _partIds[0], - orElse: () => null, - ); - - if (section != null) { - subtitleBuffer.write(section.title); - subtitleBuffer.write(': '); - } - - subtitleBuffer.write( - _partIds.map((i) => _workInfo.parts[i].part.title).join(', ')); + subtitleBuffer + .write(_partIds.map((i) => _workInfo.parts[i].title).join(', ')); } subtitle = subtitleBuffer.toString(); diff --git a/common/lib/src/widgets/recording_tile.dart b/common/lib/src/widgets/recording_tile.dart index 257f38f..1a7735e 100644 --- a/common/lib/src/widgets/recording_tile.dart +++ b/common/lib/src/widgets/recording_tile.dart @@ -25,9 +25,8 @@ class RecordingTile extends StatelessWidget { children: [ DefaultTextStyle( style: textTheme.subtitle1, - child: Text(workInfo.composers - .map((p) => '${p.firstName} ${p.lastName}') - .join(', ')), + child: Text( + '${workInfo.composer.firstName} ${workInfo.composer.lastName}'), ), DefaultTextStyle( style: textTheme.headline6, diff --git a/common/pubspec.yaml b/common/pubspec.yaml index 7607570..4ac7e09 100644 --- a/common/pubspec.yaml +++ b/common/pubspec.yaml @@ -13,6 +13,7 @@ dependencies: meta: musicus_database: path: ../database + path: ^1.8.1 rxdart: url_launcher: ^6.1.0