2020-04-18 23:41:08 +02:00
|
|
|
import 'dart:convert';
|
|
|
|
|
|
|
|
|
|
import 'package:flutter/services.dart';
|
|
|
|
|
|
|
|
|
|
import 'platform.dart';
|
|
|
|
|
|
2020-04-21 16:38:13 +02:00
|
|
|
/// Bundles a [Track] with the URI of the audio file it represents.
|
|
|
|
|
///
|
|
|
|
|
/// The uri shouldn't be stored on disk, but will be used at runtime.
|
|
|
|
|
class InternalTrack {
|
|
|
|
|
/// The represented track.
|
|
|
|
|
final Track track;
|
|
|
|
|
|
|
|
|
|
/// The URI of the represented audio file as retrieved from the SAF.
|
|
|
|
|
final String uri;
|
|
|
|
|
|
|
|
|
|
InternalTrack({
|
|
|
|
|
this.track,
|
|
|
|
|
this.uri,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
factory InternalTrack.fromJson(Map<String, dynamic> json) => InternalTrack(
|
|
|
|
|
track: Track.fromJson(json['track']),
|
|
|
|
|
uri: json['uri'],
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
Map<String, dynamic> toJson() => {
|
|
|
|
|
'track': track.toJson(),
|
|
|
|
|
'uri': uri,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2020-04-18 23:41:08 +02:00
|
|
|
/// 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 MusicLibrary {
|
|
|
|
|
static const platform = MethodChannel('de.johrpan.musicus/platform');
|
|
|
|
|
|
|
|
|
|
/// URI of the music library folder.
|
|
|
|
|
///
|
|
|
|
|
/// This is a tree URI in the terms of the Android Storage Access Framework.
|
|
|
|
|
final String treeUri;
|
|
|
|
|
|
|
|
|
|
/// Map of all available tracks by recording ID.
|
2020-04-21 16:38:13 +02:00
|
|
|
///
|
|
|
|
|
/// These are [InternalTrack] objects to store the URI of the corresponding
|
|
|
|
|
/// audio file alongside the real [Track] object.
|
|
|
|
|
final Map<int, List<InternalTrack>> tracks = {};
|
2020-04-18 23:41:08 +02:00
|
|
|
|
|
|
|
|
MusicLibrary(this.treeUri);
|
|
|
|
|
|
|
|
|
|
/// Load all available tracks.
|
|
|
|
|
///
|
|
|
|
|
/// This recursively searches through the whole music library, reads the
|
|
|
|
|
/// 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(treeUri, parentId);
|
|
|
|
|
|
|
|
|
|
for (final child in children) {
|
|
|
|
|
if (child.isDirectory) {
|
|
|
|
|
recurse(child.id);
|
|
|
|
|
} else if (child.name == 'musicus.json') {
|
|
|
|
|
final content = await Platform.readFile(treeUri, child.id);
|
|
|
|
|
final musicusFile = MusicusFile.fromJson(jsonDecode(content));
|
|
|
|
|
for (final track in musicusFile.tracks) {
|
2020-04-21 16:38:13 +02:00
|
|
|
_indexTrack(parentId, track);
|
2020-04-18 23:41:08 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await recurse();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Add a list of new tracks to the music library.
|
2020-04-19 18:37:16 +02:00
|
|
|
///
|
2020-04-18 23:41:08 +02:00
|
|
|
/// 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.readFileByName(treeUri, parentId, 'musicus.json');
|
|
|
|
|
|
|
|
|
|
if (oldContent != null) {
|
|
|
|
|
musicusFile = MusicusFile.fromJson(jsonDecode(oldContent));
|
|
|
|
|
} else {
|
|
|
|
|
musicusFile = MusicusFile();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (final track in newTracks) {
|
2020-04-21 16:38:13 +02:00
|
|
|
_indexTrack(parentId, track);
|
2020-04-18 23:41:08 +02:00
|
|
|
musicusFile.tracks.add(track);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Platform.writeFileByName(
|
|
|
|
|
treeUri, parentId, 'musicus.json', jsonEncode(musicusFile.toJson()));
|
|
|
|
|
}
|
2020-04-21 16:38:13 +02:00
|
|
|
|
|
|
|
|
/// Add a track to the map of available tracks.
|
|
|
|
|
Future<void> _indexTrack(String parentId, Track track) async {
|
|
|
|
|
final iTrack = InternalTrack(
|
|
|
|
|
track: track,
|
|
|
|
|
uri: await Platform.getUriByName(treeUri, parentId, track.fileName),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (tracks.containsKey(track.recordingId)) {
|
|
|
|
|
tracks[track.recordingId].add(iTrack);
|
|
|
|
|
} else {
|
|
|
|
|
tracks[track.recordingId] = [iTrack];
|
|
|
|
|
}
|
|
|
|
|
}
|
2020-04-18 23:41:08 +02:00
|
|
|
}
|