diff --git a/example/main.dart b/example/main.dart index 8a05027..11da880 100644 --- a/example/main.dart +++ b/example/main.dart @@ -2,60 +2,19 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; - -void main() { - final track = File("Pieces.mp3"); - - // Returns a condensate - // Getting the image of a track can be heavy and slow the reading - final metadata = readMetadata(track, getImage: false); - - print(metadata.title); - print(metadata.album); - print(metadata.duration); - // etc... - - // If you need ALL the metadata of a file (eg. MP3 has bpm) - // Later you need to check the type of the metadata with a switch - // ignore: unused_local_variable - final allMetadata = readAllMetadata(track, getImage: true); - - print("Now we are going to rewrite the metadata"); - - // Use the switch if you want to update metadata for a specific format - updateMetadata( - track, - (metadata) { - switch (metadata) { - case Mp3Metadata m: - m.songName = "New title"; - break; - case Mp4Metadata m: - m.title = "New title"; - break; - case VorbisMetadata m: - m.title = ["New title"]; - break; - case RiffMetadata m: - m.title = "New title"; - } - }, - ); - - // Or use the extension methods to update common properties - updateMetadata( - track, - (metadata) { - metadata.setTitle("New title"); - metadata.setArtist("New artist"); - metadata.setAlbum("New album"); - metadata.setTrackNumber(1); - metadata.setYear(DateTime(2014)); - metadata.setLyrics("I'm singing"); - metadata.setGenres(["Rock", "Metal", "Salsa"]); - metadata.setPictures([ - Picture(Uint8List.fromList([]), "image/png", PictureType.coverFront) - ]); - }, - ); +import 'package:audio_metadata_reader/src/parser.dart'; +import 'package:webdav_client/webdav_client.dart' as webdav; + +final client = webdav.newClient( + "https://xxx.xxx.xx/dav", + user: 'xxx', + password: 'xxxxxx', +); + +void main() async { + File file = File('/Users/lyp/Music/LZF-Music/After Hours - The Weeknd.flac'); + Uint8List data = await file.readAsBytes(); + print(data.length); + final metadata = await readMetadataUint8List(Stream.value(data), getImage: true); + print(metadata); } diff --git a/lib/audio_metadata_reader.dart b/lib/audio_metadata_reader.dart index fa781cc..9eb22a1 100644 --- a/lib/audio_metadata_reader.dart +++ b/lib/audio_metadata_reader.dart @@ -4,9 +4,8 @@ export "src/parsers/tag_parser.dart" show AudioMetadata, Picture; export 'src/metadata/base.dart' show PictureType; export 'src/utils/metadata_parser_exception.dart' show MetadataParserException, NoMetadataParserException; -export 'src/parser.dart' show readMetadata; export 'src/utils/metadata_parser_exception.dart' show MetadataParserException; -export 'src/parser.dart' show readMetadata, readAllMetadata; +export 'src/parser.dart' show readMetadata, readAllMetadata,readMetadataFile,readMetadataUint8List,readMetadataWebDav; export 'src/writer.dart' show updateMetadata, writeMetadata; export 'src/parsers/id3v1.dart' show ID3v1Parser; diff --git a/lib/src/metadata/base.dart b/lib/src/metadata/base.dart index 62b169d..b586b3e 100644 --- a/lib/src/metadata/base.dart +++ b/lib/src/metadata/base.dart @@ -242,7 +242,8 @@ extension CommonMetadataSetters on ParserTag { case VorbisMetadata m: m.lyric = lyric; break; - case RiffMetadata(): + case RiffMetadata m: + m.lyric = lyric; break; } } diff --git a/lib/src/metadata/riff_metadata.dart b/lib/src/metadata/riff_metadata.dart index d358cf4..c4e521e 100644 --- a/lib/src/metadata/riff_metadata.dart +++ b/lib/src/metadata/riff_metadata.dart @@ -6,6 +6,7 @@ class RiffMetadata extends ParserTag { String? album; DateTime? year; String? comment; + String? lyric; String? genre; int? trackNumber; String? encoder; @@ -22,6 +23,7 @@ class RiffMetadata extends ParserTag { this.album, this.year, this.comment, + this.lyric, this.genre, this.trackNumber, this.encoder, @@ -40,6 +42,7 @@ class RiffMetadata extends ParserTag { ' album: $album,\n' ' year: $year,\n' ' comment: $comment,\n' + ' lyrics: $lyric,\n' ' genre: $genre,\n' ' trackNumber: $trackNumber,\n' ' encoder: $encoder,\n' diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 7f9eec8..8ffaed2 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -1,29 +1,41 @@ import 'dart:io'; +import 'dart:typed_data'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; import 'package:audio_metadata_reader/src/parsers/riff.dart'; import 'package:audio_metadata_reader/src/metadata/base.dart'; +import 'package:audio_metadata_reader/src/utils/buffer.dart'; +import 'package:webdav_client/webdav_client.dart' as webdav; -/// Parse the metadata of a file. -/// -/// It automatically detects the type of the file (`.mp3`, `.ogg`, `.flac` etc) -/// and select the matching file parser. -/// -/// If there's no parser or an error, a `MetadataParserException` exception is raised. -/// -/// By default, it does not fetch the images of the file. Because some -/// images/covers are sometimes huge (~5MB), it can drastically make the -/// parsing slower. -AudioMetadata readMetadata(File track, {bool getImage = false}) { - final reader = track.openSync(); +Future readMetadataFile(File track, {bool getImage = false}) async { + MyRandomAccessFile reader = FileRandomAccessFile(randomAccessFile: track.openSync()); + return readMetadata(reader, getImage: getImage); +} + + +Future readMetadataWebDav(webdav.Client client, String path, + {bool getImage = false}) async { + final reader = WebDavRandomAccessFile(client: client, path: path); + return readMetadata(reader, getImage: getImage); +} + +Future readMetadataUint8List(Stream data,{bool getImage = false}) async { + final reader = StreamRandomAccessFile(stream: data); + return readMetadata(reader, getImage: getImage); +} + + + +Future readMetadata(MyRandomAccessFile reader, + {bool getImage = false}) async { try { - if (ID3v2Parser.canUserParser(reader)) { - final mp3Metadata = - ID3v2Parser(fetchImage: getImage).parse(reader) as Mp3Metadata; + if (await ID3v2Parser.canUserParser(reader)) { + final mp3Metadata = (await ID3v2Parser(fetchImage: getImage) + .parse(reader)) as Mp3Metadata; final a = AudioMetadata( - file: track, + file: File(''), album: mp3Metadata.album, artist: mp3Metadata.bandOrOrchestra ?? mp3Metadata.leadPerformer ?? @@ -52,12 +64,12 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { } return a; - } else if (FlacParser.canUserParser(reader)) { - final vorbisMetadata = - FlacParser(fetchImage: getImage).parse(reader) as VorbisMetadata; + } else if (await FlacParser.canUserParser(reader)) { + final vorbisMetadata = (await FlacParser(fetchImage: getImage) + .parse(reader)) as VorbisMetadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: vorbisMetadata.album.firstOrNull, artist: vorbisMetadata.artist.firstOrNull, bitrate: vorbisMetadata.bitrate, @@ -78,12 +90,12 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { newMetadata.performers.addAll(vorbisMetadata.performer); return newMetadata; - } else if (MP4Parser.canUserParser(reader)) { + } else if (await MP4Parser.canUserParser(reader)) { final mp4Metadata = - MP4Parser(fetchImage: getImage).parse(reader) as Mp4Metadata; + (await MP4Parser(fetchImage: getImage).parse(reader)) as Mp4Metadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: mp4Metadata.album, artist: mp4Metadata.artist, bitrate: mp4Metadata.bitrate, @@ -108,12 +120,12 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { } return newMetadata; - } else if (OGGParser.canUserParser(reader)) { - final oggMetadata = - OGGParser(fetchImage: getImage).parse(reader) as VorbisMetadata; + } else if (await OGGParser.canUserParser(reader)) { + final oggMetadata = (await OGGParser(fetchImage: getImage).parse(reader)) + as VorbisMetadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: oggMetadata.album.firstOrNull, artist: oggMetadata.artist.firstOrNull, bitrate: oggMetadata.bitrate, @@ -133,17 +145,17 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { newMetadata.performers.addAll(oggMetadata.performer); return newMetadata; - } else if (RiffParser.canUserParser(reader)) { - final riffMetadata = RiffParser().parse(reader) as RiffMetadata; + } else if (await RiffParser.canUserParser(reader)) { + final riffMetadata = (await RiffParser().parse(reader)) as RiffMetadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: riffMetadata.album, artist: riffMetadata.artist, bitrate: riffMetadata.bitrate, duration: riffMetadata.duration, language: null, - lyrics: null, + lyrics: riffMetadata.comment, sampleRate: riffMetadata.samplerate, title: riffMetadata.title, totalDisc: null, @@ -158,11 +170,11 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { (riffMetadata.genre != null) ? [riffMetadata.genre!] : []; return newMetadata; - } else if (ID3v1Parser.canUserParser(reader)) { - final mp3Metadata = ID3v1Parser().parse(reader) as Mp3Metadata; + } else if (await ID3v1Parser.canUserParser(reader)) { + final mp3Metadata = (await ID3v1Parser().parse(reader)) as Mp3Metadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: mp3Metadata.album, artist: mp3Metadata.bandOrOrchestra ?? mp3Metadata.originalArtist ?? @@ -188,50 +200,40 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { } } on MetadataParserException catch (e, s) { Error.throwWithStackTrace( - MetadataParserException(track: track, message: e.message), s); + MetadataParserException(track: reader, message: e.message), s); } catch (e, s) { Error.throwWithStackTrace( - MetadataParserException(track: track, message: e.toString()), s); + MetadataParserException(track: reader, message: e.toString()), s); } throw NoMetadataParserException( - track: track, + track: reader, ); } -/// Parse the metadata of a file and return ALL the metadata. It contains more information -/// than `readMetadata(...)` but you need more knowledges about the metadata specifications. -/// -/// Use this one if you want to rewrite the metadata later. -/// -/// It automatically detects the type of the file (`.mp3`, `.ogg`, `.flac` etc) -/// and select the matching file parser. -/// -/// If there's no parser or an error, a `MetadataParserException` exception is raised. -/// -/// By default, it fetches the cover images -ParserTag readAllMetadata(File track, {bool getImage = true}) { - final reader = track.openSync(); +Future readAllMetadata(MyRandomAccessFile reader, {bool getImage = true}) async { try { - if (ID3v2Parser.canUserParser(reader)) { - return ID3v2Parser(fetchImage: getImage).parse(reader); - } else if (FlacParser.canUserParser(reader)) { - return FlacParser(fetchImage: getImage).parse(reader); - } else if (MP4Parser.canUserParser(reader)) { - return MP4Parser(fetchImage: getImage).parse(reader); - } else if (OGGParser.canUserParser(reader)) { - return OGGParser(fetchImage: getImage).parse(reader); - } else if (ID3v2Parser.isID3v1(reader)) { - return ID3v1Parser().parse(reader); + if (await ID3v2Parser.canUserParser(reader)) { + return await ID3v2Parser(fetchImage: getImage).parse(reader); + } else if (await FlacParser.canUserParser(reader)) { + return await FlacParser(fetchImage: getImage).parse(reader); + } else if (await MP4Parser.canUserParser(reader)) { + return await MP4Parser(fetchImage: getImage).parse(reader); + } else if (await OGGParser.canUserParser(reader)) { + return await OGGParser(fetchImage: getImage).parse(reader); + } else if (await ID3v2Parser.isID3v1(reader)) { + return await ID3v1Parser().parse(reader); + } else if (await RiffParser.canUserParser(reader)) { + return await RiffParser().parse(reader); } } catch (e, trace) { print(trace); - throw MetadataParserException(track: track, message: e.toString()); + throw MetadataParserException(track: reader, message: e.toString()); } throw MetadataParserException( - track: track, + track: reader, message: "No available parser for this file. Please raise an issue in Github", ); diff --git a/lib/src/parsers/flac.dart b/lib/src/parsers/flac.dart index d35ba5d..b46f346 100644 --- a/lib/src/parsers/flac.dart +++ b/lib/src/parsers/flac.dart @@ -48,27 +48,27 @@ class FlacParser extends TagParser { FlacParser({super.fetchImage = false}); @override - ParserTag parse(RandomAccessFile reader) { - reader.setPositionSync(0); + Future parse(MyRandomAccessFile reader) async { + await reader.setPosition(0); - buffer = Buffer(randomAccessFile: reader); + buffer = await Buffer.create(randomAccessFile: reader); - buffer.skip(4); + await buffer.skip(4); bool isLastBlock = false; while (!isLastBlock) { - final block = _parseMetadataBlock(reader); + final block = await _parseMetadataBlock(reader); isLastBlock = block.isLastBlock; } - reader.closeSync(); + await reader.close(); return metadata; } - MetadataBlockHeader _parseMetadataBlock(RandomAccessFile reader) { - final bytes = buffer.read(4); + Future _parseMetadataBlock(MyRandomAccessFile reader) async { + final bytes = await buffer.read(4); final byteNumber = bytes[0]; final MetadataBlockHeader block = ( @@ -79,9 +79,9 @@ class FlacParser extends TagParser { switch (block.type) { case 0: - buffer.skip(10); + await buffer.skip(10); - final end = ByteData.sublistView(buffer.read(8)).getUint64(0); + final end = ByteData.sublistView(await buffer.read(8)).getUint64(0); final sampleRate = getIntFromArbitraryBits(end, 0, 20); final bitPerSample = getIntFromArbitraryBits(end, 23, 5) + 1; @@ -92,34 +92,34 @@ class FlacParser extends TagParser { metadata.sampleRate = sampleRate; metadata.bitrate = (bitPerSample * sampleRate).toInt(); - buffer.skip(16); // signature (128 ~/ 8) + await buffer.skip(16); // signature (128 ~/ 8) break; case 3: - buffer.skip(block.length); + await buffer.skip(block.length); break; case 4: - final bytes = buffer.read(block.length); + final bytes = await buffer.read(block.length); _parseVorbisComment(bytes); break; case 6: if (!fetchImage) { - buffer.skip(block.length); + await buffer.skip(block.length); } else { - final pictureType = getUint32(buffer.read(4)); - final mimeLength = getUint32(buffer.read(4)); + final pictureType = getUint32(await buffer.read(4)); + final mimeLength = getUint32(await buffer.read(4)); - final mime = String.fromCharCodes(buffer.read(mimeLength)); - final descriptionLength = getUint32(buffer.read(4)); + final mime = String.fromCharCodes(await buffer.read(mimeLength)); + final descriptionLength = getUint32(await buffer.read(4)); - buffer.skip(descriptionLength + 16); + await buffer.skip(descriptionLength + 16); // (descriptionLength > 0) // ? const Utf8Decoder().convert(buffer.read(descriptionLength)) // : ""; // description - // buffer.skip(16); - final lengthData = getUint32(buffer.read(4)); + // await buffer.skip(16); + final lengthData = getUint32(await buffer.read(4)); - final data = buffer.read(lengthData); + final data = await buffer.read(lengthData); metadata.pictures.add( Picture( data, @@ -130,7 +130,7 @@ class FlacParser extends TagParser { } break; default: - buffer.skip(block.length); + await buffer.skip(block.length); break; } @@ -139,9 +139,9 @@ class FlacParser extends TagParser { /// To detect if this parser can be used to parse this file, the 4 first bytes /// must be equal to `fLaC` - static bool canUserParser(RandomAccessFile reader) { - reader.setPositionSync(0); - final vendorName = String.fromCharCodes(reader.readSync(4)); + static Future canUserParser(MyRandomAccessFile reader) async { + await reader.setPosition(0); + final vendorName = String.fromCharCodes(await reader.read(4)); return vendorName == "fLaC"; } @@ -168,7 +168,7 @@ class FlacParser extends TagParser { final a = comment.split("="); final commentName = a[0]; - final value = a[1]; + final value = a.sublist(1).join("="); // 避免 value 中也有 "=" 被截断 switch (commentName.toUpperCase()) { case 'TITLE': diff --git a/lib/src/parsers/id3v1.dart b/lib/src/parsers/id3v1.dart index 33b1c69..bfa505c 100644 --- a/lib/src/parsers/id3v1.dart +++ b/lib/src/parsers/id3v1.dart @@ -29,10 +29,10 @@ class ID3v1Parser extends TagParser { } @override - ParserTag parse(RandomAccessFile reader) { - reader.setPositionSync(reader.lengthSync() - 128); + Future parse(MyRandomAccessFile reader) async{ + await reader.setPosition(await reader.length() - 128); - final tagData = reader.readSync(128); + final tagData = await reader.read(128); metadata.songName = _extract(tagData, 3, 33); metadata.leadPerformer = _extract(tagData, 33, 63); metadata.album = _extract(tagData, 63, 93); @@ -45,20 +45,20 @@ class ID3v1Parser extends TagParser { // metadata.genres = [tagData[127]]; - final buffer = Buffer(randomAccessFile: reader); - buffer.setPositionSync(0); + final buffer = await Buffer.create(randomAccessFile: reader); + await buffer.setPosition(0); // List mp3FrameHeader = [...buffer.read(4)]; Uint8List mp3FrameHeader = Uint8List(4); - mp3FrameHeader[0] = buffer.read(1)[0]; + mp3FrameHeader[0] = (await buffer.read(1))[0]; // CHECK : may have performance issues while (mp3FrameHeader.first != 0xff) { - mp3FrameHeader[0] = buffer.read(1)[0]; + mp3FrameHeader[0] = (await buffer.read(1))[0]; } - mp3FrameHeader[1] = buffer.read(1)[0]; - mp3FrameHeader[2] = buffer.read(1)[0]; - mp3FrameHeader[3] = buffer.read(1)[0]; + mp3FrameHeader[1] = (await buffer.read(1))[0]; + mp3FrameHeader[2] = (await buffer.read(1))[0]; + mp3FrameHeader[3] = (await buffer.read(1))[0]; final mpegVersion = (mp3FrameHeader[1] >> 3) & 0x3; final mpegLayer = (mp3FrameHeader[1] >> 1) & 3; @@ -70,7 +70,7 @@ class ID3v1Parser extends TagParser { // arbitrary choice. Usually the `Xing` header is located after ~30 bytes // then the header size is about ~150 bytes - final possibleXingHeader = buffer.read(400); + final possibleXingHeader = await buffer.read(400); int i = 0; while (possibleXingHeader[i] == 0) { @@ -100,23 +100,23 @@ class ID3v1Parser extends TagParser { metadata.bitrate = _getBitrate(mpegVersion, mpegLayer, bitrateIndex); if (metadata.bitrate != null) { - final fileSizeWithoutMetadata = reader.lengthSync() - 128; + final fileSizeWithoutMetadata = (await reader.length()) - 128; metadata.duration = Duration( seconds: (8 * fileSizeWithoutMetadata / metadata.bitrate!).round()); } } - reader.closeSync(); + await reader.close(); return metadata; } /// To detect if this file can be parsed with this parser, /// We have to check the 128 last bytes /// And if the first 3 are "TAG", it's ID3v1 - static bool canUserParser(RandomAccessFile reader) { - reader.setPositionSync(reader.lengthSync() - 128); + static Future canUserParser(MyRandomAccessFile reader)async { + await reader.setPosition((await reader.length()) - 128); - final headerBytes = reader.readSync(3); + final headerBytes = await reader.read(3); final tagIdentity = String.fromCharCodes(headerBytes); return tagIdentity == "TAG"; diff --git a/lib/src/parsers/id3v2.dart b/lib/src/parsers/id3v2.dart index ea6e817..425ce53 100644 --- a/lib/src/parsers/id3v2.dart +++ b/lib/src/parsers/id3v2.dart @@ -163,11 +163,11 @@ class ID3v2Parser extends TagParser { ID3v2Parser({fetchImage = false}) : super(fetchImage: fetchImage); @override - ParserTag parse(RandomAccessFile reader) { - reader.setPositionSync(0); - buffer = Buffer(randomAccessFile: reader); + Future parse(MyRandomAccessFile reader) async { + await reader.setPosition(0); + buffer = await Buffer.create(randomAccessFile: reader); - final headerBytes = buffer.read(10); + final headerBytes = await buffer.read(10); final majorVersion = headerBytes[3]; if (majorVersion == 1) { @@ -191,12 +191,13 @@ class ID3v2Parser extends TagParser { // > that is not vital to the correct // > parsing of the tag information if (getBit(headerBytes[5], 6) == 1) { - final extendedHeaderSize = getUint32(buffer.read(4)); - buffer.skip(extendedHeaderSize - 4); + final extendedHeaderSize = getUint32(await buffer.read(4)); + await buffer.skip(extendedHeaderSize - 4); } + while (offset < size) { - final frame = getFrame(reader, majorVersion == 4); + final frame = await getFrame(reader, majorVersion == 4); if (frame == null) { break; @@ -204,16 +205,20 @@ class ID3v2Parser extends TagParser { // 10 -> frame header offset = offset + 10 + frame.size; - processFrame(frame.id, frame.size); + + await processFrame(frame.id, frame.size); + } - if (metadata.duration == null || metadata.duration == Duration.zero) { - buffer.setPositionSync(size + 10); + // if (metadata.duration == null || metadata.duration == Duration.zero) { + if (true) { + + await buffer.setPosition(size + 10); - final mp3FrameHeader = _findFirstMp3Frame(buffer); + final mp3FrameHeader = await _findFirstMp3Frame(buffer); if (mp3FrameHeader == null) { - reader.closeSync(); + await reader.close(); return metadata; } @@ -240,7 +245,7 @@ class ID3v2Parser extends TagParser { // arbitrary choice. Usually the `Xing` header is located after ~30 bytes // then the header size is about ~150 bytes. - final possibleXingHeader = buffer.readAtMost(1500); + final possibleXingHeader = await buffer.readAtMost(1500); int i = 0; while (i < possibleXingHeader.length && possibleXingHeader[i] == 0) { @@ -274,7 +279,7 @@ class ID3v2Parser extends TagParser { } else { // it's a CBR file (Constant Bit Rate) if (metadata.bitrate != null && metadata.bitrate! > 0) { - final fileSizeWithoutMetadata = reader.lengthSync() - size; + final fileSizeWithoutMetadata = await reader.length() - size; final durationInSeconds = (8 * fileSizeWithoutMetadata) / metadata.bitrate!; @@ -285,7 +290,7 @@ class ID3v2Parser extends TagParser { } } - reader.closeSync(); + await reader.close(); return metadata; } @@ -301,8 +306,8 @@ class ID3v2Parser extends TagParser { /// valid word are correct. The MP3 specs specify several flags that must be set or not. /// /// Credit to [exiftool](https://github.com/exiftool/exiftool/blob/master/lib/Image/ExifTool/MPEG.pm#L464) - Uint8List? _findFirstMp3Frame(Buffer buffer) { - Uint8List frameHeader = buffer.readAtMost(4); + Future _findFirstMp3Frame(Buffer buffer) async { + Uint8List frameHeader = await buffer.readAtMost(4); while (frameHeader.length == 4) { // Look for frame sync (0xFF followed by 3 bytes) @@ -316,7 +321,7 @@ class ID3v2Parser extends TagParser { frameHeader[0] = frameHeader[1]; frameHeader[1] = frameHeader[2]; frameHeader[2] = frameHeader[3]; - frameHeader[3] = buffer.read(1)[0]; + frameHeader[3] = (await buffer.read(1))[0]; continue; } @@ -331,7 +336,7 @@ class ID3v2Parser extends TagParser { frameHeader[0] = frameHeader[1]; frameHeader[1] = frameHeader[2]; frameHeader[2] = frameHeader[3]; - frameHeader[3] = buffer.read(1)[0]; + frameHeader[3] = (await buffer.read(1))[0]; continue; } @@ -341,7 +346,7 @@ class ID3v2Parser extends TagParser { frameHeader[0] = frameHeader[1]; frameHeader[1] = frameHeader[2]; frameHeader[2] = frameHeader[3]; - frameHeader[3] = buffer.read(1)[0]; + frameHeader[3] = (await buffer.read(1))[0]; } return null; @@ -350,36 +355,35 @@ class ID3v2Parser extends TagParser { /// Process a frame. /// /// If the frame ID is not defined in the id3vX specs, then its content is dropped. - void processFrame(String frameId, int size) { + Future processFrame(String frameId, int size) async { // why do we duplicate the content in every block? // it's because the biggest thing to get in the cover // sometimes, we don't want to read so we have to read the content // at the very last time - final handlers = switch (frameId) { - "APIC" => () { + "APIC" => () async { if (fetchImage) { - final content = buffer.read(size); + final content = await buffer.read(size); final picture = getPicture(content); metadata.pictures.add(picture); } else { - buffer.skip(size); + await buffer.skip(size); } }, - "TALB" => () { - final content = buffer.read(size); + "TALB" => () async { + final content = await buffer.read(size); metadata.album = getTextFromFrame(content); }, - "TBPM" => () { - final content = buffer.read(size); + "TBPM" => () async { + final content = await buffer.read(size); metadata.bpm = getTextFromFrame(content); }, - "TCOP" => () { - final content = buffer.read(size); + "TCOP" => () async { + final content = await buffer.read(size); metadata.copyrightMessage = getTextFromFrame(content); }, - "TCON" => () { - final content = buffer.read(size); + "TCON" => () async { + final content = await buffer.read(size); metadata.contentType = getTextFromFrame(content); final regex = RegExp(r"(\d+).*"); final containRegex = RegExp(r";|/|\||,"); @@ -397,52 +401,52 @@ class ID3v2Parser extends TagParser { metadata.genres.add(metadata.contentType!); } }, - "TCOM" => () { - final content = buffer.read(size); + "TCOM" => () async { + final content = await buffer.read(size); metadata.composer = getTextFromFrame(content); }, - "TDAT" => () { - final content = buffer.read(size); + "TDAT" => () async { + final content = await buffer.read(size); metadata.date = getTextFromFrame(content); }, - "TDLY" => () { - final content = buffer.read(size); + "TDLY" => () async { + final content = await buffer.read(size); metadata.playlistDelay = getTextFromFrame(content); }, - "TENC" => () { - final content = buffer.read(size); + "TENC" => () async { + final content = await buffer.read(size); metadata.encodedBy = getTextFromFrame(content); }, - "TFLT" => () { - final content = buffer.read(size); + "TFLT" => () async { + final content = await buffer.read(size); metadata.fileType = getTextFromFrame(content); }, - "TIME" => () { - final content = buffer.read(size); + "TIME" => () async { + final content = await buffer.read(size); metadata.time = getTextFromFrame(content); }, - "TIT1" => () { - final content = buffer.read(size); + "TIT1" => () async { + final content = await buffer.read(size); metadata.contentGroupDescription = getTextFromFrame(content); }, - "TIT2" => () { - final content = buffer.read(size); + "TIT2" => () async { + final content = await buffer.read(size); metadata.songName = getTextFromFrame(content); }, - "TIT3" => () { - final content = buffer.read(size); + "TIT3" => () async { + final content = await buffer.read(size); metadata.subtitle = getTextFromFrame(content); }, - "TKEY" => () { - final content = buffer.read(size); + "TKEY" => () async { + final content = await buffer.read(size); metadata.initialKey = getTextFromFrame(content); }, - "TLAN" => () { - final content = buffer.read(size); + "TLAN" => () async { + final content = await buffer.read(size); metadata.languages = getTextFromFrame(content); }, - "TLEN" => () { - final content = buffer.read(size); + "TLEN" => () async { + final content = await buffer.read(size); final time = int.tryParse(getTextFromFrame(content)); if (time != null) { @@ -453,68 +457,68 @@ class ID3v2Parser extends TagParser { } } }, - "TMED" => () { - final content = buffer.read(size); + "TMED" => () async { + final content = await buffer.read(size); metadata.mediatype = getTextFromFrame(content); }, - "TOAL" => () { - final content = buffer.read(size); + "TOAL" => () async { + final content = await buffer.read(size); metadata.originalAlbum = getTextFromFrame(content); }, - "TOFN" => () { - final content = buffer.read(size); + "TOFN" => () async { + final content = await buffer.read(size); metadata.originalFilename = getTextFromFrame(content); }, - "TOLY" => () { - final content = buffer.read(size); + "TOLY" => () async { + final content = await buffer.read(size); metadata.originalTextWriter = getTextFromFrame(content); }, - "TOPE" => () { - final content = buffer.read(size); + "TOPE" => () async { + final content = await buffer.read(size); metadata.originalArtist = getTextFromFrame(content); }, - "TORY" => () { - final content = buffer.read(size); + "TORY" => () async { + final content = await buffer.read(size); metadata.originalReleaseYear = _parseYear(getTextFromFrame(content)); }, - "TOWN" => () { - final content = buffer.read(size); + "TOWN" => () async { + final content = await buffer.read(size); metadata.fileOwner == getTextFromFrame(content); }, - "TDRC" => () { - final content = buffer.read(size); + "TDRC" => () async { + final content = await buffer.read(size); metadata.year = _parseYear(getTextFromFrame(content)); }, - "TYER" => () { - final content = buffer.read(size); + "TYER" => () async { + final content = await buffer.read(size); metadata.year = _parseYear(getTextFromFrame(content)); }, - "TRDA" => () { - final content = buffer.read(size); + "TRDA" => () async { + final content = await buffer.read(size); metadata.year = _parseYear(getTextFromFrame(content)); }, - "TPE1" => () { - final content = buffer.read(size); + "TPE1" => () async { + final content = await buffer.read(size); metadata.leadPerformer = getTextFromFrame(content); }, - "TPE2" => () { - final content = buffer.read(size); + "TPE2" => () async { + final content = await buffer.read(size); metadata.bandOrOrchestra = getTextFromFrame(content); }, - "TPE3" => () { - final content = buffer.read(size); + "TPE3" => () async { + final content = await buffer.read(size); metadata.conductor = getTextFromFrame(content); }, - "TPE4" => () { - final content = buffer.read(size); + "TPE4" => () async { + final content = await buffer.read(size); metadata.interpreted = getTextFromFrame(content); }, - "TEXT" => () { - final content = buffer.read(size); + "TEXT" => () async { + final content = await buffer.read(size); metadata.textWriter = getTextFromFrame(content); }, - "TPOS" || "TPA" => () { - final content = buffer.read(size); + "TPOS" || "TPA" => () async { + final content = await buffer.read(size); final value = getTextFromFrame(content); metadata.partOfSet = value; @@ -527,12 +531,12 @@ class ID3v2Parser extends TagParser { metadata.discNumber = int.tryParse(value); } }, - "TPUB" => () { - final content = buffer.read(size); + "TPUB" => () async { + final content = await buffer.read(size); metadata.publisher = getTextFromFrame(content); }, - "TRCK" => () { - final content = buffer.read(size); + "TRCK" => () async { + final content = await buffer.read(size); final trackInfo = getTextFromFrame(content); if (trackInfo.isEmpty) { @@ -548,45 +552,45 @@ class ID3v2Parser extends TagParser { metadata.trackNumber = int.parse(trackInfo); } }, - "TRSN" => () { - final content = buffer.read(size); + "TRSN" => () async { + final content = await buffer.read(size); metadata.internetRadioStationName = getTextFromFrame(content); }, - "TRSO" => () { - final content = buffer.read(size); + "TRSO" => () async { + final content = await buffer.read(size); metadata.internetRadioStationOwner = getTextFromFrame(content); }, - "TSIZ" => () { - final content = buffer.read(size); + "TSIZ" => () async { + final content = await buffer.read(size); metadata.size = getTextFromFrame(content); }, - "TSRC" => () { - final content = buffer.read(size); + "TSRC" => () async { + final content = await buffer.read(size); metadata.isrc = getTextFromFrame(content); }, - "TXXX" => () { - final content = buffer.read(size); + "TXXX" => () async { + final content = await buffer.read(size); final frame = TXXXFrame(content); metadata.customMetadata[frame.description] = frame.information; }, - "USLT" => () { - final content = buffer.read(size); + "USLT" => () async { + final content = await buffer.read(size); metadata.lyric = getUnsynchronisedLyric(content); }, - "TSSE" => () { - final content = buffer.read(size); + "TSSE" => () async { + final content = await buffer.read(size); metadata.encoderSoftware = getTextFromFrame(content); }, - _ => () { - buffer.skip(size); + _ => () async { + await buffer.skip(size); } }; - handlers.call(); + await handlers.call(); } - ID3v3Frame? getFrame(RandomAccessFile reader, bool isV4) { - final headerBytes = buffer.read(10); + Future getFrame(MyRandomAccessFile reader, bool isV4) async { + final headerBytes = await buffer.read(10); if (headerBytes.every((element) => element == 0)) return null; @@ -653,82 +657,61 @@ class ID3v2Parser extends TagParser { } String getUnsynchronisedLyric(Uint8List content) { - int offset = 1; + if (content.isEmpty) return ""; + int offset = 1; // encoding final reader = ByteData.sublistView(content); final encoding = reader.getInt8(0); - // skip language + // 跳过 language (3 bytes) offset += 3; - final description = [reader.getInt8(offset)]; - - while (description.last != 0) { - description.add(reader.getInt8(offset)); + // description 字节,以 0 结束 + while (offset < content.length && reader.getInt8(offset) != 0) { offset++; } + offset++; // 跳过 description 的 null - if (encoding == 1 || encoding == 2) { - while (reader.getInt8(offset) == 0) { - offset++; - } + // 跳过可能的 padding 0 + while ((encoding == 1 || encoding == 2) && + offset < content.length && + reader.getInt8(offset) == 0) { + offset++; } - final rest = reader.buffer.asUint8List(offset); + // 剩余就是歌词内容 + final rest = content.sublist(offset); switch (encoding) { - case 0: - final nullCharacterPosition = rest.indexOf(0, 1); - final informationBytes = rest.sublist( - 1, (nullCharacterPosition >= 0) ? nullCharacterPosition : null); - return latin1Decoder.convert(informationBytes); - case 1: - if (encoding == 1 || encoding == 2) { - return utf16Decoder.decodeUtf16Le(rest, 0, rest.length - 2); - } - return utf16Decoder.decodeUtf16Le(rest); - case 2: - int nullCharacterPosition = 1; - bool zeroFound = false; - while (nullCharacterPosition < rest.length) { - if (rest[nullCharacterPosition] == 0) { - if (zeroFound) { - break; - } - zeroFound = true; - } - nullCharacterPosition++; - } - - final informationBytes = rest.sublist( - 1, (nullCharacterPosition >= 0) ? nullCharacterPosition : null); - return utf16Decoder.decodeUtf16Le(informationBytes); - case 3: - final nullCharacterPosition = rest.indexOf(0, 1); - final informationBytes = rest.sublist( - 1, (nullCharacterPosition >= 0) ? nullCharacterPosition : null); - return utf8Decoder.convert(informationBytes); + case 0: // Latin1 + return latin1Decoder.convert(rest).trimRight(); + case 1: // UTF-16 LE + case 2: // UTF-16 BE + String text = utf16Decoder.decodeUtf16Le(rest, 0, rest.length); + return text.replaceAll('\u0000', '').trimRight(); + case 3: // UTF-8 + return utf8Decoder.convert(rest).trimRight(); + default: + return ""; } - - return ""; } /// /// To detect if this file can be parsed with this parser, the first 3 bytes /// must be equal to `ID3` /// - static bool canUserParser(RandomAccessFile reader) { - reader.setPositionSync(0); - final headerBytes = reader.readSync(3); + static Future canUserParser(MyRandomAccessFile reader) async { + await reader.setPosition(0); + final headerBytes = await reader.read(3); final tagIdentity = String.fromCharCodes(headerBytes); return tagIdentity == "ID3"; } - static bool isID3v1(RandomAccessFile reader) { - reader.setPositionSync(reader.lengthSync() - 128); + static Future isID3v1(MyRandomAccessFile reader) async{ + await reader.setPosition((await reader.length()) - 128); - final headerBytes = reader.readSync(3); + final headerBytes = await reader.read(3); final tagIdentity = String.fromCharCodes(headerBytes); return tagIdentity == "TAG"; diff --git a/lib/src/parsers/mp4.dart b/lib/src/parsers/mp4.dart index b83ff23..fec2cbd 100644 --- a/lib/src/parsers/mp4.dart +++ b/lib/src/parsers/mp4.dart @@ -74,25 +74,25 @@ class MP4Parser extends TagParser { MP4Parser({fetchImage = false}) : super(fetchImage: fetchImage); @override - ParserTag parse(RandomAccessFile reader) { - reader.setPositionSync(0); - buffer = Buffer(randomAccessFile: reader); + Future parse(MyRandomAccessFile reader) async{ + await reader.setPosition(0); + buffer = await Buffer.create(randomAccessFile: reader); - final lengthFile = reader.lengthSync(); + final lengthFile = await reader.length(); while (buffer.fileCursor < lengthFile) { - final box = _readBox(buffer); + final box = await _readBox(buffer); if (supportedBox.contains(box.type)) { - processBox(buffer, box); + await processBox(buffer, box); } else { // We substract 8 to the box size because we already read the data for // the box header - buffer.skip(box.size - 8); + await buffer.skip(box.size - 8); } } - reader.closeSync(); + await reader.close(); return tags; } @@ -103,8 +103,8 @@ class MP4Parser extends TagParser { /// [0...3] -> box size (header + body) /// [4...7] -> box name (ASCII) /// - BoxHeader _readBox(Buffer buffer) { - final headerBytes = buffer.read(8); + Future _readBox(Buffer buffer) async { + final headerBytes = await buffer.read(8); final parser = ByteData.sublistView(headerBytes); final boxSize = parser.getUint32(0); @@ -116,7 +116,7 @@ class MP4Parser extends TagParser { boxNameBytes[2] == 0 && boxNameBytes[3] == 0) { throw MetadataParserException( - track: File(""), message: "Malformed MP4 file"); + track: null, message: "Malformed MP4 file"); } return BoxHeader(boxSize, String.fromCharCodes(boxNameBytes)); @@ -126,15 +126,24 @@ class MP4Parser extends TagParser { /// /// The metadata are inside special boxes. We only read data when we need it /// otherwise we skip them - void processBox(Buffer buffer, BoxHeader box) { + /// + + int mdatSize = 0; + int durationSeconds = 0; + Future processBox(Buffer buffer, BoxHeader box) async { if (box.type == "moov") { - parseRecurvise(buffer, box); + await parseRecurvise(buffer, box); + } else if (box.type == "mdat") { + await buffer.setPosition(buffer.fileCursor + box.size - 8); + mdatSize = box.size; + if (durationSeconds != 0) { + tags.bitrate = (mdatSize * 8 / durationSeconds).toInt(); + } } else if (box.type == "mvhd") { - final version = buffer.read(1)[0]; - + final version = (await buffer.read(1))[0]; // version 0 has 100 bytes // version 1 has 112 bytes - final bytes = buffer.read(version == 1 ? 111 : 99); + final bytes = await buffer.read(version == 1 ? 111 : 99); int timeScale = 0; int timeUnit = 0; @@ -149,27 +158,31 @@ class MP4Parser extends TagParser { double microseconds = (timeUnit / timeScale) * 1000000; tags.duration = Duration(microseconds: microseconds.toInt()); + durationSeconds = tags.duration!.inSeconds; + if (mdatSize != 0) { + tags.bitrate = (mdatSize * 8 / durationSeconds).toInt(); + } } else if (box.type == "udta") { - parseRecurvise(buffer, box); + await parseRecurvise(buffer, box); } else if (box.type == "ilst") { - parseRecurvise(buffer, box); + await parseRecurvise(buffer, box); } else if (["trak", "mdia", "minf", "stbl", "stsd"].contains(box.type)) { - parseRecurvise(buffer, box); + await parseRecurvise(buffer, box); } else if (box.type == "meta") { - buffer.read(4); + await buffer.read(4); - parseRecurvise(buffer, box); + await parseRecurvise(buffer, box); } else if (box.type[0] == "©" || ["gnre", "trkn", "disk", "tmpo", "cpil", "too", "covr", "pgap", "gen"] .contains(box.type)) { final boxName = (box.type[0] == "©") ? box.type.substring(1) : box.type; if (boxName == "covr" && !fetchImage) { - buffer.skip(box.size - 8); + await buffer.skip(box.size - 8); return; } - final metadataValue = buffer.read(box.size - 8); + final metadataValue = await buffer.read(box.size - 8); // sometimes the data is stored inside another box called `data` // we try to find out if the data contains the box type "data" (0:4 is the box size) @@ -236,15 +249,15 @@ class MP4Parser extends TagParser { break; } } else if (box.type == "----") { - final mean = _readBox(buffer); - String.fromCharCodes(buffer.read(mean.size - 8)); // mean value + final mean =await _readBox(buffer); + String.fromCharCodes(await buffer.read(mean.size - 8)); // mean value - final name = _readBox(buffer); + final name =await _readBox(buffer); final nameValue = - String.fromCharCodes(buffer.read(name.size - 8).sublist(4)); - final dataBox = _readBox(buffer); - final data = buffer.read(dataBox.size - 8); + String.fromCharCodes((await buffer.read(name.size - 8)).sublist(4)); + final dataBox =await _readBox(buffer); + final data = await buffer.read(dataBox.size - 8); final finalValue = String.fromCharCodes(data.sublist(8)); switch (nameValue) { @@ -257,17 +270,24 @@ class MP4Parser extends TagParser { default: } } else if (box.type == "mp4a") { - final bytes = buffer.read(box.size - 8); - + final bytes = await buffer.read(box.size - 8); // tags.bitrate = timeScale; tags.sampleRate = getUint32(bytes.sublist(22, 26)); + } else if (box.type == "alac") { + final bytes = await buffer.read(box.size - 8); // 减去 header + final sampleRate = ((bytes[24] << 24) | + (bytes[25] << 16) | + (bytes[26] << 8) | + bytes[27]) >> + 16; + tags.sampleRate = sampleRate; } else { - buffer.setPositionSync(buffer.fileCursor + box.size - 8); + await buffer.setPosition(buffer.fileCursor + box.size - 8); } } /// Parse a box with multiple sub boxes. - void parseRecurvise(Buffer buffer, BoxHeader box) { + Future parseRecurvise(Buffer buffer, BoxHeader box) async { final limit = box.size - 8; int offset = 0; @@ -275,17 +295,22 @@ class MP4Parser extends TagParser { if ("meta" == box.type) { offset += 4; } else if (box.type == "stsd") { - offset += 8; - buffer.read(8); + await buffer.read(4); + int entryCount =await buffer.readUint32(); + for (int i = 0; i < entryCount; i++) { + final newBox =await _readBox(buffer); // 会读到 mp4a 或 alac + await processBox(buffer, newBox); + } + return; } while (offset < limit) { - final newBox = _readBox(buffer); + final newBox =await _readBox(buffer); if (supportedBox.contains(newBox.type)) { - processBox(buffer, newBox); + await processBox(buffer, newBox); } else { - buffer.skip(newBox.size - 8); + await buffer.skip(newBox.size - 8); } offset += newBox.size; @@ -294,10 +319,10 @@ class MP4Parser extends TagParser { /// To detect if this parser can be used to parse this file, we need to detect /// the first box. It should be a `ftyp` box - static bool canUserParser(RandomAccessFile reader) { - reader.setPositionSync(4); + static Future canUserParser(MyRandomAccessFile reader) async { + await reader.setPosition(4); - final headerBytes = reader.readSync(4); + final headerBytes = await reader.read(4); final boxName = String.fromCharCodes(headerBytes); return boxName == "ftyp"; diff --git a/lib/src/parsers/ogg.dart b/lib/src/parsers/ogg.dart index dd70354..3123cb1 100644 --- a/lib/src/parsers/ogg.dart +++ b/lib/src/parsers/ogg.dart @@ -33,17 +33,17 @@ class OGGParser extends TagParser { late final Buffer buffer; @override - ParserTag parse(RandomAccessFile reader) { - reader.setPositionSync(0); + Future parse(MyRandomAccessFile reader) async { + await reader.setPosition(0); - buffer = Buffer(randomAccessFile: reader); + buffer = await Buffer.create(randomAccessFile: reader); // first page : contains samplerate and bitrate // second page : contains the metadata // it may not contains all the metadata so we would have to get them later final pages = [ - _parseUniquePage(), - _parseUniquePage(), + await _parseUniquePage(), + await _parseUniquePage(), ]; VorbisMetadata m = VorbisMetadata(); @@ -80,7 +80,7 @@ class OGGParser extends TagParser { OggPage? page; while (page?.headerType != 0x04) { - page = _parseUniquePageHeader(); + page = await _parseUniquePageHeader(); } m.duration = Duration( @@ -89,16 +89,16 @@ class OGGParser extends TagParser { ); } - reader.closeSync(); + await reader.close(); return m; } - VorbisMetadata _parseVorbisComment( + Future _parseVorbisComment( Uint8List page, int headerOffset, VorbisMetadata m, - ) { + ) async { final dataBuffer = List.from(page.sublist(headerOffset)); int offset = 0; @@ -115,7 +115,7 @@ class OGGParser extends TagParser { offset += 4; while (dataBuffer.length - offset < commentLength) { - dataBuffer.addAll(_parseUniquePage().data); + dataBuffer.addAll((await _parseUniquePage()).data); } final commentData = dataBuffer.sublist(offset, offset + commentLength); offset += commentLength; @@ -126,10 +126,10 @@ class OGGParser extends TagParser { } /// Check if the OGG parser can be use for a specific file - static bool canUserParser(RandomAccessFile reader) { - reader.setPositionSync(0); + static Future canUserParser(MyRandomAccessFile reader) async { + await reader.setPosition(0); - final capturePatternBytes = reader.readSync(4); + final capturePatternBytes = await reader.read(4); final capturePattern = String.fromCharCodes(capturePatternBytes); return capturePattern == "OggS"; @@ -137,13 +137,13 @@ class OGGParser extends TagParser { /// Parse a unique OGG page /// It means from the OggS magic word to the last segment - OggPage _parseUniquePage() { + Future _parseUniquePage() async{ // for the spec, see: https://wiki.xiph.org/Ogg List data = []; // contains data from previous (continuing) pages Uint8List headerData; try { - headerData = buffer.read(27); + headerData = await buffer.read(27); } catch (e) { // Handle end of file gracefully (return what we have or throw an error) if (data.isNotEmpty) { @@ -171,11 +171,11 @@ class OGGParser extends TagParser { // define the total of segments in this page final totalSegments = headerData[26]; - final segsizes = buffer.read(totalSegments); + final segsizes =await buffer.read(totalSegments); List pageData = []; // Data for the current page for (final segsize in segsizes) { - pageData.addAll(buffer.read(segsize)); + pageData.addAll(await buffer.read(segsize)); } // Concatenate data from continuing pages if necessary @@ -190,24 +190,24 @@ class OGGParser extends TagParser { /// Parse the header of an OGG page /// We also skip the content of the page to be synchronized - OggPage _parseUniquePageHeader() { + Future _parseUniquePageHeader()async { Uint8List headerData; - headerData = buffer.read(27); + headerData = await buffer.read(27); final headerType = headerData[5]; final granulePosition = getUint64LE(headerData.sublist(6, 14)); final bitstreamSerialNumber = getUint32LE(headerData.sublist(14, 18)); final totalSegments = headerData[26]; - final segsizes = buffer.read(totalSegments); + final segsizes =await buffer.read(totalSegments); int total = 0; for (final segsize in segsizes) { total += segsize; } - buffer.skip(total); + await buffer.skip(total); return OggPage( data: Uint8List(0), diff --git a/lib/src/parsers/riff.dart b/lib/src/parsers/riff.dart index 7095b29..3325ed7 100644 --- a/lib/src/parsers/riff.dart +++ b/lib/src/parsers/riff.dart @@ -16,13 +16,13 @@ class RiffParser extends TagParser { RiffParser({super.fetchImage = false}); @override - ParserTag parse(RandomAccessFile reader) { - reader.setPositionSync(0); + Future parse(MyRandomAccessFile reader) async{ + await reader.setPosition(0); - buffer = Buffer(randomAccessFile: reader); + buffer = await Buffer.create(randomAccessFile: reader); // Skip RIFF word, total size and WAVE - buffer.skip(12); + await buffer.skip(12); // Read chunks after the RIFF header (starting with WAVE header) _parseChunks(); @@ -33,7 +33,7 @@ class RiffParser extends TagParser { Duration(microseconds: (durationSeconds * 1000000).round()); } - reader.closeSync(); + await reader.close(); return metadata; } @@ -48,22 +48,22 @@ class RiffParser extends TagParser { /// contains the metadata. /// `data` chunk contains the number of audio bytes. Useful to calculate /// the track duration. - void _parseChunks() { + Future _parseChunks() async { // we substract 8 - while (buffer.fileCursor < buffer.randomAccessFile.lengthSync() - 12) { - final chunkId = String.fromCharCodes(buffer.read(4)); - final chunkSize = getUint32LE(buffer.read(4)); + while (buffer.fileCursor < (await buffer.randomAccessFile.length()) - 12) { + final chunkId = String.fromCharCodes(await buffer.read(4)); + final chunkSize = getUint32LE(await buffer.read(4)); if (chunkId == "fmt ") { - buffer.skip(4); - metadata.samplerate = getUint32LE(buffer.read(4)); - metadata.bitrate = getUint32LE(buffer.read(4)); + await buffer.skip(4); + metadata.samplerate = getUint32LE(await buffer.read(4)); + metadata.bitrate = getUint32LE(await buffer.read(4)); - buffer.skip(4); + await buffer.skip(4); } else if (chunkId == "LIST") { - final listType = String.fromCharCodes(buffer.read(4)); + final listType = String.fromCharCodes(await buffer.read(4)); if (listType == 'INFO') { - _parseInfoChunk(buffer.read(chunkSize - 4)); + _parseInfoChunk(await buffer.read(chunkSize - 4)); } } else if (chunkId == "data") { dataSize = chunkSize; @@ -106,6 +106,7 @@ class RiffParser extends TagParser { metadata.year = DateTime.tryParse(subChunkData); break; case 'ICMT': + print(subChunkData); metadata.comment = subChunkData; break; case 'ITRK': @@ -124,9 +125,9 @@ class RiffParser extends TagParser { } /// Must start with the magic word `RIFF` if it's a wav file - static bool canUserParser(RandomAccessFile reader) { - reader.setPositionSync(0); - final vendorName = String.fromCharCodes(reader.readSync(4)); + static Future canUserParser(MyRandomAccessFile reader) async{ + await reader.setPosition(0); + final vendorName = String.fromCharCodes(await reader.read(4)); return vendorName == "RIFF"; } diff --git a/lib/src/parsers/tag_parser.dart b/lib/src/parsers/tag_parser.dart index 4899965..77ff8f6 100644 --- a/lib/src/parsers/tag_parser.dart +++ b/lib/src/parsers/tag_parser.dart @@ -1,6 +1,6 @@ import 'dart:io'; import 'dart:typed_data'; - +import 'package:audio_metadata_reader/src/utils/buffer.dart'; import 'package:audio_metadata_reader/src/metadata/base.dart'; /// Picture is a representation of the images we can find in an ID3 tag @@ -134,5 +134,5 @@ abstract class TagParser { final bool fetchImage; TagParser({required this.fetchImage}); - ParserTag parse(RandomAccessFile reader); + Future parse(MyRandomAccessFile reader); } diff --git a/lib/src/utils/buffer.dart b/lib/src/utils/buffer.dart index 2d7d0a5..d0544cf 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -1,11 +1,225 @@ import 'dart:io'; import 'dart:math'; import 'dart:typed_data'; - +import 'dart:math' as math; import '../../audio_metadata_reader.dart'; +import 'package:webdav_client/webdav_client.dart' as webdav; -class Buffer { +int ceilDivInt(int a, int b) => (a + b - 1) ~/ b; + + + +abstract class MyRandomAccessFile{ + Future position(); + Future setPosition(int position); + Future readInto(Uint8List buffer, [int start = 0, int? end]); + Future length(); + Future close(); + Future read(int size); + +} + +class FileRandomAccessFile implements MyRandomAccessFile{ final RandomAccessFile randomAccessFile; + + FileRandomAccessFile({required this.randomAccessFile}); + + @override + Future length(){ + return randomAccessFile.length(); + } + + @override + Future position() { + return randomAccessFile.position(); + } + + @override + Future setPosition(int position) { + return randomAccessFile.setPosition(position); + } + + @override + Future readInto(Uint8List buffer, [int start = 0, int? end]) { + return randomAccessFile.readInto(buffer, start, end); + } + + @override + Future close() async { + await randomAccessFile.close(); + } + + @override + Future read(int size) async { + return await randomAccessFile.read(size); + } +} +class StreamRandomAccessFile implements MyRandomAccessFile { + final Stream _stream; + Uint8List? _data; + int _position = 0; + bool _isLoaded = false; + + StreamRandomAccessFile({required Stream stream}) : _stream = stream; + + // 懒加载:第一次访问时才从 Stream 读取数据 + Future _ensureLoaded() async { + if (_isLoaded) return; + + final chunks = []; + await for (final chunk in _stream) { + chunks.add(chunk); + } + + // 合并所有数据块 + final totalLength = chunks.fold(0, (sum, chunk) => sum + chunk.length); + _data = Uint8List(totalLength); + + int offset = 0; + for (final chunk in chunks) { + _data!.setRange(offset, offset + chunk.length, chunk); + offset += chunk.length; + } + + _isLoaded = true; + } + + @override + Future position() async { + return _position; + } + + @override + Future setPosition(int position) async { + if (position < 0) { + throw RangeError('Position cannot be negative'); + } + _position = position; + } + + @override + Future readInto(Uint8List buffer, [int start = 0, int? end]) async { + await _ensureLoaded(); + + end ??= buffer.length; + + if (start < 0 || end > buffer.length || start > end) { + throw RangeError('Invalid buffer range'); + } + + if (_position >= _data!.length) { + return 0; // EOF + } + + final availableBytes = _data!.length - _position; + final requestedBytes = end - start; + final bytesToRead = math.min(availableBytes, requestedBytes); + + buffer.setRange(start, start + bytesToRead, _data!, _position); + + _position += bytesToRead; + return bytesToRead; + } + + @override + Future length() async { + await _ensureLoaded(); + return _data!.length; + } + + @override + Future close() async { + // 释放内存 + _data = null; + _isLoaded = false; + } + + @override + Future read(int size) async { + await _ensureLoaded(); + + if (size < 0) { + throw ArgumentError('Size cannot be negative'); + } + + if (_position >= _data!.length) { + return Uint8List(0); // EOF + } + + final availableBytes = _data!.length - _position; + final bytesToRead = math.min(availableBytes, size); + + final result = Uint8List.sublistView(_data!, _position, _position + bytesToRead); + + _position += bytesToRead; + return result; + } +} + + + + +class WebDavRandomAccessFile implements MyRandomAccessFile{ + final webdav.Client client; + final String path; + int _position = 0; + int? _length; + + WebDavRandomAccessFile({required this.client, required this.path}); + + @override + Future length() async { + if(_length == null){ + _length = await client.readContentLength(path); + } + return _length!; + } + + @override + Future position() async { + return _position; + } + + @override + Future setPosition(int position) async { + _position = position; + } + + @override + Future readInto(Uint8List buffer, [int start = 0, int? end]) async { + final toRead = (end ?? buffer.length) - start; + final rangeEnd = min(_position + toRead -1, await length() -1); + if(rangeEnd < _position){ + return 0; + } + final data = await client.read(path, range: webdav.Range(start: _position, end: rangeEnd)); + buffer.setRange(start, start + data.length, data); + _position += data.length; + return data.length; + } + + @override + Future close() async { + + } + @override + Future read(int size) async { + final rangeEnd = min(_position + size -1, await length() -1); + if(rangeEnd < _position){ + return Uint8List(0); + } + final data = await client.read(path, range: webdav.Range(start: _position, end: rangeEnd)); + _position += data.length; + return data; + } +} + + + + + +class Buffer { + final MyRandomAccessFile randomAccessFile; final Uint8List _buffer; /// It's like positionSync() but adapted with the buffer @@ -22,16 +236,21 @@ class Buffer { static final int _bufferSize = 16384; /// The number of bytes remaining to be read from the file. - int get remainingBytes => - (_bufferedBytes - _cursor) + - (randomAccessFile.lengthSync() - randomAccessFile.positionSync()); + Future remainingBytes()async { + return (_bufferedBytes - _cursor) + + (await randomAccessFile.length() - await randomAccessFile.position()); + } - Buffer({required this.randomAccessFile}) : _buffer = Uint8List(_bufferSize) { - _fill(); + Buffer._({required this.randomAccessFile}) : _buffer = Uint8List(_bufferSize); + + static Future create({required MyRandomAccessFile randomAccessFile}) async { + final buffer = Buffer._(randomAccessFile: randomAccessFile); + await buffer._fill(); + return buffer; } - void _fill() { - _bufferedBytes = randomAccessFile.readIntoSync(_buffer); + Future _fill() async { + _bufferedBytes = await randomAccessFile.readInto(_buffer); _cursor = 0; } @@ -44,11 +263,11 @@ class Buffer { void _throwOnNoData() { if (_bufferedBytes == 0) { throw MetadataParserException( - track: File(""), message: "Expected more data in file"); + track: randomAccessFile, message: "Expected more data in file"); } } - Uint8List read(int size) { + Future read(int size) async { fileCursor += size; // if we read something big (~100kb), we can read it directly from file @@ -60,8 +279,8 @@ class Buffer { if (remaining > 0) { result.setRange(0, remaining, _buffer, _cursor); } - randomAccessFile.readIntoSync(result, remaining); - _fill(); + await randomAccessFile.readInto(result, remaining); + await _fill(); return result; } @@ -78,7 +297,7 @@ class Buffer { result.setRange(0, remaining, _buffer, _cursor); // Refill the buffer and adjust the cursor - _fill(); + await _fill(); int filled = remaining; // Continue filling `result` with new buffer data @@ -93,7 +312,7 @@ class Buffer { // Fill buffer again if more data is needed if (filled < size) { - _fill(); + await _fill(); // Avoid infinite loops if we are trying to read // more data than there is left in the file. _throwOnNoData(); @@ -107,18 +326,18 @@ class Buffer { /// /// May return a smaller list if [remainingBytes] is /// less than [size]. - Uint8List readAtMost(int size) { - final readSize = min(size, remainingBytes); - return read(readSize); + Future readAtMost(int size) async { + final readSize = min(size, await remainingBytes()); + return await read(readSize); } - void setPositionSync(int position) { + Future setPosition(int position)async { fileCursor = position; - randomAccessFile.setPositionSync(position); - _fill(); + await randomAccessFile.setPosition(position); + await _fill(); } - void skip(int length) { + Future skip(int length) async{ // Calculate how many bytes we can skip in the current buffer final remainingInBuffer = _bufferedBytes - _cursor; @@ -129,12 +348,31 @@ class Buffer { _cursor += length; } else { // Calculate the actual file position we need to skip to - int currentPosition = randomAccessFile.positionSync() - remainingInBuffer; + int currentPosition = (await randomAccessFile.position()) - remainingInBuffer; fileCursor += currentPosition; // Skip to the new position - randomAccessFile.setPositionSync(currentPosition + length); + await randomAccessFile.setPosition(currentPosition + length); // Refill the buffer at the new position - _fill(); + await _fill(); } } + + Future lengthSync() async{ + return randomAccessFile.length(); + } + + Future closeSync() async { + await randomAccessFile.close(); + } } + +extension BufferRead on Buffer { + Future readUint32() async { + final bytes = await read(4); + // MP4 box 是 big-endian + return (bytes[0] << 24) | + (bytes[1] << 16) | + (bytes[2] << 8) | + bytes[3]; + } +} \ No newline at end of file diff --git a/lib/src/utils/metadata_parser_exception.dart b/lib/src/utils/metadata_parser_exception.dart index f887e98..caac015 100644 --- a/lib/src/utils/metadata_parser_exception.dart +++ b/lib/src/utils/metadata_parser_exception.dart @@ -1,27 +1,32 @@ import 'dart:io'; +import 'package:audio_metadata_reader/src/utils/buffer.dart'; class MetadataParserException implements Exception { - final File track; + final MyRandomAccessFile? track; final String message; const MetadataParserException({ - required this.track, + this.track, required this.message, }); @override - String toString() => - 'MetadataParserException : error during the parsing of "${track.path}"\n${message}'; + String toString() { + final trackInfo = track != null ? '"$track"' : 'unknown track'; + return 'MetadataParserException : error during the parsing of $trackInfo\n$message'; + } } class NoMetadataParserException implements Exception { - final File track; + final MyRandomAccessFile? track; const NoMetadataParserException({ - required this.track, + this.track, }); @override - String toString() => - 'NoMetadataParserException : no parser for "${track.path}"'; + String toString() { + final trackInfo = track != null ? '"$track"' : 'unknown track'; + return 'NoMetadataParserException : no parser for $trackInfo'; + } } diff --git a/lib/src/writer.dart b/lib/src/writer.dart index 1b674e5..f166ee8 100644 --- a/lib/src/writer.dart +++ b/lib/src/writer.dart @@ -1,5 +1,5 @@ import 'dart:io'; - +import 'package:audio_metadata_reader/src/utils/buffer.dart'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; import 'package:audio_metadata_reader/src/metadata/base.dart'; import 'package:audio_metadata_reader/src/parsers/riff.dart'; @@ -8,27 +8,27 @@ import 'package:audio_metadata_reader/src/parsers/riff.dart'; /// /// The [updater] receives the specific metadata object (e.g Mp3Metadata). /// You can modify this object directly within the callback. -void updateMetadata(File track, void Function(ParserTag metadata) updater) { - final metadata = readAllMetadata(track); +Future updateMetadata(File track, void Function(ParserTag metadata) updater) async{ + // final metadata = await readAllMetadata(track); - updater(metadata); + // updater(metadata); - writeMetadata(track, metadata); + // writeMetadata(track, metadata); } /// Write the [metadata] into the [track] -void writeMetadata(File track, ParserTag metadata) { - final reader = track.openSync(); +Future writeMetadata(File track, ParserTag metadata) async{ + final reader = FileRandomAccessFile(randomAccessFile: track.openSync()); - if (ID3v2Parser.canUserParser(reader)) { + if (await ID3v2Parser.canUserParser(reader)) { Id3v4Writer().write(track, metadata as Mp3Metadata); - } else if (MP4Parser.canUserParser(reader)) { + } else if (await MP4Parser.canUserParser(reader)) { Mp4Writer().write(track, metadata as Mp4Metadata); - } else if (FlacParser.canUserParser(reader)) { + } else if (await FlacParser.canUserParser(reader)) { FlacWriter().write(track, metadata as VorbisMetadata); - } else if (RiffParser.canUserParser(reader)) { + } else if (await RiffParser.canUserParser(reader)) { RiffWriter().write(track, metadata as RiffMetadata); - } else if (ID3v1Parser.canUserParser(reader)) { + } else if (await ID3v1Parser.canUserParser(reader)) { ID3v1Writer().write(track, metadata as Mp3Metadata); } } diff --git a/lib/src/writers/flac_writer.dart b/lib/src/writers/flac_writer.dart index d84bd70..c51d288 100644 --- a/lib/src/writers/flac_writer.dart +++ b/lib/src/writers/flac_writer.dart @@ -157,6 +157,9 @@ class FlacWriter extends BaseMetadataWriter { _writeComment("LOCATION", metadata.location); _writeComment("CONTACT", metadata.contact); _writeComment("ISRC", metadata.isrc); + if (metadata.lyric != null && metadata.lyric!.isNotEmpty) { + _writeComment("LYRICS", [metadata.lyric!]); + } mainBuilder.add(intToUint32LE(i)); mainBuilder.add(commentsBuilder.toBytes()); diff --git a/lib/src/writers/id3v4_writer.dart b/lib/src/writers/id3v4_writer.dart index 69b7662..f996a2a 100644 --- a/lib/src/writers/id3v4_writer.dart +++ b/lib/src/writers/id3v4_writer.dart @@ -33,9 +33,9 @@ class Id3v4Writer extends BaseMetadataWriter { _writeHeader(finalBuilder, builder.length); finalBuilder.add(builder.toBytes()); - + if (size == 0) { - file.writeAsBytesSync(finalBuilder.toBytes()); + file.writeAsBytesSync(finalBuilder.toBytes()); } else { final oldData = file.readAsBytesSync(); file.writeAsBytesSync([ @@ -179,6 +179,11 @@ class Id3v4Writer extends BaseMetadataWriter { } else if (metadata.contentType != null) { _writeFrame(builder, "TCON", metadata.contentType!); } + + // 添加歌词支持 + if (metadata.lyric != null && metadata.lyric!.isNotEmpty) { + _writeUnsyncLyrics(builder, metadata.lyric!); + } } void _writeFrame(BytesBuilder builder, String frameId, String data) { @@ -204,6 +209,29 @@ class Id3v4Writer extends BaseMetadataWriter { builder.add(data); } + // 写入非同步歌词(USLT frame) + void _writeUnsyncLyrics(BytesBuilder builder, String lyrics) { + final frameBuilder = BytesBuilder(); + + // Text encoding (UTF-8) + frameBuilder.addByte(0x03); + + // Language (3 bytes) - 使用"XXX"表示未指定语言 + frameBuilder.add(ascii.encode("XXX")); + + // Content descriptor (空描述 + null terminator) + frameBuilder.addByte(0x00); + + // Lyrics text + frameBuilder.add(utf8.encode(lyrics)); + + // 写入USLT frame + builder.add("USLT".codeUnits); + builder.add(_encodeSynchsafeInteger(frameBuilder.length)); + builder.add([0, 0]); // flags + builder.add(frameBuilder.toBytes()); + } + void _writePictures(BytesBuilder builder, List pictures) { for (final picture in pictures) { final pictureBuilder = BytesBuilder(); diff --git a/lib/src/writers/mp4_writer.dart b/lib/src/writers/mp4_writer.dart index 79ffd73..4d2b669 100644 --- a/lib/src/writers/mp4_writer.dart +++ b/lib/src/writers/mp4_writer.dart @@ -37,7 +37,7 @@ class Mp4Writer extends BaseMetadataWriter { reader.closeSync(); - File("a_new.mp4").writeAsBytesSync(byteBuilder.toBytes()); + file.writeAsBytesSync(byteBuilder.toBytes()); } Uint8List _processBox(Uint8List data) { diff --git a/lib/src/writers/riff_writer.dart b/lib/src/writers/riff_writer.dart index 4cff3b3..ccbfda9 100644 --- a/lib/src/writers/riff_writer.dart +++ b/lib/src/writers/riff_writer.dart @@ -12,16 +12,16 @@ class RiffWriter extends BaseMetadataWriter { late final Buffer buffer; @override - void write(File file, RiffMetadata metadata) { + Future write(File file, RiffMetadata metadata) async{ this.metadata = metadata; final builder = BytesBuilder(); final reader = file.openSync(); - buffer = Buffer(randomAccessFile: reader); + buffer = await Buffer.create(randomAccessFile: FileRandomAccessFile(randomAccessFile: reader)); reader.setPositionSync(0); - buffer.skip(12); - final newData = _parseChunks(); + await buffer.skip(12); + final newData =await _parseChunks(); builder.add("RIFF".codeUnits); builder.add(intToUint32LE(newData.length)); @@ -29,28 +29,28 @@ class RiffWriter extends BaseMetadataWriter { builder.add(newData); reader.closeSync(); - File("a_new.wav").writeAsBytesSync(builder.toBytes()); + file.writeAsBytesSync(builder.toBytes()); } - Uint8List _parseChunks() { + Future _parseChunks() async{ final builder = BytesBuilder(); - while (buffer.fileCursor < buffer.randomAccessFile.lengthSync() - 8) { - final chunkIdBytes = buffer.read(4); + while (buffer.fileCursor < (await buffer.randomAccessFile.length()) - 8) { + final chunkIdBytes = await buffer.read(4); final chunkId = String.fromCharCodes(chunkIdBytes); - final chunkSizeBytes = buffer.read(4); + final chunkSizeBytes = await buffer.read(4); final chunkSize = getUint32LE(chunkSizeBytes); builder.add(chunkIdBytes); if (chunkId == "LIST") { // Peek at the next 4 bytes to check the LIST type - final listTypeBytes = buffer.read(4); + final listTypeBytes = await buffer.read(4); final listType = String.fromCharCodes(listTypeBytes); if (listType == "INFO") { // Skip old INFO data - buffer.skip(chunkSize - 4); + await buffer.skip(chunkSize - 4); // Write new LIST chunk with updated INFO final infoBuilder = BytesBuilder(); @@ -84,6 +84,9 @@ class RiffWriter extends BaseMetadataWriter { if (metadata.copyright != null) { infoBuilder.add(_writeChunk("ICOP", metadata.copyright!)); } + if (metadata.lyric != null) { + infoBuilder.add(_writeChunk("ICMT", metadata.lyric!)); + } final infoData = infoBuilder.toBytes(); // 4 bytes for "INFO" + INFO subchunks @@ -96,16 +99,16 @@ class RiffWriter extends BaseMetadataWriter { // Keep the LIST chunk as-is builder.add(chunkSizeBytes); builder.add(listTypeBytes); - builder.add(buffer.read(chunkSize - 4)); + builder.add(await buffer.read(chunkSize - 4)); } } else { builder.add(chunkSizeBytes); - builder.add(buffer.read(chunkSize)); + builder.add(await buffer.read(chunkSize)); } // Handle padding if chunkSize is odd (WAV chunks are aligned to even sizes) if (chunkSize.isOdd) { - buffer.skip(1); + await buffer.skip(1); builder.addByte(0); } } diff --git a/pubspec.yaml b/pubspec.yaml index cf68054..d1a457c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,9 +1,10 @@ name: audio_metadata_reader description: A metadata reader for audio files. Support ID3, Vorbis comments and ILST itunes -version: 1.4.2 +version: 0.0.7 homepage: https://clementbeal.github.io/ repository: https://github.com/ClementBeal/audio_metadata_reader issue_tracker: https://github.com/ClementBeal/audio_metadata_reader/issues +publish_to: none topics: - audio @@ -19,5 +20,9 @@ dependencies: charset: ^2.0.1 intl: ^0.20.2 mime: ^2.0.0 + webdav_client: + git: + url: https://github.com/GerryDush/webdav_client.git + ref: v0.0.1 dev_dependencies: test: ^1.26.2