import 'dart:convert'; 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 { /// String representing the music library base path. final String basePath; /// 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. /// /// 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 load() async { // TODO: Consider capping the recursion somewhere. Future recurse([String parentId]) async { final children = await platform.getChildren(parentId); 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); } } } } 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), ); if (tracks.containsKey(track.recordingId)) { tracks[track.recordingId].add(iTrack); } else { tracks[track.recordingId] = [iTrack]; } } }