tinytag 2.1.2__tar.gz → 2.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of tinytag might be problematic. Click here for more details.
- {tinytag-2.1.2 → tinytag-2.2.0}/PKG-INFO +23 -2
- {tinytag-2.1.2 → tinytag-2.2.0}/README.md +20 -1
- {tinytag-2.1.2 → tinytag-2.2.0}/pyproject.toml +2 -0
- {tinytag-2.1.2 → tinytag-2.2.0}/tinytag/__init__.py +1 -1
- {tinytag-2.1.2 → tinytag-2.2.0}/tinytag/tinytag.py +245 -120
- {tinytag-2.1.2 → tinytag-2.2.0}/LICENSE +0 -0
- {tinytag-2.1.2 → tinytag-2.2.0}/tinytag/__main__.py +0 -0
- {tinytag-2.1.2 → tinytag-2.2.0}/tinytag/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinytag
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Read audio file metadata
|
|
5
5
|
Keywords: metadata,audio,music,mp3,m4a,wav,ogg,opus,flac,wma,aiff
|
|
6
6
|
Author: Tom Wallroth, Mat (mathiascode)
|
|
@@ -15,6 +15,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.11
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.12
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
19
|
Classifier: License :: OSI Approved :: MIT License
|
|
19
20
|
Classifier: Development Status :: 5 - Production/Stable
|
|
20
21
|
Classifier: Environment :: Web Environment
|
|
@@ -28,6 +29,7 @@ Classifier: Typing :: Typed
|
|
|
28
29
|
License-File: LICENSE
|
|
29
30
|
Requires-Dist: coverage ; extra == "tests"
|
|
30
31
|
Requires-Dist: mypy ; extra == "tests"
|
|
32
|
+
Requires-Dist: mypy<1.19.0 ; extra == "tests" and ( platform_python_implementation == 'PyPy')
|
|
31
33
|
Requires-Dist: pycodestyle ; extra == "tests"
|
|
32
34
|
Requires-Dist: pylint ; extra == "tests"
|
|
33
35
|
Requires-Dist: pyright ; extra == "tests"
|
|
@@ -182,7 +184,7 @@ These are helpful when you need quick access to common metadata.
|
|
|
182
184
|
|
|
183
185
|
### Additional Metadata
|
|
184
186
|
|
|
185
|
-
For additional values of the same field type,
|
|
187
|
+
For additional values of the same field type, uncommon metadata fields, or
|
|
186
188
|
metadata specific to certain file formats, use `other`:
|
|
187
189
|
|
|
188
190
|
tag.other # a dictionary of additional fields
|
|
@@ -202,6 +204,7 @@ present when files provide such metadata:
|
|
|
202
204
|
director
|
|
203
205
|
encoded_by
|
|
204
206
|
encoder_settings
|
|
207
|
+
grouping
|
|
205
208
|
initial_key
|
|
206
209
|
isrc
|
|
207
210
|
language
|
|
@@ -209,9 +212,14 @@ present when files provide such metadata:
|
|
|
209
212
|
lyricist
|
|
210
213
|
lyrics
|
|
211
214
|
media
|
|
215
|
+
movement
|
|
216
|
+
movement_name
|
|
217
|
+
movement_total
|
|
212
218
|
publisher
|
|
213
219
|
set_subtitle
|
|
220
|
+
show_movement
|
|
214
221
|
url
|
|
222
|
+
work
|
|
215
223
|
|
|
216
224
|
Additional `other` field names not documented above may be present, but are
|
|
217
225
|
format-specific and may change or disappear in future tinytag releases. If
|
|
@@ -421,6 +429,19 @@ TinyTag.get(file_obj=your_file_obj)
|
|
|
421
429
|
|
|
422
430
|
## Changelog
|
|
423
431
|
|
|
432
|
+
### 2.2.0 (2025-12-15)
|
|
433
|
+
|
|
434
|
+
- Add support for movement, work and grouping fields
|
|
435
|
+
- ID3: Make synced lyrics available in 'other.lyrics' (LRC format)
|
|
436
|
+
- ID3: Continue reading after encountering empty frame
|
|
437
|
+
- ID3: Fix frame reading when image parsing is disabled
|
|
438
|
+
- ID3: Exclude more frames containing binary data
|
|
439
|
+
- ID3: Avoid unnecessary string decoding
|
|
440
|
+
- M4A: Support extended atom sizes
|
|
441
|
+
- M4A: Ensure all field names are lowercase
|
|
442
|
+
- OGG: Stop reading after reaching EOS page
|
|
443
|
+
- Vorbis: Map UNSYNCEDLYRICS field to other.lyrics
|
|
444
|
+
|
|
424
445
|
### 2.1.2 (2025-08-14)
|
|
425
446
|
|
|
426
447
|
- M4A: Add a few missing additional metadata fields
|
|
@@ -146,7 +146,7 @@ These are helpful when you need quick access to common metadata.
|
|
|
146
146
|
|
|
147
147
|
### Additional Metadata
|
|
148
148
|
|
|
149
|
-
For additional values of the same field type,
|
|
149
|
+
For additional values of the same field type, uncommon metadata fields, or
|
|
150
150
|
metadata specific to certain file formats, use `other`:
|
|
151
151
|
|
|
152
152
|
tag.other # a dictionary of additional fields
|
|
@@ -166,6 +166,7 @@ present when files provide such metadata:
|
|
|
166
166
|
director
|
|
167
167
|
encoded_by
|
|
168
168
|
encoder_settings
|
|
169
|
+
grouping
|
|
169
170
|
initial_key
|
|
170
171
|
isrc
|
|
171
172
|
language
|
|
@@ -173,9 +174,14 @@ present when files provide such metadata:
|
|
|
173
174
|
lyricist
|
|
174
175
|
lyrics
|
|
175
176
|
media
|
|
177
|
+
movement
|
|
178
|
+
movement_name
|
|
179
|
+
movement_total
|
|
176
180
|
publisher
|
|
177
181
|
set_subtitle
|
|
182
|
+
show_movement
|
|
178
183
|
url
|
|
184
|
+
work
|
|
179
185
|
|
|
180
186
|
Additional `other` field names not documented above may be present, but are
|
|
181
187
|
format-specific and may change or disappear in future tinytag releases. If
|
|
@@ -385,6 +391,19 @@ TinyTag.get(file_obj=your_file_obj)
|
|
|
385
391
|
|
|
386
392
|
## Changelog
|
|
387
393
|
|
|
394
|
+
### 2.2.0 (2025-12-15)
|
|
395
|
+
|
|
396
|
+
- Add support for movement, work and grouping fields
|
|
397
|
+
- ID3: Make synced lyrics available in 'other.lyrics' (LRC format)
|
|
398
|
+
- ID3: Continue reading after encountering empty frame
|
|
399
|
+
- ID3: Fix frame reading when image parsing is disabled
|
|
400
|
+
- ID3: Exclude more frames containing binary data
|
|
401
|
+
- ID3: Avoid unnecessary string decoding
|
|
402
|
+
- M4A: Support extended atom sizes
|
|
403
|
+
- M4A: Ensure all field names are lowercase
|
|
404
|
+
- OGG: Stop reading after reaching EOS page
|
|
405
|
+
- Vorbis: Map UNSYNCEDLYRICS field to other.lyrics
|
|
406
|
+
|
|
388
407
|
### 2.1.2 (2025-08-14)
|
|
389
408
|
|
|
390
409
|
- M4A: Add a few missing additional metadata fields
|
|
@@ -35,6 +35,7 @@ classifiers = [
|
|
|
35
35
|
"Programming Language :: Python :: 3.11",
|
|
36
36
|
"Programming Language :: Python :: 3.12",
|
|
37
37
|
"Programming Language :: Python :: 3.13",
|
|
38
|
+
"Programming Language :: Python :: 3.14",
|
|
38
39
|
"License :: OSI Approved :: MIT License",
|
|
39
40
|
"Development Status :: 5 - Production/Stable",
|
|
40
41
|
"Environment :: Web Environment",
|
|
@@ -58,6 +59,7 @@ Homepage = "https://github.com/tinytag/tinytag"
|
|
|
58
59
|
tests = [
|
|
59
60
|
"coverage",
|
|
60
61
|
"mypy",
|
|
62
|
+
"mypy<1.19.0; platform_python_implementation == 'PyPy'",
|
|
61
63
|
"pycodestyle",
|
|
62
64
|
"pylint",
|
|
63
65
|
"pyright"
|
|
@@ -524,17 +524,26 @@ class _MP4(TinyTag):
|
|
|
524
524
|
b'\xa9des': {b'data': _MP4._data_parser('other.description')},
|
|
525
525
|
b'\xa9dir': {b'data': _MP4._data_parser('other.director')},
|
|
526
526
|
b'\xa9gen': {b'data': _MP4._data_parser('genre')},
|
|
527
|
+
b'\xa9grp': {b'data': _MP4._data_parser('other.grouping')},
|
|
527
528
|
b'\xa9lyr': {b'data': _MP4._data_parser('other.lyrics')},
|
|
528
|
-
b'\
|
|
529
|
+
b'\xa9mvc': {
|
|
530
|
+
b'data': _MP4._data_parser('other.movement_total')
|
|
531
|
+
},
|
|
532
|
+
b'\xa9mvi': {b'data': _MP4._data_parser('other.movement')},
|
|
533
|
+
b'\xa9mvn': {
|
|
534
|
+
b'data': _MP4._data_parser('other.movement_name')
|
|
535
|
+
},
|
|
529
536
|
b'\xa9nam': {b'data': _MP4._data_parser('title')},
|
|
530
537
|
b'\xa9pub': {b'data': _MP4._data_parser('other.publisher')},
|
|
531
538
|
b'\xa9too': {b'data': _MP4._data_parser('other.encoded_by')},
|
|
539
|
+
b'\xa9wrk': {b'data': _MP4._data_parser('other.work')},
|
|
532
540
|
b'\xa9wrt': {b'data': _MP4._data_parser('composer')},
|
|
533
541
|
b'aART': {b'data': _MP4._data_parser('albumartist')},
|
|
534
542
|
b'cprt': {b'data': _MP4._data_parser('other.copyright')},
|
|
535
543
|
b'desc': {b'data': _MP4._data_parser('other.description')},
|
|
536
544
|
b'disk': {b'data': _MP4._nums_parser('disc', 'disc_total')},
|
|
537
545
|
b'gnre': {b'data': _MP4._parse_id3v1_genre},
|
|
546
|
+
b'shwm': {b'data': _MP4._data_parser('other.show_movement')},
|
|
538
547
|
b'trkn': {b'data': _MP4._nums_parser('track', 'track_total')},
|
|
539
548
|
b'tmpo': {b'data': _MP4._data_parser('other.bpm')},
|
|
540
549
|
b'covr': {b'data': _MP4._parse_cover_image},
|
|
@@ -547,13 +556,18 @@ class _MP4(TinyTag):
|
|
|
547
556
|
path: _DataTreeDict,
|
|
548
557
|
stop_pos: int | None = None,
|
|
549
558
|
curr_path: list[bytes] | None = None) -> None:
|
|
550
|
-
header_len = 8
|
|
559
|
+
header_len = ext_size_len = 8
|
|
551
560
|
atom_header = fh.read(header_len)
|
|
552
561
|
while len(atom_header) == header_len:
|
|
553
|
-
atom_size = unpack('>I', atom_header[:4])[0]
|
|
562
|
+
atom_size = unpack('>I', atom_header[:4])[0]
|
|
554
563
|
atom_type = atom_header[4:]
|
|
555
564
|
if curr_path is None: # keep track how we traversed in the tree
|
|
556
565
|
curr_path = [atom_type]
|
|
566
|
+
if atom_size == 1: # 64-bit size
|
|
567
|
+
ext_size_header = fh.read(ext_size_len)
|
|
568
|
+
if len(ext_size_header) == ext_size_len:
|
|
569
|
+
atom_size = unpack('>Q', ext_size_header)[0] - ext_size_len
|
|
570
|
+
atom_size -= header_len
|
|
557
571
|
if atom_size <= 0: # empty atom, jump to next one
|
|
558
572
|
atom_header = fh.read(header_len)
|
|
559
573
|
continue
|
|
@@ -563,8 +577,10 @@ class _MP4(TinyTag):
|
|
|
563
577
|
f'atom: {atom_type!r} len: {atom_size + header_len}')
|
|
564
578
|
if atom_type in self._VERSIONED_ATOMS: # jump atom version for now
|
|
565
579
|
fh.seek(4, SEEK_CUR)
|
|
580
|
+
atom_size -= 4
|
|
566
581
|
if atom_type in self._FLAGGED_ATOMS: # jump atom flags for now
|
|
567
582
|
fh.seek(4, SEEK_CUR)
|
|
583
|
+
atom_size -= 4
|
|
568
584
|
sub_path = path.get(atom_type, None)
|
|
569
585
|
# if the path leaf is a dict, traverse deeper into the tree:
|
|
570
586
|
if isinstance(sub_path, dict):
|
|
@@ -589,7 +605,9 @@ class _MP4(TinyTag):
|
|
|
589
605
|
# unknown data atom, try to parse it
|
|
590
606
|
elif curr_path == self._ILST_PATH:
|
|
591
607
|
atom_end_pos = fh.tell() + atom_size
|
|
592
|
-
field_name =
|
|
608
|
+
field_name = (
|
|
609
|
+
self._OTHER_PREFIX + atom_type.decode('latin-1').lower()
|
|
610
|
+
)
|
|
593
611
|
fh.seek(-header_len, SEEK_CUR)
|
|
594
612
|
self._traverse_atoms(
|
|
595
613
|
fh,
|
|
@@ -739,31 +757,35 @@ class _ID3(TinyTag):
|
|
|
739
757
|
_ID3_MAPPING = {
|
|
740
758
|
# Mapping from Frame ID to a field of the TinyTag
|
|
741
759
|
# https://exiftool.org/TagNames/ID3.html
|
|
742
|
-
'COMM': 'comment', 'COM': 'comment',
|
|
743
|
-
'TRCK': 'track', 'TRK': 'track',
|
|
744
|
-
'TYER': 'year', 'TYE': 'year', 'TDRC': 'year',
|
|
745
|
-
'TALB': 'album', 'TAL': 'album',
|
|
746
|
-
'TPE1': 'artist', 'TP1': 'artist',
|
|
747
|
-
'TIT2': 'title', 'TT2': 'title',
|
|
748
|
-
'TCON': 'genre', 'TCO': 'genre',
|
|
749
|
-
'TPOS': 'disc', 'TPA': 'disc',
|
|
750
|
-
'TPE2': 'albumartist', 'TP2': 'albumartist',
|
|
751
|
-
'TCOM': 'composer', 'TCM': 'composer',
|
|
752
|
-
'WOAR': 'other.url', 'WAR': 'other.url',
|
|
753
|
-
'TSRC': 'other.isrc', 'TRC': 'other.isrc',
|
|
754
|
-
'TCOP': 'other.copyright', 'TCR': 'other.copyright',
|
|
755
|
-
'TBPM': 'other.bpm', 'TBP': 'other.bpm',
|
|
756
|
-
'TKEY': 'other.initial_key', 'TKE': 'other.initial_key',
|
|
757
|
-
'TLAN': 'other.language', 'TLA': 'other.language',
|
|
758
|
-
'TPUB': 'other.publisher', 'TPB': 'other.publisher',
|
|
759
|
-
'USLT': 'other.lyrics', 'ULT': 'other.lyrics',
|
|
760
|
-
'TPE3': 'other.conductor', 'TP3': 'other.conductor',
|
|
761
|
-
'TEXT': 'other.lyricist', 'TXT': 'other.lyricist',
|
|
762
|
-
'TSST': 'other.set_subtitle',
|
|
763
|
-
'TENC': 'other.encoded_by', 'TEN': 'other.encoded_by',
|
|
764
|
-
'TSSE': 'other.encoder_settings', 'TSS': 'other.encoder_settings',
|
|
765
|
-
'TMED': 'other.media', 'TMT': 'other.media',
|
|
766
|
-
'WCOP': 'other.license',
|
|
760
|
+
b'COMM': 'comment', b'COM': 'comment',
|
|
761
|
+
b'TRCK': 'track', b'TRK': 'track',
|
|
762
|
+
b'TYER': 'year', b'TYE': 'year', b'TDRC': 'year',
|
|
763
|
+
b'TALB': 'album', b'TAL': 'album',
|
|
764
|
+
b'TPE1': 'artist', b'TP1': 'artist',
|
|
765
|
+
b'TIT2': 'title', b'TT2': 'title',
|
|
766
|
+
b'TCON': 'genre', b'TCO': 'genre',
|
|
767
|
+
b'TPOS': 'disc', b'TPA': 'disc',
|
|
768
|
+
b'TPE2': 'albumartist', b'TP2': 'albumartist',
|
|
769
|
+
b'TCOM': 'composer', b'TCM': 'composer',
|
|
770
|
+
b'WOAR': 'other.url', b'WAR': 'other.url',
|
|
771
|
+
b'TSRC': 'other.isrc', b'TRC': 'other.isrc',
|
|
772
|
+
b'TCOP': 'other.copyright', b'TCR': 'other.copyright',
|
|
773
|
+
b'TBPM': 'other.bpm', b'TBP': 'other.bpm',
|
|
774
|
+
b'TKEY': 'other.initial_key', b'TKE': 'other.initial_key',
|
|
775
|
+
b'TLAN': 'other.language', b'TLA': 'other.language',
|
|
776
|
+
b'TPUB': 'other.publisher', b'TPB': 'other.publisher',
|
|
777
|
+
b'USLT': 'other.lyrics', b'ULT': 'other.lyrics',
|
|
778
|
+
b'TPE3': 'other.conductor', b'TP3': 'other.conductor',
|
|
779
|
+
b'TEXT': 'other.lyricist', b'TXT': 'other.lyricist',
|
|
780
|
+
b'TSST': 'other.set_subtitle',
|
|
781
|
+
b'TENC': 'other.encoded_by', b'TEN': 'other.encoded_by',
|
|
782
|
+
b'TSSE': 'other.encoder_settings', b'TSS': 'other.encoder_settings',
|
|
783
|
+
b'TMED': 'other.media', b'TMT': 'other.media',
|
|
784
|
+
b'WCOP': 'other.license',
|
|
785
|
+
b'MVNM': 'other.movement_name',
|
|
786
|
+
b'MVIN': 'other.movement',
|
|
787
|
+
b'GRP1': 'modern_grouping', b'GP1': 'modern_grouping',
|
|
788
|
+
b'TIT1': 'legacy_grouping', b'TT1': 'legacy_grouping',
|
|
767
789
|
}
|
|
768
790
|
_ID3_MAPPING_CUSTOM = {
|
|
769
791
|
'artists': 'artist',
|
|
@@ -771,23 +793,40 @@ class _ID3(TinyTag):
|
|
|
771
793
|
'license': 'other.license',
|
|
772
794
|
'barcode': 'other.barcode',
|
|
773
795
|
'catalognumber': 'other.catalog_number',
|
|
796
|
+
'showmovement': 'other.show_movement'
|
|
774
797
|
}
|
|
775
|
-
|
|
776
|
-
|
|
798
|
+
_EMPTY_FRAME_IDS = {b'\x00\x00\x00\x00', b'\x00\x00\x00'}
|
|
799
|
+
_IMAGE_FRAME_IDS = {b'APIC', b'PIC'}
|
|
800
|
+
_CUSTOM_FRAME_IDS = {b'TXXX', b'TXX'}
|
|
801
|
+
_SYNCED_LYRICS_FRAME_IDS = {b'SYLT', b'SLT'}
|
|
777
802
|
_IGNORED_FRAME_IDS = {
|
|
778
|
-
'AENC', 'CRA',
|
|
779
|
-
'
|
|
780
|
-
'
|
|
781
|
-
'
|
|
782
|
-
'
|
|
783
|
-
'
|
|
784
|
-
'
|
|
785
|
-
'
|
|
786
|
-
'
|
|
787
|
-
'
|
|
788
|
-
'
|
|
789
|
-
'
|
|
790
|
-
'
|
|
803
|
+
b'AENC', b'CRA',
|
|
804
|
+
b'APIC', b'PIC',
|
|
805
|
+
b'ASPI',
|
|
806
|
+
b'ATXT',
|
|
807
|
+
b'CHAP',
|
|
808
|
+
b'COMR',
|
|
809
|
+
b'CRM',
|
|
810
|
+
b'CTOC',
|
|
811
|
+
b'ENCR',
|
|
812
|
+
b'EQU2', b'EQU',
|
|
813
|
+
b'ETCO', b'ETC',
|
|
814
|
+
b'GEOB', b'GEO',
|
|
815
|
+
b'GRID',
|
|
816
|
+
b'LINK', b'LNK',
|
|
817
|
+
b'MCDI', b'MCI',
|
|
818
|
+
b'MLLT', b'MLL',
|
|
819
|
+
b'PCNT', b'CNT',
|
|
820
|
+
b'POPM', b'POP',
|
|
821
|
+
b'POSS',
|
|
822
|
+
b'PRIV',
|
|
823
|
+
b'RBUF', b'BUF',
|
|
824
|
+
b'RGAD',
|
|
825
|
+
b'RVA2', b'RVA',
|
|
826
|
+
b'RVRB', b'REV',
|
|
827
|
+
b'SEEK',
|
|
828
|
+
b'SIGN',
|
|
829
|
+
b'SYTC', b'STC',
|
|
791
830
|
}
|
|
792
831
|
_ID3V1_TAG_SIZE = 128
|
|
793
832
|
_MAX_ESTIMATION_SEC = 30.0
|
|
@@ -837,9 +876,9 @@ class _ID3(TinyTag):
|
|
|
837
876
|
'Psybient',
|
|
838
877
|
)
|
|
839
878
|
_ID3V2_2_IMAGE_FORMATS = {
|
|
840
|
-
'bmp': 'image/bmp',
|
|
841
|
-
'jpg': 'image/jpeg',
|
|
842
|
-
'png': 'image/png',
|
|
879
|
+
b'bmp': 'image/bmp',
|
|
880
|
+
b'jpg': 'image/jpeg',
|
|
881
|
+
b'png': 'image/png',
|
|
843
882
|
}
|
|
844
883
|
_IMAGE_TYPES = (
|
|
845
884
|
'other.generic',
|
|
@@ -904,6 +943,8 @@ class _ID3(TinyTag):
|
|
|
904
943
|
super().__init__()
|
|
905
944
|
# save position after the ID3 tag for duration measurement speedup
|
|
906
945
|
self._bytepos_after_id3v2 = -1
|
|
946
|
+
self._modern_grouping_values: list[str] = []
|
|
947
|
+
self._legacy_grouping_values: list[str] = []
|
|
907
948
|
|
|
908
949
|
@staticmethod
|
|
909
950
|
def _parse_xing_header(fh: BinaryIO) -> tuple[int, int]:
|
|
@@ -1048,10 +1089,11 @@ class _ID3(TinyTag):
|
|
|
1048
1089
|
fh.seek(extd_size - 6, SEEK_CUR) # jump over extended_header
|
|
1049
1090
|
while parsed_size < size:
|
|
1050
1091
|
frame_size = self._parse_frame(fh, size, id3version=major)
|
|
1051
|
-
if frame_size ==
|
|
1092
|
+
if frame_size == -1:
|
|
1052
1093
|
break
|
|
1053
1094
|
parsed_size += frame_size
|
|
1054
1095
|
fh.seek(end_pos)
|
|
1096
|
+
self._set_grouping_work_fields()
|
|
1055
1097
|
|
|
1056
1098
|
def _parse_id3v1(self, fh: BinaryIO) -> None:
|
|
1057
1099
|
content = fh.read(3 + 30 + 30 + 30 + 4 + 30 + 1)
|
|
@@ -1093,7 +1135,7 @@ class _ID3(TinyTag):
|
|
|
1093
1135
|
if genre_id < len(self._ID3V1_GENRES):
|
|
1094
1136
|
self._set_field('genre', self._ID3V1_GENRES[genre_id])
|
|
1095
1137
|
|
|
1096
|
-
def
|
|
1138
|
+
def _parse_custom_field(self, content: str) -> bool:
|
|
1097
1139
|
custom_field_name, separator, value = content.partition('\x00')
|
|
1098
1140
|
custom_field_name_lower = custom_field_name.lower()
|
|
1099
1141
|
value = value.lstrip('\ufeff')
|
|
@@ -1105,6 +1147,18 @@ class _ID3(TinyTag):
|
|
|
1105
1147
|
return True
|
|
1106
1148
|
return False
|
|
1107
1149
|
|
|
1150
|
+
def _set_grouping_work_fields(self) -> None:
|
|
1151
|
+
# iTunes 12.5.4.42 added a new GRP1 frame for 'grouping', and
|
|
1152
|
+
# repurposed the TIT1 frame for 'work'. Handle this mess here.
|
|
1153
|
+
if self._modern_grouping_values:
|
|
1154
|
+
for value in self._modern_grouping_values:
|
|
1155
|
+
self._set_field('other.grouping', value)
|
|
1156
|
+
for value in self._legacy_grouping_values:
|
|
1157
|
+
self._set_field('other.work', value)
|
|
1158
|
+
return
|
|
1159
|
+
for value in self._legacy_grouping_values:
|
|
1160
|
+
self._set_field('other.grouping', value)
|
|
1161
|
+
|
|
1108
1162
|
@classmethod
|
|
1109
1163
|
def _create_tag_image(cls,
|
|
1110
1164
|
data: bytes,
|
|
@@ -1124,6 +1178,75 @@ class _ID3(TinyTag):
|
|
|
1124
1178
|
image.description = description
|
|
1125
1179
|
return field_name, image
|
|
1126
1180
|
|
|
1181
|
+
def _parse_image(self,
|
|
1182
|
+
frame_id: bytes,
|
|
1183
|
+
content: bytes) -> tuple[str, Image]:
|
|
1184
|
+
# See section 4.14: http://id3.org/id3v2.4.0-frames
|
|
1185
|
+
encoding = content[:1]
|
|
1186
|
+
if frame_id == b'PIC': # ID3 v2.2:
|
|
1187
|
+
imgformat = content[1:4].lower()
|
|
1188
|
+
mime_type = self._ID3V2_2_IMAGE_FORMATS.get(imgformat)
|
|
1189
|
+
# skip encoding (1), imgformat (3), pictype(1)
|
|
1190
|
+
desc_start_pos = 5
|
|
1191
|
+
else: # ID3 v2.3+
|
|
1192
|
+
mime_start_pos = 1
|
|
1193
|
+
mime_end_pos = self._find_string_end_pos(
|
|
1194
|
+
content, start_pos=mime_start_pos)
|
|
1195
|
+
mime_type = self._decode_string(
|
|
1196
|
+
content[mime_start_pos:mime_end_pos]).lower()
|
|
1197
|
+
# skip mtype, pictype(1)
|
|
1198
|
+
desc_start_pos = mime_end_pos + 1
|
|
1199
|
+
pic_type = content[desc_start_pos - 1]
|
|
1200
|
+
desc_end_pos = self._find_string_end_pos(
|
|
1201
|
+
content, encoding, desc_start_pos)
|
|
1202
|
+
# skip stray null byte in broken file
|
|
1203
|
+
if (desc_end_pos + 1 < len(content)
|
|
1204
|
+
and content[desc_end_pos] == 0
|
|
1205
|
+
and content[desc_end_pos + 1] != 0):
|
|
1206
|
+
desc_end_pos += 1
|
|
1207
|
+
desc = self._decode_string(
|
|
1208
|
+
encoding + content[desc_start_pos:desc_end_pos])
|
|
1209
|
+
return self._create_tag_image(
|
|
1210
|
+
content[desc_end_pos:], pic_type, mime_type, desc)
|
|
1211
|
+
|
|
1212
|
+
@staticmethod
|
|
1213
|
+
def _lrc_timestamp(seconds: float) -> str:
|
|
1214
|
+
cs = int(seconds * 100)
|
|
1215
|
+
minutes, cs = divmod(cs, 6000)
|
|
1216
|
+
seconds, cs = divmod(cs, 100)
|
|
1217
|
+
return f"{minutes:02d}:{seconds:02d}.{cs:02d}"
|
|
1218
|
+
|
|
1219
|
+
def _parse_synced_lyrics(self, content: bytes) -> str:
|
|
1220
|
+
# Convert ID3 synced lyrics to LRC format
|
|
1221
|
+
content_length = len(content)
|
|
1222
|
+
encoding = content[:1]
|
|
1223
|
+
# skip language (3)
|
|
1224
|
+
timestamp_format = content[4:5]
|
|
1225
|
+
# skip content type (1)
|
|
1226
|
+
start_pos = 6
|
|
1227
|
+
end_pos = self._find_string_end_pos(content, encoding, start_pos)
|
|
1228
|
+
lyrics = ""
|
|
1229
|
+
offset = end_pos
|
|
1230
|
+
found_line = False
|
|
1231
|
+
while offset < content_length:
|
|
1232
|
+
end_pos = self._find_string_end_pos(content, encoding, offset)
|
|
1233
|
+
value = self._decode_string(
|
|
1234
|
+
encoding + content[offset:end_pos]).lstrip('\n')
|
|
1235
|
+
offset = end_pos
|
|
1236
|
+
time = unpack('>I', content[offset:offset + 4])[0]
|
|
1237
|
+
offset += 4
|
|
1238
|
+
if found_line:
|
|
1239
|
+
lyrics += '\n'
|
|
1240
|
+
found_line = True
|
|
1241
|
+
if timestamp_format == b'\x02':
|
|
1242
|
+
# time in milliseconds
|
|
1243
|
+
timestamp = self._lrc_timestamp(time / 1000)
|
|
1244
|
+
else:
|
|
1245
|
+
lyrics += value
|
|
1246
|
+
continue
|
|
1247
|
+
lyrics += f'[{timestamp}]{value}'
|
|
1248
|
+
return lyrics
|
|
1249
|
+
|
|
1127
1250
|
def _parse_frame(self,
|
|
1128
1251
|
fh: BinaryIO,
|
|
1129
1252
|
total_size: int,
|
|
@@ -1134,8 +1257,10 @@ class _ID3(TinyTag):
|
|
|
1134
1257
|
is_synchsafe_int = id3version == 4
|
|
1135
1258
|
header = fh.read(header_len)
|
|
1136
1259
|
if len(header) != header_len:
|
|
1137
|
-
return
|
|
1138
|
-
frame_id =
|
|
1260
|
+
return -1
|
|
1261
|
+
frame_id = header[:frame_size_bytes]
|
|
1262
|
+
if frame_id in self._EMPTY_FRAME_IDS:
|
|
1263
|
+
return -1
|
|
1139
1264
|
frame_size: int
|
|
1140
1265
|
if frame_size_bytes == 3:
|
|
1141
1266
|
frame_size = unpack('>I', b'\x00' + header[3:6])[0]
|
|
@@ -1144,15 +1269,13 @@ class _ID3(TinyTag):
|
|
|
1144
1269
|
else:
|
|
1145
1270
|
frame_size = unpack('>I', header[4:8])[0]
|
|
1146
1271
|
if _DEBUG:
|
|
1147
|
-
print(f'Found id3 Frame {frame_id} at '
|
|
1272
|
+
print(f'Found id3 Frame {frame_id!r} at '
|
|
1148
1273
|
f'{fh.tell()}-{fh.tell() + frame_size} of {self.filesize}')
|
|
1149
1274
|
if frame_size > total_size:
|
|
1150
1275
|
# invalid frame size, stop here
|
|
1151
|
-
return
|
|
1276
|
+
return -1
|
|
1152
1277
|
should_set_field = True
|
|
1153
|
-
if frame_id in self._ID3_MAPPING:
|
|
1154
|
-
if not self._parse_tags:
|
|
1155
|
-
return frame_size
|
|
1278
|
+
if self._parse_tags and frame_id in self._ID3_MAPPING:
|
|
1156
1279
|
fieldname = self._ID3_MAPPING[frame_id]
|
|
1157
1280
|
language = fieldname in {'comment', 'other.lyrics'}
|
|
1158
1281
|
value = self._decode_string(fh.read(frame_size), language)
|
|
@@ -1160,8 +1283,8 @@ class _ID3(TinyTag):
|
|
|
1160
1283
|
return frame_size
|
|
1161
1284
|
if fieldname == "comment":
|
|
1162
1285
|
# check if comment is a key-value pair (used by iTunes)
|
|
1163
|
-
should_set_field = not self.
|
|
1164
|
-
elif fieldname in {'track', 'disc'}:
|
|
1286
|
+
should_set_field = not self._parse_custom_field(value)
|
|
1287
|
+
elif fieldname in {'track', 'disc', 'other.movement'}:
|
|
1165
1288
|
if '/' in value:
|
|
1166
1289
|
value, total = value.split('/')[:2]
|
|
1167
1290
|
if total.isdecimal():
|
|
@@ -1182,62 +1305,52 @@ class _ID3(TinyTag):
|
|
|
1182
1305
|
genre_id = int(parens_text)
|
|
1183
1306
|
if 0 <= genre_id < len(self._ID3V1_GENRES):
|
|
1184
1307
|
value = self._ID3V1_GENRES[genre_id]
|
|
1308
|
+
elif fieldname == 'modern_grouping':
|
|
1309
|
+
self._modern_grouping_values.append(value)
|
|
1310
|
+
should_set_field = False
|
|
1311
|
+
elif fieldname == 'legacy_grouping':
|
|
1312
|
+
self._legacy_grouping_values.append(value)
|
|
1313
|
+
should_set_field = False
|
|
1185
1314
|
if should_set_field:
|
|
1186
1315
|
self._set_field(fieldname, value)
|
|
1187
|
-
elif frame_id in self.
|
|
1316
|
+
elif self._parse_tags and frame_id in self._SYNCED_LYRICS_FRAME_IDS:
|
|
1317
|
+
lyrics = self._parse_synced_lyrics(fh.read(frame_size))
|
|
1318
|
+
self._set_field('other.lyrics', lyrics)
|
|
1319
|
+
elif self._parse_tags and frame_id in self._CUSTOM_FRAME_IDS:
|
|
1188
1320
|
# custom fields
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
elif frame_id in self._IMAGE_FRAME_IDS:
|
|
1194
|
-
if self._load_image:
|
|
1195
|
-
# See section 4.14: http://id3.org/id3v2.4.0-frames
|
|
1196
|
-
content = fh.read(frame_size)
|
|
1197
|
-
encoding = content[:1]
|
|
1198
|
-
if frame_id == 'PIC': # ID3 v2.2:
|
|
1199
|
-
imgformat = self._decode_string(content[1:4]).lower()
|
|
1200
|
-
mime_type = self._ID3V2_2_IMAGE_FORMATS.get(imgformat)
|
|
1201
|
-
# skip encoding (1), imgformat (3), pictype(1)
|
|
1202
|
-
desc_start_pos = 5
|
|
1203
|
-
else: # ID3 v2.3+
|
|
1204
|
-
mime_end_pos = content.index(b'\x00', 1)
|
|
1205
|
-
mime_type = self._decode_string(
|
|
1206
|
-
content[1:mime_end_pos]).lower()
|
|
1207
|
-
# skip mtype, pictype(1)
|
|
1208
|
-
desc_start_pos = mime_end_pos + 2
|
|
1209
|
-
pic_type = content[desc_start_pos - 1]
|
|
1210
|
-
# latin1 and utf-8 are 1 byte
|
|
1211
|
-
if encoding in {b'\x00', b'\x03'}:
|
|
1212
|
-
desc_end_pos = content.find(b'\x00', desc_start_pos) + 1
|
|
1213
|
-
else:
|
|
1214
|
-
desc_end_pos = 0
|
|
1215
|
-
for i in range(desc_start_pos, len(content), 2):
|
|
1216
|
-
if content[i:i + 2] == b'\x00\x00':
|
|
1217
|
-
desc_end_pos = i + 2
|
|
1218
|
-
break
|
|
1219
|
-
# skip stray null byte in broken file
|
|
1220
|
-
if (desc_end_pos + 1 < len(content)
|
|
1221
|
-
and content[desc_end_pos] == 0
|
|
1222
|
-
and content[desc_end_pos + 1] != 0):
|
|
1223
|
-
desc_end_pos += 1
|
|
1224
|
-
desc = self._decode_string(
|
|
1225
|
-
encoding + content[desc_start_pos:desc_end_pos])
|
|
1226
|
-
field_name, image = self._create_tag_image(
|
|
1227
|
-
content[desc_end_pos:], pic_type, mime_type, desc)
|
|
1228
|
-
# pylint: disable=protected-access
|
|
1229
|
-
self.images._set_field(field_name, image)
|
|
1230
|
-
elif frame_id not in self._IGNORED_FRAME_IDS:
|
|
1321
|
+
value = self._decode_string(fh.read(frame_size))
|
|
1322
|
+
if value:
|
|
1323
|
+
self._parse_custom_field(value)
|
|
1324
|
+
elif self._parse_tags and frame_id not in self._IGNORED_FRAME_IDS:
|
|
1231
1325
|
# unknown, try to add to other dict
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
self.
|
|
1236
|
-
|
|
1326
|
+
value = self._decode_string(fh.read(frame_size))
|
|
1327
|
+
if value:
|
|
1328
|
+
self._set_field(
|
|
1329
|
+
self._OTHER_PREFIX + frame_id.decode('latin-1').lower(),
|
|
1330
|
+
value)
|
|
1331
|
+
elif self._load_image and frame_id in self._IMAGE_FRAME_IDS:
|
|
1332
|
+
field_name, image = self._parse_image(
|
|
1333
|
+
frame_id, fh.read(frame_size))
|
|
1334
|
+
# pylint: disable=protected-access
|
|
1335
|
+
self.images._set_field(field_name, image)
|
|
1237
1336
|
else: # skip frame
|
|
1238
1337
|
fh.seek(frame_size, SEEK_CUR)
|
|
1239
1338
|
return frame_size
|
|
1240
1339
|
|
|
1340
|
+
@staticmethod
|
|
1341
|
+
def _find_string_end_pos(content: bytes,
|
|
1342
|
+
encoding: bytes = b'\x00',
|
|
1343
|
+
start_pos: int = 0) -> int:
|
|
1344
|
+
# latin1 and utf-8 are 1 byte
|
|
1345
|
+
if encoding in {b'\x00', b'\x03'}:
|
|
1346
|
+
return content.find(b'\x00', start_pos) + 1
|
|
1347
|
+
end_pos = 0
|
|
1348
|
+
for i in range(start_pos, len(content), 2):
|
|
1349
|
+
if content[i:i + 2] == b'\x00\x00':
|
|
1350
|
+
end_pos = i + 2
|
|
1351
|
+
break
|
|
1352
|
+
return end_pos
|
|
1353
|
+
|
|
1241
1354
|
def _decode_string(self, value: bytes, language: bool = False) -> str:
|
|
1242
1355
|
default_encoding = 'ISO-8859-1'
|
|
1243
1356
|
if self._default_encoding:
|
|
@@ -1310,6 +1423,7 @@ class _Ogg(TinyTag):
|
|
|
1310
1423
|
'copyright': 'other.copyright',
|
|
1311
1424
|
'isrc': 'other.isrc',
|
|
1312
1425
|
'lyrics': 'other.lyrics',
|
|
1426
|
+
'unsyncedlyrics': 'other.lyrics',
|
|
1313
1427
|
'publisher': 'other.publisher',
|
|
1314
1428
|
'language': 'other.language',
|
|
1315
1429
|
'director': 'other.director',
|
|
@@ -1326,6 +1440,13 @@ class _Ogg(TinyTag):
|
|
|
1326
1440
|
'license': 'other.license',
|
|
1327
1441
|
'barcode': 'other.barcode',
|
|
1328
1442
|
'catalognumber': 'other.catalog_number',
|
|
1443
|
+
'movementname': 'other.movement_name',
|
|
1444
|
+
'movement': 'other.movement',
|
|
1445
|
+
'movementtotal': 'other.movement_total',
|
|
1446
|
+
'showmovement': 'other.show_movement',
|
|
1447
|
+
'grouping': 'other.grouping',
|
|
1448
|
+
'contentgroup': 'other.grouping',
|
|
1449
|
+
'work': 'other.work'
|
|
1329
1450
|
}
|
|
1330
1451
|
|
|
1331
1452
|
def __init__(self) -> None:
|
|
@@ -1516,6 +1637,8 @@ class _Ogg(TinyTag):
|
|
|
1516
1637
|
else:
|
|
1517
1638
|
self._audio_size += last_audio_size
|
|
1518
1639
|
last_audio_size = audio_size
|
|
1640
|
+
if eos:
|
|
1641
|
+
break
|
|
1519
1642
|
page_header = fh.read(header_len)
|
|
1520
1643
|
|
|
1521
1644
|
|
|
@@ -1568,7 +1691,7 @@ class _Wave(TinyTag):
|
|
|
1568
1691
|
subchunk_size = unpack('I', chunk_header[4:])[0]
|
|
1569
1692
|
# IFF chunks are padded to an even number of bytes
|
|
1570
1693
|
subchunk_size += subchunk_size % 2
|
|
1571
|
-
if subchunk_id == b'fmt '
|
|
1694
|
+
if self._parse_duration and subchunk_id == b'fmt ':
|
|
1572
1695
|
chunk = fh.read(subchunk_size)
|
|
1573
1696
|
_format_tag, channels, samplerate = unpack('<HHI', chunk[:8])
|
|
1574
1697
|
bitdepth = unpack('<H', chunk[14:16])[0]
|
|
@@ -1579,14 +1702,14 @@ class _Wave(TinyTag):
|
|
|
1579
1702
|
self.bitrate = samplerate * channels * bitdepth / 1000
|
|
1580
1703
|
self.channels, self.samplerate, self.bitdepth = (
|
|
1581
1704
|
channels, samplerate, bitdepth)
|
|
1582
|
-
elif subchunk_id == b'data'
|
|
1705
|
+
elif self._parse_duration and subchunk_id == b'data':
|
|
1583
1706
|
if (self.channels is not None and self.samplerate is not None
|
|
1584
1707
|
and self.bitdepth is not None):
|
|
1585
1708
|
self.duration = (
|
|
1586
1709
|
subchunk_size / self.channels / self.samplerate
|
|
1587
1710
|
/ (self.bitdepth / 8))
|
|
1588
1711
|
fh.seek(subchunk_size, SEEK_CUR)
|
|
1589
|
-
elif subchunk_id == b'LIST'
|
|
1712
|
+
elif self._parse_tags and subchunk_id == b'LIST':
|
|
1590
1713
|
chunk = fh.read(subchunk_size)
|
|
1591
1714
|
if chunk.startswith(b'INFO'):
|
|
1592
1715
|
walker = BytesIO(chunk)
|
|
@@ -1607,7 +1730,7 @@ class _Wave(TinyTag):
|
|
|
1607
1730
|
else:
|
|
1608
1731
|
self._set_field(fieldname, value)
|
|
1609
1732
|
field = walker.read(4)
|
|
1610
|
-
elif subchunk_id in {b'id3 ', b'ID3 '}
|
|
1733
|
+
elif self._parse_tags and subchunk_id in {b'id3 ', b'ID3 '}:
|
|
1611
1734
|
# pylint: disable=protected-access
|
|
1612
1735
|
id3 = _ID3()
|
|
1613
1736
|
id3._filehandler = fh
|
|
@@ -1651,7 +1774,7 @@ class _Flac(TinyTag):
|
|
|
1651
1774
|
is_last_block = block_header[0] & 0x80
|
|
1652
1775
|
size = unpack('>I', b'\x00' + block_header[1:])[0]
|
|
1653
1776
|
# http://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
1654
|
-
if block_type == self._STREAMINFO
|
|
1777
|
+
if self._parse_duration and block_type == self._STREAMINFO:
|
|
1655
1778
|
head = fh.read(size)
|
|
1656
1779
|
if len(head) < 34: # invalid streaminfo
|
|
1657
1780
|
break
|
|
@@ -1681,13 +1804,13 @@ class _Flac(TinyTag):
|
|
|
1681
1804
|
self.samplerate = sr
|
|
1682
1805
|
if duration > 0:
|
|
1683
1806
|
self.bitrate = self.filesize * 8 / duration / 1000
|
|
1684
|
-
elif block_type == self._VORBIS_COMMENT
|
|
1807
|
+
elif self._parse_tags and block_type == self._VORBIS_COMMENT:
|
|
1685
1808
|
# pylint: disable=protected-access
|
|
1686
1809
|
walker = BytesIO(fh.read(size))
|
|
1687
1810
|
oggtag = _Ogg()
|
|
1688
1811
|
oggtag._parse_vorbis_comment(walker)
|
|
1689
1812
|
self._update(oggtag)
|
|
1690
|
-
elif block_type == self._PICTURE
|
|
1813
|
+
elif self._load_image and block_type == self._PICTURE:
|
|
1691
1814
|
fieldname, value = self._parse_image(fh)
|
|
1692
1815
|
# pylint: disable=protected-access
|
|
1693
1816
|
self.images._set_field(fieldname, value)
|
|
@@ -1746,6 +1869,8 @@ class _Wma(TinyTag):
|
|
|
1746
1869
|
'WM/Media': 'other.media',
|
|
1747
1870
|
'WM/Barcode': 'other.barcode',
|
|
1748
1871
|
'WM/CatalogNo': 'other.catalog_number',
|
|
1872
|
+
'WM/ContentGroupDescription': 'other.grouping',
|
|
1873
|
+
'WM/Work': 'other.work'
|
|
1749
1874
|
}
|
|
1750
1875
|
_UNPACK_FORMATS = {
|
|
1751
1876
|
1: '<B',
|
|
@@ -1781,7 +1906,7 @@ class _Wma(TinyTag):
|
|
|
1781
1906
|
if object_size == 0 or object_size > self.filesize:
|
|
1782
1907
|
break # invalid object, stop parsing.
|
|
1783
1908
|
object_id = object_header[:16]
|
|
1784
|
-
if object_id == self._ASF_CONTENT_DESC
|
|
1909
|
+
if self._parse_tags and object_id == self._ASF_CONTENT_DESC:
|
|
1785
1910
|
walker = BytesIO(fh.read(object_size - header_len))
|
|
1786
1911
|
(title_length, author_length,
|
|
1787
1912
|
copyright_length, description_length,
|
|
@@ -1798,7 +1923,7 @@ class _Wma(TinyTag):
|
|
|
1798
1923
|
walker.read(length).decode('utf-16', 'replace'))
|
|
1799
1924
|
if not i_field_name.startswith('_') and value:
|
|
1800
1925
|
self._set_field(i_field_name, value)
|
|
1801
|
-
elif object_id == self._ASF_EXT_CONTENT_DESC
|
|
1926
|
+
elif self._parse_tags and object_id == self._ASF_EXT_CONTENT_DESC:
|
|
1802
1927
|
# http://web.archive.org/web/20131203084402/http://msdn.microsoft.com/en-us/library/bb643323.aspx#_Toc509555195
|
|
1803
1928
|
walker = BytesIO(fh.read(object_size - header_len))
|
|
1804
1929
|
descriptor_count = unpack('<H', walker.read(2))[0]
|
|
@@ -1831,13 +1956,13 @@ class _Wma(TinyTag):
|
|
|
1831
1956
|
self._set_field(field_name, int(value))
|
|
1832
1957
|
elif value:
|
|
1833
1958
|
self._set_field(field_name, value)
|
|
1834
|
-
elif object_id == self._ASF_FILE_PROP
|
|
1959
|
+
elif self._parse_duration and object_id == self._ASF_FILE_PROP:
|
|
1835
1960
|
data = fh.read(object_size - header_len)
|
|
1836
1961
|
play_duration = unpack('<Q', data[40:48])[0] / 10000000
|
|
1837
1962
|
preroll = unpack('<Q', data[56:64])[0] / 1000
|
|
1838
1963
|
# subtract the preroll to get the actual duration
|
|
1839
1964
|
self.duration = max(play_duration - preroll, 0.0)
|
|
1840
|
-
elif object_id == self._ASF_STREAM_PROPS
|
|
1965
|
+
elif self._parse_duration and object_id == self._ASF_STREAM_PROPS:
|
|
1841
1966
|
data = fh.read(object_size - header_len)
|
|
1842
1967
|
stream_type = data[:16]
|
|
1843
1968
|
if stream_type == self._STREAM_TYPE_ASF_AUDIO_MEDIA:
|
|
@@ -1893,11 +2018,11 @@ class _Aiff(TinyTag):
|
|
|
1893
2018
|
subchunk_size = unpack('>I', chunk_header[4:])[0]
|
|
1894
2019
|
# IFF chunks are padded to an even number of bytes
|
|
1895
2020
|
subchunk_size += subchunk_size % 2
|
|
1896
|
-
if subchunk_id in self._AIFF_MAPPING
|
|
2021
|
+
if self._parse_tags and subchunk_id in self._AIFF_MAPPING:
|
|
1897
2022
|
value = self._unpad(
|
|
1898
2023
|
fh.read(subchunk_size).decode('utf-8', 'replace'))
|
|
1899
2024
|
self._set_field(self._AIFF_MAPPING[subchunk_id], value)
|
|
1900
|
-
elif subchunk_id == b'COMM'
|
|
2025
|
+
elif self._parse_duration and subchunk_id == b'COMM':
|
|
1901
2026
|
chunk = fh.read(subchunk_size)
|
|
1902
2027
|
channels, num_frames, bitdepth = unpack('>hLh', chunk[:8])
|
|
1903
2028
|
self.channels, self.bitdepth = channels, bitdepth
|
|
@@ -1911,7 +2036,7 @@ class _Aiff(TinyTag):
|
|
|
1911
2036
|
sr, duration, bitrate)
|
|
1912
2037
|
except OverflowError:
|
|
1913
2038
|
pass
|
|
1914
|
-
elif subchunk_id in {b'id3 ', b'ID3 '}
|
|
2039
|
+
elif self._parse_tags and subchunk_id in {b'id3 ', b'ID3 '}:
|
|
1915
2040
|
# pylint: disable=protected-access
|
|
1916
2041
|
id3 = _ID3()
|
|
1917
2042
|
id3._filehandler = fh
|
|
File without changes
|
|
File without changes
|
|
File without changes
|