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
Widget build(BuildContext context) {
return MusicusBackend(
dbPath: dbPath,
settingsStorage: settingsStorage,
playback: playback,
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: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<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.
///
/// 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<MusicusBackend> {
/// Initialize resources.
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;
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.
class _InheritedBackend extends InheritedWidget {
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';
/// 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.
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<int, List<InternalTrack>> 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<void> load() async {
// TODO: Consider capping the recursion somewhere.
Future<void> 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<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),
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);
}

View file

@ -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(<InternalTrack>[]);
final playlist = BehaviorSubject.seeded(<Track>[]);
/// 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<InternalTrack>.seeded(null);
final currentTrack = BehaviorSubject<Track>.seeded(null);
/// Whether we are currently playing or not.
///
@ -50,7 +49,7 @@ abstract class MusicusPlayback {
Future<void> setup();
/// 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.
Future<void> removeTrack(int index);

View file

@ -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<ProgramScreen> {
StreamSubscription<bool> playerActiveSubscription;
StreamSubscription<List<InternalTrack>> playlistSubscription;
StreamSubscription<List<Track>> playlistSubscription;
List<Widget> widgets = [];
StreamSubscription<double> positionSubscription;
@ -64,7 +63,7 @@ class _ProgramScreenState extends State<ProgramScreen> {
}
/// 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 = [];
// 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
// 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<Widget> 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<ProgramScreen> {
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<ProgramScreen> {
]);
}
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,
),

View file

@ -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);
},
);

View file

@ -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<PlayerBar> {
MusicusBackendState _backend;
StreamSubscription<InternalTrack> _currentTrackSubscribtion;
StreamSubscription<Track> _currentTrackSubscribtion;
WorkInfo _workInfo;
List<int> _partIds;
@ -29,16 +28,22 @@ class _PlayerBarState extends State<PlayerBar> {
_currentTrackSubscribtion?.cancel();
_currentTrackSubscribtion = _backend.playback.currentTrack.listen((track) {
if (track != null) {
_setTrack(track.track);
_setTrack(track);
}
});
}
Future<void> _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<PlayerBar> {
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();

View file

@ -25,9 +25,8 @@ class RecordingTile extends StatelessWidget {
children: <Widget>[
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,

View file

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