From f9a365167bdc2fac916970b4b195e8fb72bbbcf0 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 30 Aug 2025 00:32:14 +0800 Subject: [PATCH 01/16] fix(audio): add writing lyrics support (MP3 ID3v2, FLAC, M4A, WAV) and sample bitrate for M4A --- example/main.dart | 46 ++++++++++++---------------------------------- 1 file changed, 12 insertions(+), 34 deletions(-) diff --git a/example/main.dart b/example/main.dart index 8a05027..30584be 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,15 +4,14 @@ import 'dart:typed_data'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; void main() { - final track = File("Pieces.mp3"); + final track = File("Free Loop - Daniel Powter.mp3"); // Returns a condensate // Getting the image of a track can be heavy and slow the reading - final metadata = readMetadata(track, getImage: false); + AudioMetadata metadata = readMetadata(track, getImage: true); + + print(metadata); - print(metadata.title); - print(metadata.album); - print(metadata.duration); // etc... // If you need ALL the metadata of a file (eg. MP3 has bpm) @@ -26,36 +25,15 @@ void main() { 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"; - } + metadata.setLyrics("I'm singing fuck"); }, ); - // 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) - ]); - }, - ); + print('===================='); + metadata = readMetadata(track, getImage: true); + + print(metadata); + print('lyric: ${metadata.lyrics}'); } + + From 65a4a6a9ec754389cf7e3127dbec8d1902759886 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 30 Aug 2025 00:33:05 +0800 Subject: [PATCH 02/16] fix(audio): add writing lyrics support (MP3 ID3v2, FLAC, M4A, WAV) and sample bitrate for M4A --- lib/src/metadata/base.dart | 3 ++- lib/src/metadata/riff_metadata.dart | 3 +++ lib/src/parser.dart | 4 +++- lib/src/parsers/id3v2.dart | 3 ++- lib/src/parsers/mp4.dart | 33 +++++++++++++++++++++++++---- lib/src/parsers/riff.dart | 1 + lib/src/utils/buffer.dart | 12 +++++++++++ lib/src/writer.dart | 5 +++++ lib/src/writers/flac_writer.dart | 3 +++ lib/src/writers/id3v4_writer.dart | 32 ++++++++++++++++++++++++++-- lib/src/writers/mp4_writer.dart | 2 +- lib/src/writers/riff_writer.dart | 7 +++++- 12 files changed, 97 insertions(+), 11 deletions(-) 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..8b4067c 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -143,7 +143,7 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { bitrate: riffMetadata.bitrate, duration: riffMetadata.duration, language: null, - lyrics: null, + lyrics: riffMetadata.comment, sampleRate: riffMetadata.samplerate, title: riffMetadata.title, totalDisc: null, @@ -224,6 +224,8 @@ ParserTag readAllMetadata(File track, {bool getImage = true}) { return OGGParser(fetchImage: getImage).parse(reader); } else if (ID3v2Parser.isID3v1(reader)) { return ID3v1Parser().parse(reader); + } else if (RiffParser.canUserParser(reader)) { + return RiffParser().parse(reader); } } catch (e, trace) { print(trace); diff --git a/lib/src/parsers/id3v2.dart b/lib/src/parsers/id3v2.dart index ea6e817..b133e98 100644 --- a/lib/src/parsers/id3v2.dart +++ b/lib/src/parsers/id3v2.dart @@ -207,7 +207,8 @@ class ID3v2Parser extends TagParser { processFrame(frame.id, frame.size); } - if (metadata.duration == null || metadata.duration == Duration.zero) { + // if (metadata.duration == null || metadata.duration == Duration.zero) { + if (true) { buffer.setPositionSync(size + 10); final mp3FrameHeader = _findFirstMp3Frame(buffer); diff --git a/lib/src/parsers/mp4.dart b/lib/src/parsers/mp4.dart index b83ff23..0a6cad1 100644 --- a/lib/src/parsers/mp4.dart +++ b/lib/src/parsers/mp4.dart @@ -126,12 +126,21 @@ class MP4Parser extends TagParser { /// /// The metadata are inside special boxes. We only read data when we need it /// otherwise we skip them + /// + + int mdatSize = 0; + int durationSeconds = 0; void processBox(Buffer buffer, BoxHeader box) { if (box.type == "moov") { parseRecurvise(buffer, box); + } else if (box.type == "mdat") { + buffer.setPositionSync(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]; - // version 0 has 100 bytes // version 1 has 112 bytes final bytes = buffer.read(version == 1 ? 111 : 99); @@ -149,6 +158,10 @@ 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); } else if (box.type == "ilst") { @@ -258,9 +271,16 @@ class MP4Parser extends TagParser { } } else if (box.type == "mp4a") { final bytes = buffer.read(box.size - 8); - // tags.bitrate = timeScale; tags.sampleRate = getUint32(bytes.sublist(22, 26)); + } else if (box.type == "alac") { + final bytes = 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); } @@ -275,8 +295,13 @@ class MP4Parser extends TagParser { if ("meta" == box.type) { offset += 4; } else if (box.type == "stsd") { - offset += 8; - buffer.read(8); + buffer.read(4); + int entryCount = buffer.readUint32(); + for (int i = 0; i < entryCount; i++) { + final newBox = _readBox(buffer); // 会读到 mp4a 或 alac + processBox(buffer, newBox); + } + return; } while (offset < limit) { diff --git a/lib/src/parsers/riff.dart b/lib/src/parsers/riff.dart index 7095b29..f558282 100644 --- a/lib/src/parsers/riff.dart +++ b/lib/src/parsers/riff.dart @@ -106,6 +106,7 @@ class RiffParser extends TagParser { metadata.year = DateTime.tryParse(subChunkData); break; case 'ICMT': + print(subChunkData); metadata.comment = subChunkData; break; case 'ITRK': diff --git a/lib/src/utils/buffer.dart b/lib/src/utils/buffer.dart index 2d7d0a5..c199602 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -138,3 +138,15 @@ class Buffer { } } } + + +extension BufferRead on Buffer { + int readUint32() { + final bytes = 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/writer.dart b/lib/src/writer.dart index 1b674e5..5d56ce6 100644 --- a/lib/src/writer.dart +++ b/lib/src/writer.dart @@ -21,14 +21,19 @@ void writeMetadata(File track, ParserTag metadata) { final reader = track.openSync(); if (ID3v2Parser.canUserParser(reader)) { + print(1); Id3v4Writer().write(track, metadata as Mp3Metadata); } else if (MP4Parser.canUserParser(reader)) { + print(2); Mp4Writer().write(track, metadata as Mp4Metadata); } else if (FlacParser.canUserParser(reader)) { + print(3); FlacWriter().write(track, metadata as VorbisMetadata); } else if (RiffParser.canUserParser(reader)) { + print(4); RiffWriter().write(track, metadata as RiffMetadata); } else if (ID3v1Parser.canUserParser(reader)) { + print(5); 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..f612ce1 100644 --- a/lib/src/writers/riff_writer.dart +++ b/lib/src/writers/riff_writer.dart @@ -14,6 +14,7 @@ class RiffWriter extends BaseMetadataWriter { @override void write(File file, RiffMetadata metadata) { this.metadata = metadata; + print('fuck:${metadata}'); final builder = BytesBuilder(); final reader = file.openSync(); @@ -29,7 +30,7 @@ class RiffWriter extends BaseMetadataWriter { builder.add(newData); reader.closeSync(); - File("a_new.wav").writeAsBytesSync(builder.toBytes()); + file.writeAsBytesSync(builder.toBytes()); } Uint8List _parseChunks() { @@ -84,6 +85,10 @@ class RiffWriter extends BaseMetadataWriter { if (metadata.copyright != null) { infoBuilder.add(_writeChunk("ICOP", metadata.copyright!)); } + print(metadata.lyric); + if (metadata.lyric != null) { + infoBuilder.add(_writeChunk("ICMT", metadata.lyric!)); + } final infoData = infoBuilder.toBytes(); // 4 bytes for "INFO" + INFO subchunks From c000f3a83f873dd61671850fc6466e60eced9150 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 20 Sep 2025 18:31:52 +0800 Subject: [PATCH 03/16] backup --- example/main.dart | 26 ++------------------------ 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/example/main.dart b/example/main.dart index 30584be..c2a34eb 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,36 +4,14 @@ import 'dart:typed_data'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; void main() { - final track = File("Free Loop - Daniel Powter.mp3"); + final track = File("9357CBDA-A004-4836-82B1-ABF03ADBC656.flac"); // Returns a condensate // Getting the image of a track can be heavy and slow the reading AudioMetadata metadata = readMetadata(track, getImage: true); - print(metadata); + // print(metadata); - // 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) { - metadata.setLyrics("I'm singing fuck"); - }, - ); - - print('===================='); - metadata = readMetadata(track, getImage: true); - - print(metadata); - print('lyric: ${metadata.lyrics}'); } From e730d0d6dec4cc5e9aac07377cd49a1eb81f4f35 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 20 Sep 2025 18:32:29 +0800 Subject: [PATCH 04/16] support ttml --- lib/src/parsers/flac.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/src/parsers/flac.dart b/lib/src/parsers/flac.dart index d35ba5d..bf88257 100644 --- a/lib/src/parsers/flac.dart +++ b/lib/src/parsers/flac.dart @@ -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': @@ -284,6 +284,7 @@ class FlacParser extends TagParser { metadata.discTotal = int.parse(value); break; case "LYRICS": + print(value); metadata.lyric = value; break; default: From 2c5d3fdf86378c88dfc41cf430eb043652b56533 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 20 Sep 2025 18:33:15 +0800 Subject: [PATCH 05/16] delete debug print --- lib/src/parsers/flac.dart | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/src/parsers/flac.dart b/lib/src/parsers/flac.dart index bf88257..80d9f91 100644 --- a/lib/src/parsers/flac.dart +++ b/lib/src/parsers/flac.dart @@ -284,7 +284,6 @@ class FlacParser extends TagParser { metadata.discTotal = int.parse(value); break; case "LYRICS": - print(value); metadata.lyric = value; break; default: From 823b9556929a06ae160150f17c720701ac17ba90 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sun, 21 Sep 2025 02:42:11 +0800 Subject: [PATCH 06/16] fix: mp3 ttml read --- example/main.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/example/main.dart b/example/main.dart index c2a34eb..6eecbb4 100644 --- a/example/main.dart +++ b/example/main.dart @@ -4,12 +4,12 @@ import 'dart:typed_data'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; void main() { - final track = File("9357CBDA-A004-4836-82B1-ABF03ADBC656.flac"); + final track = File("Carpenters - Yesterday Once More.mp3"); // Returns a condensate // Getting the image of a track can be heavy and slow the reading AudioMetadata metadata = readMetadata(track, getImage: true); - +// print(metadata); // print(metadata); } From ba286c814147b3be6898e1fdb05a713c596f308d Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sun, 21 Sep 2025 03:03:02 +0800 Subject: [PATCH 07/16] fix: mp3 ttml read --- lib/src/parsers/id3v2.dart | 81 ++++++++++++++------------------------ 1 file changed, 29 insertions(+), 52 deletions(-) diff --git a/lib/src/parsers/id3v2.dart b/lib/src/parsers/id3v2.dart index b133e98..28989f1 100644 --- a/lib/src/parsers/id3v2.dart +++ b/lib/src/parsers/id3v2.dart @@ -654,65 +654,42 @@ class ID3v2Parser extends TagParser { } String getUnsynchronisedLyric(Uint8List content) { - int offset = 1; + if (content.isEmpty) return ""; - final reader = ByteData.sublistView(content); - final encoding = reader.getInt8(0); - - // skip language - offset += 3; - - final description = [reader.getInt8(offset)]; - - while (description.last != 0) { - description.add(reader.getInt8(offset)); - offset++; - } + int offset = 1; // encoding + final reader = ByteData.sublistView(content); + final encoding = reader.getInt8(0); - if (encoding == 1 || encoding == 2) { - while (reader.getInt8(offset) == 0) { - offset++; - } - } + // 跳过 language (3 bytes) + offset += 3; - final rest = reader.buffer.asUint8List(offset); + // description 字节,以 0 结束 + while (offset < content.length && reader.getInt8(offset) != 0) { + offset++; + } + offset++; // 跳过 description 的 null - 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++; - } + // 跳过可能的 padding 0 + while ((encoding == 1 || encoding == 2) && offset < content.length && reader.getInt8(offset) == 0) { + offset++; + } - 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); - } + // 剩余就是歌词内容 + final rest = content.sublist(offset); - return ""; + switch (encoding) { + 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 ""; } +} /// /// To detect if this file can be parsed with this parser, the first 3 bytes From b81cb9b81bd841878a60ca9c7a203d16bdd44a7d Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 1 Nov 2025 01:14:11 +0800 Subject: [PATCH 08/16] Supports reading music metadata from WebDAV. --- example/main.dart | 29 +++++++++++++++++++---------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/example/main.dart b/example/main.dart index 6eecbb4..f2fd3af 100644 --- a/example/main.dart +++ b/example/main.dart @@ -2,16 +2,25 @@ import 'dart:io'; import 'dart:typed_data'; import 'package:audio_metadata_reader/audio_metadata_reader.dart'; +import 'package:audio_metadata_reader/src/parser.dart'; +import 'package:webdav_client/webdav_client.dart' as webdav; -void main() { - final track = File("Carpenters - Yesterday Once More.mp3"); - - // Returns a condensate - // Getting the image of a track can be heavy and slow the reading - AudioMetadata metadata = readMetadata(track, getImage: true); -// print(metadata); - // print(metadata); +final client = webdav.newClient( + "https://xxx.xxx.xx/dav", + user: 'xxx', + password: 'xxxxxx', +); +void main() async { + final dirs = await client.readDir('/public/Music'); + int i = 0 ; + dirs.forEach((v) { + readMetadataWebDav(client, v.path!, getImage: true).then((res) { + print(res); + i++; + print(i); + }).catchError((v){ + print(v); + }); + }); } - - From bb816a632d5e7da799174b5080e2fc5caeebcbf8 Mon Sep 17 00:00:00 2001 From: GerryDush Date: Sat, 1 Nov 2025 01:14:21 +0800 Subject: [PATCH 09/16] Supports reading music metadata from WebDAV. --- lib/src/parser.dart | 230 ++++++++++++++++++++--- lib/src/parsers/flac.dart | 52 +++--- lib/src/parsers/id3v1.dart | 32 ++-- lib/src/parsers/id3v2.dart | 302 ++++++++++++++++--------------- lib/src/parsers/mp4.dart | 84 ++++----- lib/src/parsers/ogg.dart | 42 ++--- lib/src/parsers/riff.dart | 36 ++-- lib/src/parsers/tag_parser.dart | 4 +- lib/src/utils/buffer.dart | 170 ++++++++++++++--- lib/src/writer.dart | 20 +- lib/src/writers/riff_writer.dart | 27 ++- pubspec.yaml | 5 + 12 files changed, 664 insertions(+), 340 deletions(-) diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 8b4067c..210b659 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -3,6 +3,8 @@ import 'dart:io'; 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. /// @@ -14,13 +16,13 @@ import 'package:audio_metadata_reader/src/metadata/base.dart'; /// 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 readMetadata(File track, {bool getImage = false}) async { + final reader = FileRandomAccessFile(randomAccessFile: track.openSync()); + 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, @@ -52,9 +54,9 @@ 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, @@ -78,7 +80,7 @@ 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; @@ -108,7 +110,7 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { } return newMetadata; - } else if (OGGParser.canUserParser(reader)) { + } else if (await OGGParser.canUserParser(reader)) { final oggMetadata = OGGParser(fetchImage: getImage).parse(reader) as VorbisMetadata; @@ -133,7 +135,7 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { newMetadata.performers.addAll(oggMetadata.performer); return newMetadata; - } else if (RiffParser.canUserParser(reader)) { + } else if (await RiffParser.canUserParser(reader)) { final riffMetadata = RiffParser().parse(reader) as RiffMetadata; final newMetadata = AudioMetadata( @@ -158,7 +160,7 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { (riffMetadata.genre != null) ? [riffMetadata.genre!] : []; return newMetadata; - } else if (ID3v1Parser.canUserParser(reader)) { + } else if (await ID3v1Parser.canUserParser(reader)) { final mp3Metadata = ID3v1Parser().parse(reader) as Mp3Metadata; final newMetadata = AudioMetadata( @@ -210,21 +212,21 @@ AudioMetadata readMetadata(File track, {bool getImage = false}) { /// 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(File track, {bool getImage = true}) async { + final reader = FileRandomAccessFile(randomAccessFile: track.openSync()); try { - if (ID3v2Parser.canUserParser(reader)) { + if (await ID3v2Parser.canUserParser(reader)) { return ID3v2Parser(fetchImage: getImage).parse(reader); - } else if (FlacParser.canUserParser(reader)) { + } else if (await FlacParser.canUserParser(reader)) { return FlacParser(fetchImage: getImage).parse(reader); - } else if (MP4Parser.canUserParser(reader)) { + } else if (await MP4Parser.canUserParser(reader)) { return MP4Parser(fetchImage: getImage).parse(reader); - } else if (OGGParser.canUserParser(reader)) { + } else if (await OGGParser.canUserParser(reader)) { return OGGParser(fetchImage: getImage).parse(reader); - } else if (ID3v2Parser.isID3v1(reader)) { + } else if (await ID3v2Parser.isID3v1(reader)) { return ID3v1Parser().parse(reader); - } else if (RiffParser.canUserParser(reader)) { + } else if (await RiffParser.canUserParser(reader)) { return RiffParser().parse(reader); } } catch (e, trace) { @@ -238,3 +240,189 @@ ParserTag readAllMetadata(File track, {bool getImage = true}) { "No available parser for this file. Please raise an issue in Github", ); } + + +Future readMetadataWebDav(webdav.Client client,String path, {bool getImage = false}) async { + final reader = WebDavRandomAccessFile(client: client,path: path); + + try { + if (await ID3v2Parser.canUserParser(reader)) { + final mp3Metadata = (await ID3v2Parser(fetchImage: getImage) + .parse(reader)) as Mp3Metadata; + + final a = AudioMetadata( + file: File(''), + album: mp3Metadata.album, + artist: mp3Metadata.bandOrOrchestra ?? + mp3Metadata.leadPerformer ?? + mp3Metadata.originalArtist, + bitrate: mp3Metadata.bitrate, + duration: mp3Metadata.duration, + language: mp3Metadata.languages, + lyrics: mp3Metadata.lyric, + sampleRate: mp3Metadata.samplerate, + title: mp3Metadata.songName, + totalDisc: mp3Metadata.totalDics, + trackNumber: mp3Metadata.trackNumber, + trackTotal: mp3Metadata.trackTotal, + year: + DateTime(mp3Metadata.originalReleaseYear ?? mp3Metadata.year ?? 0), + discNumber: mp3Metadata.discNumber, + ); + + a.pictures = mp3Metadata.pictures; + a.genres = mp3Metadata.genres; + + final guestArtistFrame = mp3Metadata.customMetadata["GUEST ARTIST"]; + + if (guestArtistFrame != null) { + a.performers.addAll(guestArtistFrame.split("/")); + } + + return a; + } else if (await FlacParser.canUserParser(reader)) { + final vorbisMetadata = (await FlacParser(fetchImage: getImage) + .parse(reader)) as VorbisMetadata; + + final newMetadata = AudioMetadata( + file: File(''), + album: vorbisMetadata.album.firstOrNull, + artist: vorbisMetadata.artist.firstOrNull, + bitrate: vorbisMetadata.bitrate, + discNumber: vorbisMetadata.discNumber, + duration: vorbisMetadata.duration, + language: vorbisMetadata.artist.firstOrNull, + lyrics: vorbisMetadata.lyric, + sampleRate: vorbisMetadata.sampleRate, + title: vorbisMetadata.title.firstOrNull, + totalDisc: vorbisMetadata.discTotal, + trackNumber: vorbisMetadata.trackNumber.firstOrNull, + trackTotal: vorbisMetadata.trackTotal, + year: vorbisMetadata.date.firstOrNull, + ); + + newMetadata.genres = vorbisMetadata.genres; + newMetadata.pictures = vorbisMetadata.pictures; + newMetadata.performers.addAll(vorbisMetadata.performer); + + return newMetadata; + } else if (await MP4Parser.canUserParser(reader)) { + final mp4Metadata = + (await MP4Parser(fetchImage: getImage).parse(reader)) as Mp4Metadata; + + final newMetadata = AudioMetadata( + file: File(''), + album: mp4Metadata.album, + artist: mp4Metadata.artist, + bitrate: mp4Metadata.bitrate, + discNumber: mp4Metadata.discNumber, + duration: mp4Metadata.duration, + language: null, + lyrics: mp4Metadata.lyrics, + sampleRate: mp4Metadata.sampleRate, + title: mp4Metadata.title, + totalDisc: mp4Metadata.totalDiscs, + trackNumber: mp4Metadata.trackNumber, + trackTotal: mp4Metadata.totalTracks, + year: mp4Metadata.year, + ); + + if (mp4Metadata.picture != null) { + newMetadata.pictures.add(mp4Metadata.picture!); + } + + if (mp4Metadata.genre != null) { + newMetadata.genres.add(mp4Metadata.genre!); + } + + return newMetadata; + } else if (await OGGParser.canUserParser(reader)) { + final oggMetadata = + (await OGGParser(fetchImage: getImage).parse(reader)) as VorbisMetadata; + + final newMetadata = AudioMetadata( + file: File(''), + album: oggMetadata.album.firstOrNull, + artist: oggMetadata.artist.firstOrNull, + bitrate: oggMetadata.bitrate, + discNumber: oggMetadata.discNumber, + duration: oggMetadata.duration, + language: oggMetadata.artist.firstOrNull, + lyrics: oggMetadata.lyric, + sampleRate: oggMetadata.sampleRate, + title: oggMetadata.title.firstOrNull, + totalDisc: oggMetadata.discTotal, + trackNumber: oggMetadata.trackNumber.firstOrNull, + trackTotal: oggMetadata.trackTotal, + year: oggMetadata.date.firstOrNull, + ); + newMetadata.genres = oggMetadata.genres; + newMetadata.pictures.addAll(oggMetadata.pictures); + newMetadata.performers.addAll(oggMetadata.performer); + + return newMetadata; + } else if (await RiffParser.canUserParser(reader)) { + final riffMetadata = (await RiffParser().parse(reader)) as RiffMetadata; + + final newMetadata = AudioMetadata( + file: File(''), + album: riffMetadata.album, + artist: riffMetadata.artist, + bitrate: riffMetadata.bitrate, + duration: riffMetadata.duration, + language: null, + lyrics: riffMetadata.comment, + sampleRate: riffMetadata.samplerate, + title: riffMetadata.title, + totalDisc: null, + trackNumber: riffMetadata.trackNumber, + trackTotal: null, + year: riffMetadata.year, + discNumber: null, + ); + + newMetadata.pictures = riffMetadata.pictures; + newMetadata.genres = + (riffMetadata.genre != null) ? [riffMetadata.genre!] : []; + + return newMetadata; + } else if (await ID3v1Parser.canUserParser(reader)) { + final mp3Metadata = (await ID3v1Parser().parse(reader)) as Mp3Metadata; + + final newMetadata = AudioMetadata( + file: File(''), + album: mp3Metadata.album, + artist: mp3Metadata.bandOrOrchestra ?? + mp3Metadata.originalArtist ?? + mp3Metadata.leadPerformer, + bitrate: mp3Metadata.bitrate, + duration: mp3Metadata.duration, + language: mp3Metadata.languages, + lyrics: mp3Metadata.lyric, + sampleRate: mp3Metadata.samplerate, + title: mp3Metadata.songName, + totalDisc: mp3Metadata.totalDics, + trackNumber: mp3Metadata.trackNumber, + trackTotal: mp3Metadata.trackTotal, + year: + DateTime(mp3Metadata.originalReleaseYear ?? mp3Metadata.year ?? 0), + discNumber: mp3Metadata.discNumber, + ); + + newMetadata.pictures = mp3Metadata.pictures; + newMetadata.genres = mp3Metadata.genres; + + return newMetadata; + } + } on MetadataParserException catch (e, s) { + Error.throwWithStackTrace( + MetadataParserException(track: File(''), message: e.message), s); + } catch (e, s) { + Error.throwWithStackTrace( + MetadataParserException(track: File(''), message: e.toString()), s); + } + + throw NoMetadataParserException( + track: File(''), + ); +} \ No newline at end of file diff --git a/lib/src/parsers/flac.dart b/lib/src/parsers/flac.dart index 80d9f91..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"; } 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 28989f1..366c6f9 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,14 @@ 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); + print(1222); + final frame = await getFrame(reader, majorVersion == 4); if (frame == null) { break; @@ -204,17 +206,21 @@ class ID3v2Parser extends TagParser { // 10 -> frame header offset = offset + 10 + frame.size; - processFrame(frame.id, frame.size); + + await processFrame(frame.id, frame.size); + } + print(14444); // if (metadata.duration == null || metadata.duration == Duration.zero) { if (true) { - buffer.setPositionSync(size + 10); + + await buffer.setPosition(size + 10); - final mp3FrameHeader = _findFirstMp3Frame(buffer); + final mp3FrameHeader = await _findFirstMp3Frame(buffer); if (mp3FrameHeader == null) { - reader.closeSync(); + await reader.close(); return metadata; } @@ -241,7 +247,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) { @@ -275,7 +281,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!; @@ -286,7 +292,7 @@ class ID3v2Parser extends TagParser { } } - reader.closeSync(); + await reader.close(); return metadata; } @@ -302,8 +308,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) @@ -317,7 +323,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; } @@ -332,7 +338,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; } @@ -342,7 +348,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; @@ -351,36 +357,36 @@ 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 - +print(frameId); 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";|/|\||,"); @@ -398,52 +404,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) { @@ -454,68 +460,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; @@ -528,12 +534,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) { @@ -549,45 +555,47 @@ 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 { + print(1101001); + await buffer.skip(size); + print(20020); } }; - 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; @@ -654,59 +662,61 @@ class ID3v2Parser extends TagParser { } String getUnsynchronisedLyric(Uint8List content) { - if (content.isEmpty) return ""; + if (content.isEmpty) return ""; - int offset = 1; // encoding - final reader = ByteData.sublistView(content); - final encoding = reader.getInt8(0); + int offset = 1; // encoding + final reader = ByteData.sublistView(content); + final encoding = reader.getInt8(0); - // 跳过 language (3 bytes) - offset += 3; + // 跳过 language (3 bytes) + offset += 3; - // description 字节,以 0 结束 - while (offset < content.length && reader.getInt8(offset) != 0) { - offset++; - } - offset++; // 跳过 description 的 null + // description 字节,以 0 结束 + while (offset < content.length && reader.getInt8(offset) != 0) { + offset++; + } + offset++; // 跳过 description 的 null - // 跳过可能的 padding 0 - while ((encoding == 1 || encoding == 2) && offset < content.length && reader.getInt8(offset) == 0) { - offset++; - } + // 跳过可能的 padding 0 + while ((encoding == 1 || encoding == 2) && + offset < content.length && + reader.getInt8(offset) == 0) { + offset++; + } - // 剩余就是歌词内容 - final rest = content.sublist(offset); + // 剩余就是歌词内容 + final rest = content.sublist(offset); - switch (encoding) { - 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 ""; + switch (encoding) { + 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 ""; + } } -} /// /// 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 0a6cad1..3edb200 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); @@ -130,20 +130,20 @@ class MP4Parser extends TagParser { int mdatSize = 0; int durationSeconds = 0; - void processBox(Buffer buffer, BoxHeader box) { + Future processBox(Buffer buffer, BoxHeader box) async { if (box.type == "moov") { - parseRecurvise(buffer, box); + await parseRecurvise(buffer, box); } else if (box.type == "mdat") { - buffer.setPositionSync(buffer.fileCursor + box.size - 8); + 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; @@ -163,26 +163,26 @@ class MP4Parser extends TagParser { 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) @@ -249,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) { @@ -270,11 +270,11 @@ 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 = buffer.read(box.size - 8); // 减去 header + final bytes = await buffer.read(box.size - 8); // 减去 header final sampleRate = ((bytes[24] << 24) | (bytes[25] << 16) | (bytes[26] << 8) | @@ -282,12 +282,12 @@ class MP4Parser extends TagParser { 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; @@ -295,22 +295,22 @@ class MP4Parser extends TagParser { if ("meta" == box.type) { offset += 4; } else if (box.type == "stsd") { - buffer.read(4); - int entryCount = buffer.readUint32(); + await buffer.read(4); + int entryCount =await buffer.readUint32(); for (int i = 0; i < entryCount; i++) { - final newBox = _readBox(buffer); // 会读到 mp4a 或 alac - processBox(buffer, newBox); + 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; @@ -319,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 f558282..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; @@ -125,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 c199602..918a536 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -3,9 +3,119 @@ import 'dart:math'; import 'dart:typed_data'; 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 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 +132,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; } @@ -48,7 +163,7 @@ class Buffer { } } - 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 +175,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; } @@ -107,18 +222,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,20 +244,27 @@ 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 { - int readUint32() { - final bytes = read(4); + Future readUint32() async { + final bytes = await read(4); // MP4 box 是 big-endian return (bytes[0] << 24) | (bytes[1] << 16) | diff --git a/lib/src/writer.dart b/lib/src/writer.dart index 5d56ce6..2343b8c 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,8 +8,8 @@ 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); @@ -17,22 +17,22 @@ void updateMetadata(File track, void Function(ParserTag metadata) updater) { } /// 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)) { print(1); Id3v4Writer().write(track, metadata as Mp3Metadata); - } else if (MP4Parser.canUserParser(reader)) { + } else if (await MP4Parser.canUserParser(reader)) { print(2); Mp4Writer().write(track, metadata as Mp4Metadata); - } else if (FlacParser.canUserParser(reader)) { + } else if (await FlacParser.canUserParser(reader)) { print(3); FlacWriter().write(track, metadata as VorbisMetadata); - } else if (RiffParser.canUserParser(reader)) { + } else if (await RiffParser.canUserParser(reader)) { print(4); RiffWriter().write(track, metadata as RiffMetadata); - } else if (ID3v1Parser.canUserParser(reader)) { + } else if (await ID3v1Parser.canUserParser(reader)) { print(5); ID3v1Writer().write(track, metadata as Mp3Metadata); } diff --git a/lib/src/writers/riff_writer.dart b/lib/src/writers/riff_writer.dart index f612ce1..57f1108 100644 --- a/lib/src/writers/riff_writer.dart +++ b/lib/src/writers/riff_writer.dart @@ -12,17 +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; - print('fuck:${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)); @@ -33,25 +32,25 @@ class RiffWriter extends BaseMetadataWriter { 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(); @@ -101,16 +100,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..77b8234 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -4,6 +4,7 @@ version: 1.4.2 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 From 61bb529ad5358e4702de0edf62853e05fc3d3492 Mon Sep 17 00:00:00 2001 From: aa Date: Tue, 16 Dec 2025 14:59:11 +0800 Subject: [PATCH 10/16] fix: fix --- example/main.dart | 5 ++++- lib/src/parsers/id3v2.dart | 5 ----- lib/src/writer.dart | 5 ----- lib/src/writers/riff_writer.dart | 1 - 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/example/main.dart b/example/main.dart index f2fd3af..15dcb70 100644 --- a/example/main.dart +++ b/example/main.dart @@ -14,13 +14,16 @@ final client = webdav.newClient( void main() async { final dirs = await client.readDir('/public/Music'); int i = 0 ; + int j = 0 ; dirs.forEach((v) { readMetadataWebDav(client, v.path!, getImage: true).then((res) { - print(res); + // print(res); i++; print(i); }).catchError((v){ print(v); }); + j++; + print('j: $j'); }); } diff --git a/lib/src/parsers/id3v2.dart b/lib/src/parsers/id3v2.dart index 366c6f9..425ce53 100644 --- a/lib/src/parsers/id3v2.dart +++ b/lib/src/parsers/id3v2.dart @@ -197,7 +197,6 @@ class ID3v2Parser extends TagParser { while (offset < size) { - print(1222); final frame = await getFrame(reader, majorVersion == 4); if (frame == null) { @@ -210,7 +209,6 @@ class ID3v2Parser extends TagParser { await processFrame(frame.id, frame.size); } - print(14444); // if (metadata.duration == null || metadata.duration == Duration.zero) { if (true) { @@ -362,7 +360,6 @@ class ID3v2Parser extends TagParser { // 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 -print(frameId); final handlers = switch (frameId) { "APIC" => () async { if (fetchImage) { @@ -585,9 +582,7 @@ print(frameId); metadata.encoderSoftware = getTextFromFrame(content); }, _ => () async { - print(1101001); await buffer.skip(size); - print(20020); } }; diff --git a/lib/src/writer.dart b/lib/src/writer.dart index 2343b8c..a130841 100644 --- a/lib/src/writer.dart +++ b/lib/src/writer.dart @@ -21,19 +21,14 @@ Future writeMetadata(File track, ParserTag metadata) async{ final reader = FileRandomAccessFile(randomAccessFile: track.openSync()); if (await ID3v2Parser.canUserParser(reader)) { - print(1); Id3v4Writer().write(track, metadata as Mp3Metadata); } else if (await MP4Parser.canUserParser(reader)) { - print(2); Mp4Writer().write(track, metadata as Mp4Metadata); } else if (await FlacParser.canUserParser(reader)) { - print(3); FlacWriter().write(track, metadata as VorbisMetadata); } else if (await RiffParser.canUserParser(reader)) { - print(4); RiffWriter().write(track, metadata as RiffMetadata); } else if (await ID3v1Parser.canUserParser(reader)) { - print(5); ID3v1Writer().write(track, metadata as Mp3Metadata); } } diff --git a/lib/src/writers/riff_writer.dart b/lib/src/writers/riff_writer.dart index 57f1108..ccbfda9 100644 --- a/lib/src/writers/riff_writer.dart +++ b/lib/src/writers/riff_writer.dart @@ -84,7 +84,6 @@ class RiffWriter extends BaseMetadataWriter { if (metadata.copyright != null) { infoBuilder.add(_writeChunk("ICOP", metadata.copyright!)); } - print(metadata.lyric); if (metadata.lyric != null) { infoBuilder.add(_writeChunk("ICMT", metadata.lyric!)); } From f08864ed89d41ba1a0bec192115650eab96b1f70 Mon Sep 17 00:00:00 2001 From: aa Date: Sat, 20 Dec 2025 02:25:38 +0800 Subject: [PATCH 11/16] fix --- lib/src/parser.dart | 20 ++++++++++---------- lib/src/utils/buffer.dart | 4 ++-- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 210b659..ce9bfda 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -82,7 +82,7 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } 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, @@ -112,7 +112,7 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } else if (await OGGParser.canUserParser(reader)) { final oggMetadata = - OGGParser(fetchImage: getImage).parse(reader) as VorbisMetadata; + await OGGParser(fetchImage: getImage).parse(reader) as VorbisMetadata; final newMetadata = AudioMetadata( file: track, @@ -136,7 +136,7 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } else if (await RiffParser.canUserParser(reader)) { - final riffMetadata = RiffParser().parse(reader) as RiffMetadata; + final riffMetadata = await RiffParser().parse(reader) as RiffMetadata; final newMetadata = AudioMetadata( file: track, @@ -161,7 +161,7 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } else if (await ID3v1Parser.canUserParser(reader)) { - final mp3Metadata = ID3v1Parser().parse(reader) as Mp3Metadata; + final mp3Metadata = await ID3v1Parser().parse(reader) as Mp3Metadata; final newMetadata = AudioMetadata( file: track, @@ -217,17 +217,17 @@ Future readAllMetadata(File track, {bool getImage = true}) async { try { if (await ID3v2Parser.canUserParser(reader)) { - return ID3v2Parser(fetchImage: getImage).parse(reader); + return await ID3v2Parser(fetchImage: getImage).parse(reader); } else if (await FlacParser.canUserParser(reader)) { - return FlacParser(fetchImage: getImage).parse(reader); + return await FlacParser(fetchImage: getImage).parse(reader); } else if (await MP4Parser.canUserParser(reader)) { - return MP4Parser(fetchImage: getImage).parse(reader); + return await MP4Parser(fetchImage: getImage).parse(reader); } else if (await OGGParser.canUserParser(reader)) { - return OGGParser(fetchImage: getImage).parse(reader); + return await OGGParser(fetchImage: getImage).parse(reader); } else if (await ID3v2Parser.isID3v1(reader)) { - return ID3v1Parser().parse(reader); + return await ID3v1Parser().parse(reader); } else if (await RiffParser.canUserParser(reader)) { - return RiffParser().parse(reader); + return await RiffParser().parse(reader); } } catch (e, trace) { print(trace); diff --git a/lib/src/utils/buffer.dart b/lib/src/utils/buffer.dart index 918a536..1afe1a2 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -193,7 +193,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 @@ -208,7 +208,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(); From 86d4d68cf2f986c7a35de11a147fd7500dddfc02 Mon Sep 17 00:00:00 2001 From: aa Date: Sat, 20 Dec 2025 02:28:37 +0800 Subject: [PATCH 12/16] fix --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index 77b8234..ce0376d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ 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.6 homepage: https://clementbeal.github.io/ repository: https://github.com/ClementBeal/audio_metadata_reader issue_tracker: https://github.com/ClementBeal/audio_metadata_reader/issues From 9e812e7dc3cba16875cc72bd44c8ddb3188a09e9 Mon Sep 17 00:00:00 2001 From: lyp Date: Fri, 6 Feb 2026 01:29:39 +0800 Subject: [PATCH 13/16] fix --- example/main.dart | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/example/main.dart b/example/main.dart index 15dcb70..7498e21 100644 --- a/example/main.dart +++ b/example/main.dart @@ -12,18 +12,9 @@ final client = webdav.newClient( ); void main() async { - final dirs = await client.readDir('/public/Music'); - int i = 0 ; - int j = 0 ; - dirs.forEach((v) { - readMetadataWebDav(client, v.path!, getImage: true).then((res) { - // print(res); - i++; - print(i); - }).catchError((v){ - print(v); - }); - j++; - print('j: $j'); - }); + File file = File('/Users/lyp/Music/LZF-Music/After Hours - The Weeknd.flac'); + Uint8List data = file.readAsBytesSync(); + print(data.length); + final metadata = await readMetadataUint8List(data, getImage: true); + print(metadata); } From 4fa42f6e40520842d17d6f4b5ce7d6e1c4fb8472 Mon Sep 17 00:00:00 2001 From: lyp Date: Fri, 6 Feb 2026 01:31:05 +0800 Subject: [PATCH 14/16] fix --- lib/src/parser.dart | 268 +++---------------- lib/src/parsers/mp4.dart | 2 +- lib/src/utils/buffer.dart | 86 +++++- lib/src/utils/metadata_parser_exception.dart | 21 +- lib/src/writer.dart | 6 +- pubspec.yaml | 2 +- 6 files changed, 142 insertions(+), 243 deletions(-) diff --git a/lib/src/parser.dart b/lib/src/parser.dart index 210b659..457c579 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -1,4 +1,5 @@ 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'; @@ -6,26 +7,35 @@ 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. -Future readMetadata(File track, {bool getImage = false}) async { - final reader = FileRandomAccessFile(randomAccessFile: 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(Uint8List data,{bool getImage = false}) async { + final reader = Uint8ListRandomAccessFile(data: data); + return readMetadata(reader, getImage: getImage); +} + + + +Future readMetadata(MyRandomAccessFile reader, + {bool getImage = false}) async { try { 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 ?? @@ -59,7 +69,7 @@ Future readMetadata(File track, {bool getImage = false}) async { .parse(reader)) as VorbisMetadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: vorbisMetadata.album.firstOrNull, artist: vorbisMetadata.artist.firstOrNull, bitrate: vorbisMetadata.bitrate, @@ -82,10 +92,10 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } 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, @@ -111,11 +121,11 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } else if (await OGGParser.canUserParser(reader)) { - final oggMetadata = - OGGParser(fetchImage: getImage).parse(reader) as VorbisMetadata; + 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, @@ -136,10 +146,10 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } else if (await RiffParser.canUserParser(reader)) { - final riffMetadata = RiffParser().parse(reader) as RiffMetadata; + final riffMetadata = (await RiffParser().parse(reader)) as RiffMetadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: riffMetadata.album, artist: riffMetadata.artist, bitrate: riffMetadata.bitrate, @@ -161,10 +171,10 @@ Future readMetadata(File track, {bool getImage = false}) async { return newMetadata; } else if (await ID3v1Parser.canUserParser(reader)) { - final mp3Metadata = ID3v1Parser().parse(reader) as Mp3Metadata; + final mp3Metadata = (await ID3v1Parser().parse(reader)) as Mp3Metadata; final newMetadata = AudioMetadata( - file: track, + file: File(''), album: mp3Metadata.album, artist: mp3Metadata.bandOrOrchestra ?? mp3Metadata.originalArtist ?? @@ -190,31 +200,19 @@ Future readMetadata(File track, {bool getImage = false}) async { } } 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 -Future readAllMetadata(File track, {bool getImage = true}) async { - final reader = FileRandomAccessFile(randomAccessFile: track.openSync()); +Future readAllMetadata(MyRandomAccessFile reader, {bool getImage = true}) async { try { if (await ID3v2Parser.canUserParser(reader)) { return ID3v2Parser(fetchImage: getImage).parse(reader); @@ -231,198 +229,12 @@ Future readAllMetadata(File track, {bool getImage = true}) async { } } 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", ); } - - -Future readMetadataWebDav(webdav.Client client,String path, {bool getImage = false}) async { - final reader = WebDavRandomAccessFile(client: client,path: path); - - try { - if (await ID3v2Parser.canUserParser(reader)) { - final mp3Metadata = (await ID3v2Parser(fetchImage: getImage) - .parse(reader)) as Mp3Metadata; - - final a = AudioMetadata( - file: File(''), - album: mp3Metadata.album, - artist: mp3Metadata.bandOrOrchestra ?? - mp3Metadata.leadPerformer ?? - mp3Metadata.originalArtist, - bitrate: mp3Metadata.bitrate, - duration: mp3Metadata.duration, - language: mp3Metadata.languages, - lyrics: mp3Metadata.lyric, - sampleRate: mp3Metadata.samplerate, - title: mp3Metadata.songName, - totalDisc: mp3Metadata.totalDics, - trackNumber: mp3Metadata.trackNumber, - trackTotal: mp3Metadata.trackTotal, - year: - DateTime(mp3Metadata.originalReleaseYear ?? mp3Metadata.year ?? 0), - discNumber: mp3Metadata.discNumber, - ); - - a.pictures = mp3Metadata.pictures; - a.genres = mp3Metadata.genres; - - final guestArtistFrame = mp3Metadata.customMetadata["GUEST ARTIST"]; - - if (guestArtistFrame != null) { - a.performers.addAll(guestArtistFrame.split("/")); - } - - return a; - } else if (await FlacParser.canUserParser(reader)) { - final vorbisMetadata = (await FlacParser(fetchImage: getImage) - .parse(reader)) as VorbisMetadata; - - final newMetadata = AudioMetadata( - file: File(''), - album: vorbisMetadata.album.firstOrNull, - artist: vorbisMetadata.artist.firstOrNull, - bitrate: vorbisMetadata.bitrate, - discNumber: vorbisMetadata.discNumber, - duration: vorbisMetadata.duration, - language: vorbisMetadata.artist.firstOrNull, - lyrics: vorbisMetadata.lyric, - sampleRate: vorbisMetadata.sampleRate, - title: vorbisMetadata.title.firstOrNull, - totalDisc: vorbisMetadata.discTotal, - trackNumber: vorbisMetadata.trackNumber.firstOrNull, - trackTotal: vorbisMetadata.trackTotal, - year: vorbisMetadata.date.firstOrNull, - ); - - newMetadata.genres = vorbisMetadata.genres; - newMetadata.pictures = vorbisMetadata.pictures; - newMetadata.performers.addAll(vorbisMetadata.performer); - - return newMetadata; - } else if (await MP4Parser.canUserParser(reader)) { - final mp4Metadata = - (await MP4Parser(fetchImage: getImage).parse(reader)) as Mp4Metadata; - - final newMetadata = AudioMetadata( - file: File(''), - album: mp4Metadata.album, - artist: mp4Metadata.artist, - bitrate: mp4Metadata.bitrate, - discNumber: mp4Metadata.discNumber, - duration: mp4Metadata.duration, - language: null, - lyrics: mp4Metadata.lyrics, - sampleRate: mp4Metadata.sampleRate, - title: mp4Metadata.title, - totalDisc: mp4Metadata.totalDiscs, - trackNumber: mp4Metadata.trackNumber, - trackTotal: mp4Metadata.totalTracks, - year: mp4Metadata.year, - ); - - if (mp4Metadata.picture != null) { - newMetadata.pictures.add(mp4Metadata.picture!); - } - - if (mp4Metadata.genre != null) { - newMetadata.genres.add(mp4Metadata.genre!); - } - - return newMetadata; - } else if (await OGGParser.canUserParser(reader)) { - final oggMetadata = - (await OGGParser(fetchImage: getImage).parse(reader)) as VorbisMetadata; - - final newMetadata = AudioMetadata( - file: File(''), - album: oggMetadata.album.firstOrNull, - artist: oggMetadata.artist.firstOrNull, - bitrate: oggMetadata.bitrate, - discNumber: oggMetadata.discNumber, - duration: oggMetadata.duration, - language: oggMetadata.artist.firstOrNull, - lyrics: oggMetadata.lyric, - sampleRate: oggMetadata.sampleRate, - title: oggMetadata.title.firstOrNull, - totalDisc: oggMetadata.discTotal, - trackNumber: oggMetadata.trackNumber.firstOrNull, - trackTotal: oggMetadata.trackTotal, - year: oggMetadata.date.firstOrNull, - ); - newMetadata.genres = oggMetadata.genres; - newMetadata.pictures.addAll(oggMetadata.pictures); - newMetadata.performers.addAll(oggMetadata.performer); - - return newMetadata; - } else if (await RiffParser.canUserParser(reader)) { - final riffMetadata = (await RiffParser().parse(reader)) as RiffMetadata; - - final newMetadata = AudioMetadata( - file: File(''), - album: riffMetadata.album, - artist: riffMetadata.artist, - bitrate: riffMetadata.bitrate, - duration: riffMetadata.duration, - language: null, - lyrics: riffMetadata.comment, - sampleRate: riffMetadata.samplerate, - title: riffMetadata.title, - totalDisc: null, - trackNumber: riffMetadata.trackNumber, - trackTotal: null, - year: riffMetadata.year, - discNumber: null, - ); - - newMetadata.pictures = riffMetadata.pictures; - newMetadata.genres = - (riffMetadata.genre != null) ? [riffMetadata.genre!] : []; - - return newMetadata; - } else if (await ID3v1Parser.canUserParser(reader)) { - final mp3Metadata = (await ID3v1Parser().parse(reader)) as Mp3Metadata; - - final newMetadata = AudioMetadata( - file: File(''), - album: mp3Metadata.album, - artist: mp3Metadata.bandOrOrchestra ?? - mp3Metadata.originalArtist ?? - mp3Metadata.leadPerformer, - bitrate: mp3Metadata.bitrate, - duration: mp3Metadata.duration, - language: mp3Metadata.languages, - lyrics: mp3Metadata.lyric, - sampleRate: mp3Metadata.samplerate, - title: mp3Metadata.songName, - totalDisc: mp3Metadata.totalDics, - trackNumber: mp3Metadata.trackNumber, - trackTotal: mp3Metadata.trackTotal, - year: - DateTime(mp3Metadata.originalReleaseYear ?? mp3Metadata.year ?? 0), - discNumber: mp3Metadata.discNumber, - ); - - newMetadata.pictures = mp3Metadata.pictures; - newMetadata.genres = mp3Metadata.genres; - - return newMetadata; - } - } on MetadataParserException catch (e, s) { - Error.throwWithStackTrace( - MetadataParserException(track: File(''), message: e.message), s); - } catch (e, s) { - Error.throwWithStackTrace( - MetadataParserException(track: File(''), message: e.toString()), s); - } - - throw NoMetadataParserException( - track: File(''), - ); -} \ No newline at end of file diff --git a/lib/src/parsers/mp4.dart b/lib/src/parsers/mp4.dart index 3edb200..fec2cbd 100644 --- a/lib/src/parsers/mp4.dart +++ b/lib/src/parsers/mp4.dart @@ -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)); diff --git a/lib/src/utils/buffer.dart b/lib/src/utils/buffer.dart index 918a536..57b0f28 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -1,7 +1,7 @@ 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; @@ -55,6 +55,88 @@ class FileRandomAccessFile implements MyRandomAccessFile{ } } +class Uint8ListRandomAccessFile implements MyRandomAccessFile { + final Uint8List _data; + int _position = 0; + + Uint8ListRandomAccessFile({required Uint8List data}) : _data = data; + + @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 { + 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 { + return _data.length; + } + + @override + Future close() async { + // 内存数据无需关闭操作 + } + + @override + Future read(int size) async { + 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; @@ -159,7 +241,7 @@ 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"); } } 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 a130841..f166ee8 100644 --- a/lib/src/writer.dart +++ b/lib/src/writer.dart @@ -9,11 +9,11 @@ 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. Future updateMetadata(File track, void Function(ParserTag metadata) updater) async{ - final metadata = await readAllMetadata(track); + // final metadata = await readAllMetadata(track); - updater(metadata); + // updater(metadata); - writeMetadata(track, metadata); + // writeMetadata(track, metadata); } /// Write the [metadata] into the [track] diff --git a/pubspec.yaml b/pubspec.yaml index 77b8234..d1a457c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ 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 From f54eb48a62763c2adf9f1ab19b4e079144c75a74 Mon Sep 17 00:00:00 2001 From: lyp Date: Fri, 6 Feb 2026 01:47:58 +0800 Subject: [PATCH 15/16] fix --- lib/audio_metadata_reader.dart | 3 +-- lib/src/parser.dart | 12 ++++++------ 2 files changed, 7 insertions(+), 8 deletions(-) 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/parser.dart b/lib/src/parser.dart index 457c579..bc4e301 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -215,17 +215,17 @@ Future readMetadata(MyRandomAccessFile reader, Future readAllMetadata(MyRandomAccessFile reader, {bool getImage = true}) async { try { if (await ID3v2Parser.canUserParser(reader)) { - return ID3v2Parser(fetchImage: getImage).parse(reader); + return await ID3v2Parser(fetchImage: getImage).parse(reader); } else if (await FlacParser.canUserParser(reader)) { - return FlacParser(fetchImage: getImage).parse(reader); + return await FlacParser(fetchImage: getImage).parse(reader); } else if (await MP4Parser.canUserParser(reader)) { - return MP4Parser(fetchImage: getImage).parse(reader); + return await MP4Parser(fetchImage: getImage).parse(reader); } else if (await OGGParser.canUserParser(reader)) { - return OGGParser(fetchImage: getImage).parse(reader); + return await OGGParser(fetchImage: getImage).parse(reader); } else if (await ID3v2Parser.isID3v1(reader)) { - return ID3v1Parser().parse(reader); + return await ID3v1Parser().parse(reader); } else if (await RiffParser.canUserParser(reader)) { - return RiffParser().parse(reader); + return await RiffParser().parse(reader); } } catch (e, trace) { print(trace); From 6950e0ca4ca5c55d6565a191f0c88056ffa4997b Mon Sep 17 00:00:00 2001 From: lyp Date: Fri, 6 Feb 2026 02:47:32 +0800 Subject: [PATCH 16/16] fix --- example/main.dart | 4 +-- lib/src/parser.dart | 4 +-- lib/src/utils/buffer.dart | 64 ++++++++++++++++++++++++++------------- 3 files changed, 47 insertions(+), 25 deletions(-) diff --git a/example/main.dart b/example/main.dart index 7498e21..11da880 100644 --- a/example/main.dart +++ b/example/main.dart @@ -13,8 +13,8 @@ final client = webdav.newClient( void main() async { File file = File('/Users/lyp/Music/LZF-Music/After Hours - The Weeknd.flac'); - Uint8List data = file.readAsBytesSync(); + Uint8List data = await file.readAsBytes(); print(data.length); - final metadata = await readMetadataUint8List(data, getImage: true); + final metadata = await readMetadataUint8List(Stream.value(data), getImage: true); print(metadata); } diff --git a/lib/src/parser.dart b/lib/src/parser.dart index bc4e301..8ffaed2 100644 --- a/lib/src/parser.dart +++ b/lib/src/parser.dart @@ -20,8 +20,8 @@ Future readMetadataWebDav(webdav.Client client, String path, return readMetadata(reader, getImage: getImage); } -Future readMetadataUint8List(Uint8List data,{bool getImage = false}) async { - final reader = Uint8ListRandomAccessFile(data: data); +Future readMetadataUint8List(Stream data,{bool getImage = false}) async { + final reader = StreamRandomAccessFile(stream: data); return readMetadata(reader, getImage: getImage); } diff --git a/lib/src/utils/buffer.dart b/lib/src/utils/buffer.dart index bcb09a4..d0544cf 100644 --- a/lib/src/utils/buffer.dart +++ b/lib/src/utils/buffer.dart @@ -54,12 +54,35 @@ class FileRandomAccessFile implements MyRandomAccessFile{ return await randomAccessFile.read(size); } } - -class Uint8ListRandomAccessFile implements MyRandomAccessFile { - final Uint8List _data; +class StreamRandomAccessFile implements MyRandomAccessFile { + final Stream _stream; + Uint8List? _data; int _position = 0; + bool _isLoaded = false; + + StreamRandomAccessFile({required Stream stream}) : _stream = stream; - Uint8ListRandomAccessFile({required Uint8List data}) : _data = data; + // 懒加载:第一次访问时才从 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 { @@ -76,26 +99,23 @@ class Uint8ListRandomAccessFile implements MyRandomAccessFile { @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) { + if (_position >= _data!.length) { return 0; // EOF } - final availableBytes = _data.length - _position; + final availableBytes = _data!.length - _position; final requestedBytes = end - start; final bytesToRead = math.min(availableBytes, requestedBytes); - buffer.setRange( - start, - start + bytesToRead, - _data, - _position - ); + buffer.setRange(start, start + bytesToRead, _data!, _position); _position += bytesToRead; return bytesToRead; @@ -103,32 +123,33 @@ class Uint8ListRandomAccessFile implements MyRandomAccessFile { @override Future length() async { - return _data.length; + 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) { + if (_position >= _data!.length) { return Uint8List(0); // EOF } - final availableBytes = _data.length - _position; + final availableBytes = _data!.length - _position; final bytesToRead = math.min(availableBytes, size); - final result = Uint8List.sublistView( - _data, - _position, - _position + bytesToRead - ); + final result = Uint8List.sublistView(_data!, _position, _position + bytesToRead); _position += bytesToRead; return result; @@ -137,6 +158,7 @@ class Uint8ListRandomAccessFile implements MyRandomAccessFile { + class WebDavRandomAccessFile implements MyRandomAccessFile{ final webdav.Client client; final String path;