Skip to content

Commit ecfb2cf

Browse files
committed
Added support for table of contents (CTOC) - fixes #2.
Added support for picture and url fields in chapter frames. Updated unit tests.
1 parent 578d1fa commit ecfb2cf

File tree

11 files changed

+198
-62
lines changed

11 files changed

+198
-62
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
##Version 0.2.0
44

55
### Features
6+
* Added support for table of contents (CTOC). Resolved with input from [pull request 2](https://github.com/tolo/id3tag/issues/2), added by @szeidner.
7+
* Added support for picture and url fields in chapter frames (CHAP). Resolved with input from [pull request 2](https://github.com/tolo/id3tag/issues/2), added by @szeidner.
68
* Enabled previously commented out support for unsync lyrics/transcription frames (USLT). Resolved with input from [pull request 3](https://github.com/tolo/id3tag/issues/3), added by @theckr96.
79

810
### Bug fixes

README.md

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ ID3Tag is a small library for reading common ID3 meta data from mp3-files and ot
44
There are of course already other libraries for doing this out there, but at the time of creating this library, none of
55
them provided support in three critical areas:
66

7-
- Reading **chapter** (`CHAP`) frames
7+
- Reading **chapter** (`CHAP`) and **table of contents** (`CTOC`) frames
88
- Reading ID3 meta data without having to load the entire file into memory
99
- Easy extensibility to be able to implement support for additional frame types
1010

@@ -17,7 +17,8 @@ audio books was another motivating factor (hence support for chapters and large
1717
- Support for ID3 v2.3 (and above)
1818
- Support for common ID3 frames, such as:
1919
- Text information frames [see here](https://id3.org/id3v2.3.0#Text_information_frames)
20-
- Chapter frames, i.e. [`CHAP` frames](https://id3.org/id3v2-chapters-1.0)
20+
- Chapter frames, i.e. [`CHAP` frames](https://id3.org/id3v2-chapters-1.0#Chapter_frame)
21+
- Table of contents frames, i.e. [`CTOC` frames](https://id3.org/id3v2-chapters-1.0#Table_of_contents_frame)
2122
- Attached picture frames, i.e. [`APIC` frames](https://id3.org/id3v2.3.0#Attached_picture)
2223
- Comment frames, i.e. [`COMM` frames](https://id3.org/id3v2.3.0#Comments)
2324

@@ -31,7 +32,9 @@ the ID3 tag from a file.
3132
final parser = ID3TagReader.path('path to a file');
3233
final tag = parser.readTagSync();
3334
print('Title: ${tag.title}');
34-
print('Chapters: ${tag.chapters}');
35+
print('All chapters: ${tag.chapters}');
36+
// Or:
37+
print('Chapters in top level table of contents: ${tag.topLevelChapters}');
3538
```
3639

3740
During development, it may sometimes be convenient to use files in the form of asset resources. To accomplish this, you
@@ -46,11 +49,6 @@ File(filePath).writeAsBytesSync(fileData.buffer.asUint8List(fileData.offsetInByt
4649
```
4750

4851

49-
## On the horizon
50-
51-
The plan is to add support for reading the table of contents (`CTOC`) frame type.
52-
53-
5452
## Additional information
5553

5654
This library was initially derived from the package [id3](https://github.com/sanket143/id3), but later refactored,

lib/src/extensions/iterable_extension.dart

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,10 @@ extension IterableExtensions<E> on Iterable<E> {
1313
}
1414
}
1515
}
16+
17+
extension ListExtensions<E> on List<E> {
18+
bool addNotNull(E? element) {
19+
if (element != null) add(element);
20+
return element != null;
21+
}
22+
}

lib/src/frames/chapter_frame.dart

Lines changed: 29 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
import '../raw_frame.dart';
12
import 'frame.dart';
2-
import 'text_information_frame.dart';
33
import 'frame_parser.dart';
4-
import '../raw_frame.dart';
4+
import 'text_information_frame.dart';
5+
import 'user_url_frame.dart';
6+
import 'picture_frame.dart';
57

68

79
const String _frameName = 'CHAP';
@@ -15,19 +17,23 @@ class Chapter extends Frame {
1517
final String? description;
1618
final int startTimeMilliseconds;
1719
final int endTimeMilliseconds;
20+
final Picture? picture;
21+
final UserUrl? url;
1822

1923
Chapter({required this.elementId, required this.title, this.description,
20-
required this.startTimeMilliseconds, required this.endTimeMilliseconds});
24+
required this.startTimeMilliseconds, required this.endTimeMilliseconds, this.picture, this.url});
2125

2226
@override
2327
Map<String, dynamic> toDictionary() {
2428
return {
25-
'frameName' : frameName,
26-
'elementId' : elementId,
27-
'title' : title,
28-
'description' : description ?? '',
29-
'startTimeMilliseconds' : startTimeMilliseconds,
30-
'endTimeMilliseconds' : endTimeMilliseconds,
29+
'frameName': frameName,
30+
'elementId': elementId,
31+
'title': title,
32+
'description': description ?? '',
33+
'startTimeMilliseconds': startTimeMilliseconds,
34+
'endTimeMilliseconds': endTimeMilliseconds,
35+
'picture': picture,
36+
'url': url
3137
};
3238
}
3339
}
@@ -49,21 +55,24 @@ class ChapterFrameParser extends FrameParser<Chapter> {
4955

5056
String? chapterName;
5157
String? chapterDescription;
58+
Picture? picture;
59+
UserUrl? url;
5260
if (frameContent.remainingBytes > 0) {
53-
final subFrame1 = rawFrame.parseSubFrame();
54-
final subFrame2 = rawFrame.parseSubFrame();
55-
56-
if (subFrame1 != null && subFrame1 is TextInformation) { // TIT2 or TIT3
57-
chapterName = subFrame1.frameName == 'TIT2' ? subFrame1.value : null;
58-
chapterDescription = subFrame1.frameName == 'TIT3' ? subFrame1.value : null;
59-
}
60-
if (subFrame2 != null && subFrame2 is TextInformation) { // TIT2 or TIT3
61-
chapterName = subFrame2.frameName == 'TIT2' ? subFrame2.value : chapterName;
62-
chapterDescription = subFrame2.frameName == 'TIT3' ? subFrame2.value : chapterDescription;
61+
final subFrames = rawFrame.parseSubFrames();
62+
for(var frame in subFrames) {
63+
if (frame is TextInformation) { // TIT2 or TIT3
64+
chapterName = frame.frameName == 'TIT2' ? frame.value : chapterName;
65+
chapterDescription = frame.frameName == 'TIT3' ? frame.value : chapterDescription;
66+
} else if (frame is Picture) {
67+
picture = frame;
68+
} else if (frame is UserUrl) {
69+
url = frame;
70+
}
6371
}
6472
}
6573

6674
return Chapter(elementId: elementId, title: chapterName ?? elementId, description: chapterDescription,
67-
startTimeMilliseconds: startTimeMilliseconds, endTimeMilliseconds: endTimeMilliseconds);
75+
startTimeMilliseconds: startTimeMilliseconds, endTimeMilliseconds: endTimeMilliseconds,
76+
picture: picture, url: url);
6877
}
6978
}

lib/src/frames/frames.dart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export 'comment_frame.dart';
33
export 'frame.dart';
44
export 'frame_parser.dart';
55
export 'picture_frame.dart';
6-
//export 'table_of_contents_frame.dart';
6+
export 'table_of_contents_frame.dart';
77
export 'text_information_frame.dart';
88
//export 'url_frame.dart';
99
export 'user_url_frame.dart';
Lines changed: 60 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,63 @@
1+
import 'package:id3tag/src/frames/frames.dart';
12

2-
//const String _frameName = 'CTOC';
3+
import '../raw_frame.dart';
34

4-
// TODO
5-
class TableOfContentsFrameParser {
6-
// @override
7-
//List<String> get frameNames => [_frameName];
5+
6+
const String _frameName = 'CTOC';
7+
8+
9+
class TableOfContents extends Frame {
10+
@override String get frameName => _frameName;
11+
12+
final String elementId;
13+
final bool isTopLevel;
14+
final bool isOrdered;
15+
final List<String> childElementIds;
16+
final String? description;
17+
18+
TableOfContents({required this.elementId, required this.isTopLevel, required this.isOrdered,
19+
required this.childElementIds, this.description});
20+
21+
@override
22+
Map<String, dynamic> toDictionary() {
23+
return {
24+
'frameName': frameName,
25+
'elementId': elementId,
26+
'topLevel': isTopLevel,
27+
'ordered': isOrdered,
28+
'description': description,
29+
'childElementIds': childElementIds,
30+
};
31+
}
832
}
33+
34+
class TableOfContentsFrameParser extends FrameParser<TableOfContents> {
35+
@override
36+
List<String> get frameNames => [_frameName];
37+
38+
@override
39+
TableOfContents? parseFrame(RawFrame rawFrame) {
40+
final frameContent = rawFrame.frameContent;
41+
final elementId = frameContent.readString(checkEncoding: false);
42+
final flags = frameContent.readBytes(1);
43+
final int entryCount = frameContent.readBytes(1).first;
44+
45+
List<String> childElementIds = [];
46+
for (var i=0; i<entryCount; i++) {
47+
childElementIds.add(frameContent.readString());
48+
}
49+
50+
String? description;
51+
if (frameContent.remainingBytes > 0) {
52+
final subFrame = rawFrame.parseSubFrame();
53+
if (subFrame is TextInformation) { // TIT2
54+
description = subFrame.value;
55+
}
56+
}
57+
58+
return TableOfContents(elementId: elementId,
59+
isTopLevel: (flags.first & 0x2) > 0, isOrdered: (flags.first & 0x1) > 0,
60+
childElementIds: childElementIds, description: description,
61+
);
62+
}
63+
}

lib/src/id3_parser.dart

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,19 @@ class ID3Parser implements ID3TagReader {
4747
_parsers = frameParsers, _id3TagBytes = id3TagBytes, _initialOffset = initialOffset;
4848

4949
factory ID3Parser(File file, {Map<String, FrameParser>? frameParsers}) {
50-
final RandomAccessFile _reader = file.openSync(mode: FileMode.read);
50+
final RandomAccessFile reader = file.openSync(mode: FileMode.read);
5151

5252
try {
53-
final length = _reader.lengthSync();
54-
final List<int> fileHeaderBytes = length > id3FileHeaderLength ? _reader.readSync(id3FileHeaderLength) : [];
53+
final length = reader.lengthSync();
54+
final List<int> fileHeaderBytes = length > id3FileHeaderLength ? reader.readSync(id3FileHeaderLength) : [];
5555
// Assume ID3 tag is at beginning of file
5656
final List<int>? id3FileTag = fileHeaderBytes.length == id3FileHeaderLength ? fileHeaderBytes.sublist(0, 3) : null;
5757

5858
if (id3FileTag != null && latin1.decode(id3FileTag) == 'ID3') {
5959
final headerBytes = fileHeaderBytes.toList();
6060
final header = ID3FileHeader.fromHeaderBytes(headerBytes);
6161

62-
final id3TagBytes = _reader.readSync(header.id3TagSize);
62+
final id3TagBytes = reader.readSync(header.id3TagSize);
6363

6464
int initialOffset = 0;
6565
if (header.hasExtendedHeader) {
@@ -74,7 +74,7 @@ class ID3Parser implements ID3TagReader {
7474
id3TagBytes: [], initialOffset: 0);
7575
}
7676
} finally {
77-
_reader.closeSync();
77+
reader.closeSync();
7878
}
7979
}
8080

@@ -139,9 +139,9 @@ class ID3Parser implements ID3TagReader {
139139
final addParser = (FrameParser parser) => parserMap.addEntries(parser.frameNames.map((e) => MapEntry(e, parser)));
140140

141141
addParser(ChapterFrameParser());
142+
addParser(TableOfContentsFrameParser());
142143
addParser(CommentFrameParser());
143144
addParser(PictureFrameParser());
144-
//addParser(TableOfContentsFrameParser()); // TODO
145145
addParser(TextInformationFrameParser());
146146
//addParser(UrlFrameParser()); // TODO
147147
addParser(UserUrlFrameParser());

lib/src/id3_tag.dart

Lines changed: 36 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
55
import 'extensions/iterable_extension.dart';
66
import 'frames/frames.dart';
7-
import 'frames/chapter_frame.dart';
87

98

109
class ID3Tag {
@@ -36,12 +35,29 @@ class ID3Tag {
3635
Comment? get comment => frameWithTypeAndName<Comment>('COMM');
3736
List<Comment> get comments => framesWithTypeAndName<Comment>('COMM');
3837

39-
/// Gets the chapter (`CHAP`) frames, represented as [Chapter] objects. The returned chapters are sorted according to
40-
/// start time.
38+
/// Gets all the chapter (`CHAP`) frames defined in the tag, represented as [Chapter] objects. The returned chapters
39+
/// are sorted according to start time. **NOTE: ** Consider getting chapters via table of contents or by using
40+
/// [topLevelChapters].
4141
List<Chapter> get chapters {
42-
final _chapters = framesWithType<Chapter>();
43-
_chapters.sort((a, b) => a.startTimeMilliseconds.compareTo(b.startTimeMilliseconds));
44-
return _chapters;
42+
final chapters = framesWithType<Chapter>();
43+
chapters.sort((a, b) => a.startTimeMilliseconds.compareTo(b.startTimeMilliseconds));
44+
return chapters;
45+
}
46+
47+
/// Gets the table of contents (`CTOC`) frames, represented as [TableOfContents] objects.
48+
List<TableOfContents> get tableOfContents => framesWithType<TableOfContents>();
49+
/// Gets the the top level table of contents (`CTOC`) frame or the first one if no top level is found.
50+
TableOfContents? get topLevelTOC {
51+
final toc = tableOfContents;
52+
return toc.firstWhereOrNull((toc) => toc.isTopLevel) ?? toc.firstIfAny();
53+
}
54+
55+
/// Gets all the chapters referenced by the top level table of contents (or the first one). Falls back to [chapters]
56+
/// if no table of contents frame is present.
57+
List<Chapter> get topLevelChapters {
58+
final toc = topLevelTOC;
59+
if (toc != null && toc.childElementIds.isNotEmpty) { return chaptersForTOC(toc); }
60+
else { return chapters; }
4561
}
4662

4763
/// Gets the unsynchronized lyric/text transcription ('USLT') frame, represented as a [Lyrics] object.
@@ -82,4 +98,18 @@ class ID3Tag {
8298
T? frameWithTypeAndName<T extends Frame>(String name) {
8399
return framesWithTypeAndName<T>(name).firstIfAny();
84100
}
101+
102+
/// Gets the chapters matching the element ids in the specified [TableOfContents] object.
103+
List<Chapter> chaptersForTOC(TableOfContents toc) {
104+
final chapters = framesWithType<Chapter>();
105+
List<Chapter> tocChapters = [];
106+
for (var elementId in toc.childElementIds) {
107+
final index = chapters.indexWhere((c) => c.elementId == elementId);
108+
if (index > -1) {
109+
tocChapters.add(chapters[index]);
110+
chapters.removeAt(index);
111+
}
112+
}
113+
return tocChapters;
114+
}
85115
}

lib/src/raw_frame.dart

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import 'frames/frame.dart';
66
import 'frame_body.dart';
77
import 'id3_parser.dart';
8+
import 'extensions/iterable_extension.dart';
89

910

1011
class RawFrame {
@@ -28,5 +29,16 @@ class RawFrame {
2829
frameContent.pos += rawSubFrame.frameSize;
2930
return id3Parser.parseSubFrame(rawSubFrame);
3031
}
32+
return null;
33+
}
34+
35+
List<Frame> parseSubFrames() {
36+
List<Frame> subFrames = [];
37+
var pos = 0;
38+
do {
39+
pos = frameContent.pos;
40+
subFrames.addNotNull(parseSubFrame());
41+
} while (frameContent.remainingBytes > 0 && frameContent.pos != pos);
42+
return subFrames;
3143
}
3244
}

test/ctoc.mp3

159 KB
Binary file not shown.

0 commit comments

Comments
 (0)