common: Adapt to database changes

This commit is contained in:
Elias Projahn 2022-05-06 15:13:49 +02:00
parent 9e485eac11
commit 84b700236b
9 changed files with 85 additions and 270 deletions

View file

@ -37,7 +37,6 @@ class MusicusApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MusicusBackend( return MusicusBackend(
dbPath: dbPath,
settingsStorage: settingsStorage, settingsStorage: settingsStorage,
playback: playback, playback: playback,
platform: platform, platform: platform,

View file

@ -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:flutter/widgets.dart';
import 'package:musicus_database/musicus_database.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 /// The backend maintains a Musicus database within a Moor isolate. The connect
/// port will be registered as 'moor' in the [IsolateNameServer]. /// port will be registered as 'moor' in the [IsolateNameServer].
class MusicusBackend extends StatefulWidget { class MusicusBackend extends StatefulWidget {
/// Path to the database file.
final String dbPath;
/// An object to persist the settings. /// An object to persist the settings.
final MusicusSettingsStorage settingsStorage; final MusicusSettingsStorage settingsStorage;
@ -64,7 +54,6 @@ class MusicusBackend extends StatefulWidget {
final Widget child; final Widget child;
MusicusBackend({ MusicusBackend({
@required this.dbPath,
@required this.settingsStorage, @required this.settingsStorage,
@required this.playback, @required this.playback,
@required this.platform, @required this.platform,
@ -79,31 +68,19 @@ class MusicusBackend extends StatefulWidget {
} }
class MusicusBackendState extends State<MusicusBackend> { class MusicusBackendState extends State<MusicusBackend> {
/// 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. /// The current backend status.
/// ///
/// If this is not [MusicusBackendStatus.ready], the [child] widget should /// If this is not [MusicusBackendStatus.ready], the [child] widget should
/// prevent all access to the backend. /// prevent all access to the backend.
MusicusBackendStatus status = MusicusBackendStatus.loading; MusicusBackendStatus status = MusicusBackendStatus.loading;
MusicusClientDatabase db;
MusicusPlayback playback; MusicusPlayback playback;
MusicusSettings settings; MusicusSettings settings;
MusicusPlatform platform; MusicusPlatform platform;
MusicusLibrary library; MusicusLibrary library;
MusicusClientDatabase get db => library.db;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
@ -112,23 +89,6 @@ class MusicusBackendState extends State<MusicusBackend> {
/// Initialize resources. /// Initialize resources.
Future<void> _load() async { Future<void> _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; playback = widget.playback;
await playback.setup(); await playback.setup();
@ -186,14 +146,6 @@ class MusicusBackendState extends State<MusicusBackend> {
} }
} }
/// 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. /// Helper widget passing the current backend state down the widget tree.
class _InheritedBackend extends InheritedWidget { class _InheritedBackend extends InheritedWidget {
final Widget child; final Widget child;

View file

@ -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'; 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<String, dynamic> json) => InternalTrack(
track: Track.fromJson(json['track']),
identifier: json['identifier'],
);
Map<String, dynamic> 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<int> partIds;
Track({
this.fileName,
this.index,
this.recordingId,
this.partIds,
});
factory Track.fromJson(Map<String, dynamic> json) => Track(
fileName: json['fileName'],
index: json['index'],
recordingId: json['recording'],
partIds: List.from(json['parts']),
);
Map<String, dynamic> 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<Track> tracks;
MusicusFile({
this.version = currentVersion,
List<Track> tracks,
}) : tracks = tracks ?? [];
factory MusicusFile.fromJson(Map<String, dynamic> json) => MusicusFile(
version: json['version'],
tracks: json['tracks']
.map<Track>((trackJson) => Track.fromJson(trackJson))
.toList(growable: true),
);
Map<String, dynamic> toJson() => {
'version': version,
'tracks': tracks.map((t) => t.toJson()).toList(),
};
}
/// Manager for all available tracks and their representation on disk. /// Manager for all available tracks and their representation on disk.
class MusicusLibrary { 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. /// String representing the music library base path.
final String basePath; final String basePath;
/// The actual music library database.
MusicusClientDatabase db;
/// Access to platform dependent functionality. /// Access to platform dependent functionality.
final MusicusPlatform platform; 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<int, List<InternalTrack>> tracks = {};
MusicusLibrary(this.basePath, this.platform); MusicusLibrary(this.basePath, this.platform);
/// Load all available tracks. /// Load all available tracks.
@ -126,62 +42,32 @@ class MusicusLibrary {
/// content of all files called musicus.json and stores all track information /// content of all files called musicus.json and stores all track information
/// that it found. /// that it found.
Future<void> load() async { Future<void> load() async {
// TODO: Consider capping the recursion somewhere. SendPort driftPort = IsolateNameServer.lookupPortByName('drift');
Future<void> recurse([String parentId]) async {
final children = await platform.getChildren(parentId);
for (final child in children) { if (driftPort == null) {
if (child.isDirectory) { final receivePort = ReceivePort();
recurse(child.id);
} else if (child.name == 'musicus.json') { await Isolate.spawn(
final content = await platform.readDocument(child.id); _dbIsolateEntrypoint,
final musicusFile = MusicusFile.fromJson(jsonDecode(content)); _IsolateStartRequest(
for (final track in musicusFile.tracks) { receivePort.sendPort, p.join(basePath, 'musicus.db')),
_indexTrack(parentId, track); );
}
} driftPort = await receivePort.first;
} IsolateNameServer.registerPortWithName(driftPort, 'drift');
} }
await recurse(); final driftIsolate = DriftIsolate.fromConnectPort(driftPort);
} db = MusicusClientDatabase.connect(
connection: await driftIsolate.connect(),
/// 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<void> addTracks(String parentId, List<Track> 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<void> _indexTrack(String parentId, Track track) async {
final iTrack = InternalTrack(
track: track,
identifier: await platform.getIdentifier(parentId, track.fileName),
); );
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);
}

View file

@ -1,8 +1,7 @@
import 'package:meta/meta.dart'; import 'package:meta/meta.dart';
import 'package:musicus_database/musicus_database.dart';
import 'package:rxdart/rxdart.dart'; import 'package:rxdart/rxdart.dart';
import 'library.dart';
/// Base class for Musicus playback. /// Base class for Musicus playback.
abstract class MusicusPlayback { abstract class MusicusPlayback {
/// Whether the player is active. /// Whether the player is active.
@ -14,7 +13,7 @@ abstract class MusicusPlayback {
/// The current playlist. /// The current playlist.
/// ///
/// If the player is not active, this will be an empty list. /// If the player is not active, this will be an empty list.
final playlist = BehaviorSubject.seeded(<InternalTrack>[]); final playlist = BehaviorSubject.seeded(<Track>[]);
/// Index of the currently played (or paused) track within the playlist. /// Index of the currently played (or paused) track within the playlist.
/// ///
@ -24,7 +23,7 @@ abstract class MusicusPlayback {
/// The currently played track. /// The currently played track.
/// ///
/// This will be null, if there is no current track. /// This will be null, if there is no current track.
final currentTrack = BehaviorSubject<InternalTrack>.seeded(null); final currentTrack = BehaviorSubject<Track>.seeded(null);
/// Whether we are currently playing or not. /// Whether we are currently playing or not.
/// ///
@ -50,7 +49,7 @@ abstract class MusicusPlayback {
Future<void> setup(); Future<void> setup();
/// Add a list of tracks to the players playlist. /// Add a list of tracks to the players playlist.
Future<void> addTracks(List<InternalTrack> tracks); Future<void> addTracks(List<Track> tracks);
/// Remove the track at [index] from the playlist. /// Remove the track at [index] from the playlist.
Future<void> removeTrack(int index); Future<void> removeTrack(int index);

View file

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart'; import '../backend.dart';
import '../library.dart';
import '../widgets/play_pause_button.dart'; import '../widgets/play_pause_button.dart';
import '../widgets/recording_tile.dart'; import '../widgets/recording_tile.dart';
@ -18,7 +17,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
StreamSubscription<bool> playerActiveSubscription; StreamSubscription<bool> playerActiveSubscription;
StreamSubscription<List<InternalTrack>> playlistSubscription; StreamSubscription<List<Track>> playlistSubscription;
List<Widget> widgets = []; List<Widget> widgets = [];
StreamSubscription<double> positionSubscription; StreamSubscription<double> positionSubscription;
@ -64,7 +63,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
} }
/// Go through the tracks of [playlist] and preprocess them for displaying. /// Go through the tracks of [playlist] and preprocess them for displaying.
Future<void> updateProgram(List<InternalTrack> playlist) async { Future<void> updateProgram(List<Track> playlist) async {
List<Widget> newWidgets = []; List<Widget> newWidgets = [];
// The following variables exist to adapt the resulting ProgramItem to its // The following variables exist to adapt the resulting ProgramItem to its
@ -72,25 +71,22 @@ class _ProgramScreenState extends State<ProgramScreen> {
// If the previous recording was the same, we won't need to include the // If the previous recording was the same, we won't need to include the
// recording data again. // recording data again.
int lastRecordingId; String lastRecordingId;
// If the previous work was the same, we won't need to retrieve its parts // If the previous work was the same, we won't need to retrieve its parts
// from the database again. // from the database again.
int lastWorkId; String lastWorkId;
// This will contain information on the last new work. // This will contain information on the last new work.
WorkInfo workInfo; WorkInfo workInfo;
// The index of the last displayed section.
int lastSectionIndex;
for (var i = 0; i < playlist.length; i++) { for (var i = 0; i < playlist.length; i++) {
// The widgets displayed for this track. // The widgets displayed for this track.
List<Widget> children = []; List<Widget> children = [];
final track = playlist[i]; final track = playlist[i];
final recordingId = track.track.recordingId; final recordingId = track.recording;
final partIds = track.track.partIds; final partIds = track.workParts;
// If the recording is the same, the work will also be the same, so // If the recording is the same, the work will also be the same, so
// workInfo doesn't have to be updated either. // workInfo doesn't have to be updated either.
@ -102,7 +98,6 @@ class _ProgramScreenState extends State<ProgramScreen> {
if (recordingInfo.recording.work != lastWorkId) { if (recordingInfo.recording.work != lastWorkId) {
lastWorkId = recordingInfo.recording.work; lastWorkId = recordingInfo.recording.work;
workInfo = await backend.db.getWork(lastWorkId); workInfo = await backend.db.getWork(lastWorkId);
lastSectionIndex = null;
} }
children.addAll([ children.addAll([
@ -116,27 +111,20 @@ class _ProgramScreenState extends State<ProgramScreen> {
]); ]);
} }
for (final partId in partIds) { for (final part_id_unparsed in partIds.split(',')) {
final partInfo = workInfo.parts[partId]; if (part_id_unparsed.isEmpty) {
continue;
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),
));
} }
final partId = int.parse(part_id_unparsed);
final partInfo = workInfo.parts[partId];
children.add(Padding( children.add(Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(
left: 8.0, left: 8.0,
), ),
child: Text( child: Text(
partInfo.part.title, partInfo.title,
style: TextStyle( style: TextStyle(
fontStyle: FontStyle.italic, fontStyle: FontStyle.italic,
), ),

View file

@ -31,9 +31,8 @@ class WorkScreen extends StatelessWidget {
title: PerformancesText( title: PerformancesText(
performanceInfos: recordingInfo.performances, performanceInfos: recordingInfo.performances,
), ),
onTap: () { onTap: () async {
final tracks = backend.library.tracks[recordingId]; final tracks = await backend.db.tracksByRecording(recordingId).get();
tracks.sort((t1, t2) => t1.track.index.compareTo(t2.track.index));
backend.playback.addTracks(tracks); backend.playback.addTracks(tracks);
}, },
); );

View file

@ -4,7 +4,6 @@ import 'package:flutter/material.dart';
import 'package:musicus_database/musicus_database.dart'; import 'package:musicus_database/musicus_database.dart';
import '../backend.dart'; import '../backend.dart';
import '../library.dart';
import '../screens/program.dart'; import '../screens/program.dart';
import 'play_pause_button.dart'; import 'play_pause_button.dart';
@ -16,7 +15,7 @@ class PlayerBar extends StatefulWidget {
class _PlayerBarState extends State<PlayerBar> { class _PlayerBarState extends State<PlayerBar> {
MusicusBackendState _backend; MusicusBackendState _backend;
StreamSubscription<InternalTrack> _currentTrackSubscribtion; StreamSubscription<Track> _currentTrackSubscribtion;
WorkInfo _workInfo; WorkInfo _workInfo;
List<int> _partIds; List<int> _partIds;
@ -29,16 +28,22 @@ class _PlayerBarState extends State<PlayerBar> {
_currentTrackSubscribtion?.cancel(); _currentTrackSubscribtion?.cancel();
_currentTrackSubscribtion = _backend.playback.currentTrack.listen((track) { _currentTrackSubscribtion = _backend.playback.currentTrack.listen((track) {
if (track != null) { if (track != null) {
_setTrack(track.track); _setTrack(track);
} }
}); });
} }
Future<void> _setTrack(Track track) async { Future<void> _setTrack(Track track) async {
final recording = 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 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) { if (mounted) {
setState(() { setState(() {
@ -54,27 +59,14 @@ class _PlayerBarState extends State<PlayerBar> {
String subtitle; String subtitle;
if (_workInfo != null) { if (_workInfo != null) {
title = _workInfo.composers title = '${_workInfo.composer.firstName} ${_workInfo.composer.lastName}';
.map((p) => '${p.firstName} ${p.lastName}')
.join(', ');
final subtitleBuffer = StringBuffer(_workInfo.work.title); final subtitleBuffer = StringBuffer(_workInfo.work.title);
if (_partIds.isNotEmpty) { if (_partIds.isNotEmpty) {
subtitleBuffer.write(': '); subtitleBuffer.write(': ');
subtitleBuffer
final section = _workInfo.sections.lastWhere( .write(_partIds.map((i) => _workInfo.parts[i].title).join(', '));
(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(', '));
} }
subtitle = subtitleBuffer.toString(); subtitle = subtitleBuffer.toString();

View file

@ -25,9 +25,8 @@ class RecordingTile extends StatelessWidget {
children: <Widget>[ children: <Widget>[
DefaultTextStyle( DefaultTextStyle(
style: textTheme.subtitle1, style: textTheme.subtitle1,
child: Text(workInfo.composers child: Text(
.map((p) => '${p.firstName} ${p.lastName}') '${workInfo.composer.firstName} ${workInfo.composer.lastName}'),
.join(', ')),
), ),
DefaultTextStyle( DefaultTextStyle(
style: textTheme.headline6, style: textTheme.headline6,

View file

@ -13,6 +13,7 @@ dependencies:
meta: meta:
musicus_database: musicus_database:
path: ../database path: ../database
path: ^1.8.1
rxdart: rxdart:
url_launcher: ^6.1.0 url_launcher: ^6.1.0