tinytag 2.1.2__tar.gz → 2.2.1__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.1}/LICENSE +1 -1
- {tinytag-2.1.2 → tinytag-2.2.1}/PKG-INFO +28 -3
- {tinytag-2.1.2 → tinytag-2.2.1}/README.md +25 -2
- {tinytag-2.1.2 → tinytag-2.2.1}/pyproject.toml +2 -0
- {tinytag-2.1.2 → tinytag-2.2.1}/tinytag/__init__.py +2 -2
- {tinytag-2.1.2 → tinytag-2.2.1}/tinytag/tinytag.py +250 -122
- {tinytag-2.1.2 → tinytag-2.2.1}/tinytag/__main__.py +0 -0
- {tinytag-2.1.2 → tinytag-2.2.1}/tinytag/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
MIT License
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2014-
|
|
3
|
+
Copyright (c) 2014-2026 Tom Wallroth, Mat (mathiascode), et al.
|
|
4
4
|
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tinytag
|
|
3
|
-
Version: 2.1
|
|
3
|
+
Version: 2.2.1
|
|
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"
|
|
@@ -35,7 +37,7 @@ Project-URL: Homepage, https://github.com/tinytag/tinytag
|
|
|
35
37
|
Provides-Extra: tests
|
|
36
38
|
|
|
37
39
|
<!--
|
|
38
|
-
SPDX-FileCopyrightText: 2014-
|
|
40
|
+
SPDX-FileCopyrightText: 2014-2026 tinytag Contributors
|
|
39
41
|
SPDX-License-Identifier: MIT
|
|
40
42
|
-->
|
|
41
43
|
|
|
@@ -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,23 @@ TinyTag.get(file_obj=your_file_obj)
|
|
|
421
429
|
|
|
422
430
|
## Changelog
|
|
423
431
|
|
|
432
|
+
### 2.2.1 (2026-03-15)
|
|
433
|
+
|
|
434
|
+
- ID3: Prevent infinite loop due to malformed SYLT strings
|
|
435
|
+
|
|
436
|
+
### 2.2.0 (2025-12-15)
|
|
437
|
+
|
|
438
|
+
- Add support for movement, work and grouping fields
|
|
439
|
+
- ID3: Make synced lyrics available in 'other.lyrics' (LRC format)
|
|
440
|
+
- ID3: Continue reading after encountering empty frame
|
|
441
|
+
- ID3: Fix frame reading when image parsing is disabled
|
|
442
|
+
- ID3: Exclude more frames containing binary data
|
|
443
|
+
- ID3: Avoid unnecessary string decoding
|
|
444
|
+
- M4A: Support extended atom sizes
|
|
445
|
+
- M4A: Ensure all field names are lowercase
|
|
446
|
+
- OGG: Stop reading after reaching EOS page
|
|
447
|
+
- Vorbis: Map UNSYNCEDLYRICS field to other.lyrics
|
|
448
|
+
|
|
424
449
|
### 2.1.2 (2025-08-14)
|
|
425
450
|
|
|
426
451
|
- M4A: Add a few missing additional metadata fields
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!--
|
|
2
|
-
SPDX-FileCopyrightText: 2014-
|
|
2
|
+
SPDX-FileCopyrightText: 2014-2026 tinytag Contributors
|
|
3
3
|
SPDX-License-Identifier: MIT
|
|
4
4
|
-->
|
|
5
5
|
|
|
@@ -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,23 @@ TinyTag.get(file_obj=your_file_obj)
|
|
|
385
391
|
|
|
386
392
|
## Changelog
|
|
387
393
|
|
|
394
|
+
### 2.2.1 (2026-03-15)
|
|
395
|
+
|
|
396
|
+
- ID3: Prevent infinite loop due to malformed SYLT strings
|
|
397
|
+
|
|
398
|
+
### 2.2.0 (2025-12-15)
|
|
399
|
+
|
|
400
|
+
- Add support for movement, work and grouping fields
|
|
401
|
+
- ID3: Make synced lyrics available in 'other.lyrics' (LRC format)
|
|
402
|
+
- ID3: Continue reading after encountering empty frame
|
|
403
|
+
- ID3: Fix frame reading when image parsing is disabled
|
|
404
|
+
- ID3: Exclude more frames containing binary data
|
|
405
|
+
- ID3: Avoid unnecessary string decoding
|
|
406
|
+
- M4A: Support extended atom sizes
|
|
407
|
+
- M4A: Ensure all field names are lowercase
|
|
408
|
+
- OGG: Stop reading after reaching EOS page
|
|
409
|
+
- Vorbis: Map UNSYNCEDLYRICS field to other.lyrics
|
|
410
|
+
|
|
388
411
|
### 2.1.2 (2025-08-14)
|
|
389
412
|
|
|
390
413
|
- 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"
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2014-
|
|
1
|
+
# SPDX-FileCopyrightText: 2014-2026 tinytag Contributors
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
"""Audio file metadata reader."""
|
|
5
5
|
|
|
6
|
-
__version__ = '2.1
|
|
6
|
+
__version__ = '2.2.1'
|
|
7
7
|
|
|
8
8
|
from .tinytag import (
|
|
9
9
|
TinyTag, Image, Images, OtherFields, OtherImages,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# SPDX-FileCopyrightText: 2014-
|
|
1
|
+
# SPDX-FileCopyrightText: 2014-2026 tinytag Contributors
|
|
2
2
|
# SPDX-License-Identifier: MIT
|
|
3
3
|
|
|
4
4
|
# tinytag - an audio file metadata reader
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|
# MIT License
|
|
8
8
|
|
|
9
|
-
# Copyright (c) 2014-
|
|
9
|
+
# Copyright (c) 2014-2026 Tom Wallroth, Mat (mathiascode), et al.
|
|
10
10
|
|
|
11
11
|
# Permission is hereby granted, free of charge, to any person obtaining a
|
|
12
12
|
# copy of this software and associated documentation files (the "Software"),
|
|
@@ -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,77 @@ 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
|
+
if offset + 4 > content_length:
|
|
1237
|
+
break
|
|
1238
|
+
time = unpack('>I', content[offset:offset + 4])[0]
|
|
1239
|
+
offset += 4
|
|
1240
|
+
if found_line:
|
|
1241
|
+
lyrics += '\n'
|
|
1242
|
+
found_line = True
|
|
1243
|
+
if timestamp_format == b'\x02':
|
|
1244
|
+
# time in milliseconds
|
|
1245
|
+
timestamp = self._lrc_timestamp(time / 1000)
|
|
1246
|
+
else:
|
|
1247
|
+
lyrics += value
|
|
1248
|
+
continue
|
|
1249
|
+
lyrics += f'[{timestamp}]{value}'
|
|
1250
|
+
return lyrics
|
|
1251
|
+
|
|
1127
1252
|
def _parse_frame(self,
|
|
1128
1253
|
fh: BinaryIO,
|
|
1129
1254
|
total_size: int,
|
|
@@ -1134,8 +1259,10 @@ class _ID3(TinyTag):
|
|
|
1134
1259
|
is_synchsafe_int = id3version == 4
|
|
1135
1260
|
header = fh.read(header_len)
|
|
1136
1261
|
if len(header) != header_len:
|
|
1137
|
-
return
|
|
1138
|
-
frame_id =
|
|
1262
|
+
return -1
|
|
1263
|
+
frame_id = header[:frame_size_bytes]
|
|
1264
|
+
if frame_id in self._EMPTY_FRAME_IDS:
|
|
1265
|
+
return -1
|
|
1139
1266
|
frame_size: int
|
|
1140
1267
|
if frame_size_bytes == 3:
|
|
1141
1268
|
frame_size = unpack('>I', b'\x00' + header[3:6])[0]
|
|
@@ -1144,15 +1271,13 @@ class _ID3(TinyTag):
|
|
|
1144
1271
|
else:
|
|
1145
1272
|
frame_size = unpack('>I', header[4:8])[0]
|
|
1146
1273
|
if _DEBUG:
|
|
1147
|
-
print(f'Found id3 Frame {frame_id} at '
|
|
1274
|
+
print(f'Found id3 Frame {frame_id!r} at '
|
|
1148
1275
|
f'{fh.tell()}-{fh.tell() + frame_size} of {self.filesize}')
|
|
1149
1276
|
if frame_size > total_size:
|
|
1150
1277
|
# invalid frame size, stop here
|
|
1151
|
-
return
|
|
1278
|
+
return -1
|
|
1152
1279
|
should_set_field = True
|
|
1153
|
-
if frame_id in self._ID3_MAPPING:
|
|
1154
|
-
if not self._parse_tags:
|
|
1155
|
-
return frame_size
|
|
1280
|
+
if self._parse_tags and frame_id in self._ID3_MAPPING:
|
|
1156
1281
|
fieldname = self._ID3_MAPPING[frame_id]
|
|
1157
1282
|
language = fieldname in {'comment', 'other.lyrics'}
|
|
1158
1283
|
value = self._decode_string(fh.read(frame_size), language)
|
|
@@ -1160,8 +1285,8 @@ class _ID3(TinyTag):
|
|
|
1160
1285
|
return frame_size
|
|
1161
1286
|
if fieldname == "comment":
|
|
1162
1287
|
# check if comment is a key-value pair (used by iTunes)
|
|
1163
|
-
should_set_field = not self.
|
|
1164
|
-
elif fieldname in {'track', 'disc'}:
|
|
1288
|
+
should_set_field = not self._parse_custom_field(value)
|
|
1289
|
+
elif fieldname in {'track', 'disc', 'other.movement'}:
|
|
1165
1290
|
if '/' in value:
|
|
1166
1291
|
value, total = value.split('/')[:2]
|
|
1167
1292
|
if total.isdecimal():
|
|
@@ -1182,62 +1307,53 @@ class _ID3(TinyTag):
|
|
|
1182
1307
|
genre_id = int(parens_text)
|
|
1183
1308
|
if 0 <= genre_id < len(self._ID3V1_GENRES):
|
|
1184
1309
|
value = self._ID3V1_GENRES[genre_id]
|
|
1310
|
+
elif fieldname == 'modern_grouping':
|
|
1311
|
+
self._modern_grouping_values.append(value)
|
|
1312
|
+
should_set_field = False
|
|
1313
|
+
elif fieldname == 'legacy_grouping':
|
|
1314
|
+
self._legacy_grouping_values.append(value)
|
|
1315
|
+
should_set_field = False
|
|
1185
1316
|
if should_set_field:
|
|
1186
1317
|
self._set_field(fieldname, value)
|
|
1187
|
-
elif frame_id in self.
|
|
1318
|
+
elif self._parse_tags and frame_id in self._SYNCED_LYRICS_FRAME_IDS:
|
|
1319
|
+
lyrics = self._parse_synced_lyrics(fh.read(frame_size))
|
|
1320
|
+
self._set_field('other.lyrics', lyrics)
|
|
1321
|
+
elif self._parse_tags and frame_id in self._CUSTOM_FRAME_IDS:
|
|
1188
1322
|
# 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:
|
|
1323
|
+
value = self._decode_string(fh.read(frame_size))
|
|
1324
|
+
if value:
|
|
1325
|
+
self._parse_custom_field(value)
|
|
1326
|
+
elif self._parse_tags and frame_id not in self._IGNORED_FRAME_IDS:
|
|
1231
1327
|
# unknown, try to add to other dict
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
self.
|
|
1236
|
-
|
|
1328
|
+
value = self._decode_string(fh.read(frame_size))
|
|
1329
|
+
if value:
|
|
1330
|
+
self._set_field(
|
|
1331
|
+
self._OTHER_PREFIX + frame_id.decode('latin-1').lower(),
|
|
1332
|
+
value)
|
|
1333
|
+
elif self._load_image and frame_id in self._IMAGE_FRAME_IDS:
|
|
1334
|
+
field_name, image = self._parse_image(
|
|
1335
|
+
frame_id, fh.read(frame_size))
|
|
1336
|
+
# pylint: disable=protected-access
|
|
1337
|
+
self.images._set_field(field_name, image)
|
|
1237
1338
|
else: # skip frame
|
|
1238
1339
|
fh.seek(frame_size, SEEK_CUR)
|
|
1239
1340
|
return frame_size
|
|
1240
1341
|
|
|
1342
|
+
@staticmethod
|
|
1343
|
+
def _find_string_end_pos(content: bytes,
|
|
1344
|
+
encoding: bytes = b'\x00',
|
|
1345
|
+
start_pos: int = 0) -> int:
|
|
1346
|
+
# latin1 and utf-8 are 1 byte
|
|
1347
|
+
if encoding in {b'\x00', b'\x03'}:
|
|
1348
|
+
end_pos = content.find(b'\x00', start_pos)
|
|
1349
|
+
return start_pos if end_pos < 0 else end_pos + 1
|
|
1350
|
+
end_pos = -1
|
|
1351
|
+
for i in range(start_pos, len(content), 2):
|
|
1352
|
+
if content[i:i + 2] == b'\x00\x00':
|
|
1353
|
+
end_pos = i + 2
|
|
1354
|
+
break
|
|
1355
|
+
return start_pos if end_pos < 0 else end_pos
|
|
1356
|
+
|
|
1241
1357
|
def _decode_string(self, value: bytes, language: bool = False) -> str:
|
|
1242
1358
|
default_encoding = 'ISO-8859-1'
|
|
1243
1359
|
if self._default_encoding:
|
|
@@ -1310,6 +1426,7 @@ class _Ogg(TinyTag):
|
|
|
1310
1426
|
'copyright': 'other.copyright',
|
|
1311
1427
|
'isrc': 'other.isrc',
|
|
1312
1428
|
'lyrics': 'other.lyrics',
|
|
1429
|
+
'unsyncedlyrics': 'other.lyrics',
|
|
1313
1430
|
'publisher': 'other.publisher',
|
|
1314
1431
|
'language': 'other.language',
|
|
1315
1432
|
'director': 'other.director',
|
|
@@ -1326,6 +1443,13 @@ class _Ogg(TinyTag):
|
|
|
1326
1443
|
'license': 'other.license',
|
|
1327
1444
|
'barcode': 'other.barcode',
|
|
1328
1445
|
'catalognumber': 'other.catalog_number',
|
|
1446
|
+
'movementname': 'other.movement_name',
|
|
1447
|
+
'movement': 'other.movement',
|
|
1448
|
+
'movementtotal': 'other.movement_total',
|
|
1449
|
+
'showmovement': 'other.show_movement',
|
|
1450
|
+
'grouping': 'other.grouping',
|
|
1451
|
+
'contentgroup': 'other.grouping',
|
|
1452
|
+
'work': 'other.work'
|
|
1329
1453
|
}
|
|
1330
1454
|
|
|
1331
1455
|
def __init__(self) -> None:
|
|
@@ -1516,6 +1640,8 @@ class _Ogg(TinyTag):
|
|
|
1516
1640
|
else:
|
|
1517
1641
|
self._audio_size += last_audio_size
|
|
1518
1642
|
last_audio_size = audio_size
|
|
1643
|
+
if eos:
|
|
1644
|
+
break
|
|
1519
1645
|
page_header = fh.read(header_len)
|
|
1520
1646
|
|
|
1521
1647
|
|
|
@@ -1568,7 +1694,7 @@ class _Wave(TinyTag):
|
|
|
1568
1694
|
subchunk_size = unpack('I', chunk_header[4:])[0]
|
|
1569
1695
|
# IFF chunks are padded to an even number of bytes
|
|
1570
1696
|
subchunk_size += subchunk_size % 2
|
|
1571
|
-
if subchunk_id == b'fmt '
|
|
1697
|
+
if self._parse_duration and subchunk_id == b'fmt ':
|
|
1572
1698
|
chunk = fh.read(subchunk_size)
|
|
1573
1699
|
_format_tag, channels, samplerate = unpack('<HHI', chunk[:8])
|
|
1574
1700
|
bitdepth = unpack('<H', chunk[14:16])[0]
|
|
@@ -1579,14 +1705,14 @@ class _Wave(TinyTag):
|
|
|
1579
1705
|
self.bitrate = samplerate * channels * bitdepth / 1000
|
|
1580
1706
|
self.channels, self.samplerate, self.bitdepth = (
|
|
1581
1707
|
channels, samplerate, bitdepth)
|
|
1582
|
-
elif subchunk_id == b'data'
|
|
1708
|
+
elif self._parse_duration and subchunk_id == b'data':
|
|
1583
1709
|
if (self.channels is not None and self.samplerate is not None
|
|
1584
1710
|
and self.bitdepth is not None):
|
|
1585
1711
|
self.duration = (
|
|
1586
1712
|
subchunk_size / self.channels / self.samplerate
|
|
1587
1713
|
/ (self.bitdepth / 8))
|
|
1588
1714
|
fh.seek(subchunk_size, SEEK_CUR)
|
|
1589
|
-
elif subchunk_id == b'LIST'
|
|
1715
|
+
elif self._parse_tags and subchunk_id == b'LIST':
|
|
1590
1716
|
chunk = fh.read(subchunk_size)
|
|
1591
1717
|
if chunk.startswith(b'INFO'):
|
|
1592
1718
|
walker = BytesIO(chunk)
|
|
@@ -1607,7 +1733,7 @@ class _Wave(TinyTag):
|
|
|
1607
1733
|
else:
|
|
1608
1734
|
self._set_field(fieldname, value)
|
|
1609
1735
|
field = walker.read(4)
|
|
1610
|
-
elif subchunk_id in {b'id3 ', b'ID3 '}
|
|
1736
|
+
elif self._parse_tags and subchunk_id in {b'id3 ', b'ID3 '}:
|
|
1611
1737
|
# pylint: disable=protected-access
|
|
1612
1738
|
id3 = _ID3()
|
|
1613
1739
|
id3._filehandler = fh
|
|
@@ -1651,7 +1777,7 @@ class _Flac(TinyTag):
|
|
|
1651
1777
|
is_last_block = block_header[0] & 0x80
|
|
1652
1778
|
size = unpack('>I', b'\x00' + block_header[1:])[0]
|
|
1653
1779
|
# http://xiph.org/flac/format.html#metadata_block_streaminfo
|
|
1654
|
-
if block_type == self._STREAMINFO
|
|
1780
|
+
if self._parse_duration and block_type == self._STREAMINFO:
|
|
1655
1781
|
head = fh.read(size)
|
|
1656
1782
|
if len(head) < 34: # invalid streaminfo
|
|
1657
1783
|
break
|
|
@@ -1681,13 +1807,13 @@ class _Flac(TinyTag):
|
|
|
1681
1807
|
self.samplerate = sr
|
|
1682
1808
|
if duration > 0:
|
|
1683
1809
|
self.bitrate = self.filesize * 8 / duration / 1000
|
|
1684
|
-
elif block_type == self._VORBIS_COMMENT
|
|
1810
|
+
elif self._parse_tags and block_type == self._VORBIS_COMMENT:
|
|
1685
1811
|
# pylint: disable=protected-access
|
|
1686
1812
|
walker = BytesIO(fh.read(size))
|
|
1687
1813
|
oggtag = _Ogg()
|
|
1688
1814
|
oggtag._parse_vorbis_comment(walker)
|
|
1689
1815
|
self._update(oggtag)
|
|
1690
|
-
elif block_type == self._PICTURE
|
|
1816
|
+
elif self._load_image and block_type == self._PICTURE:
|
|
1691
1817
|
fieldname, value = self._parse_image(fh)
|
|
1692
1818
|
# pylint: disable=protected-access
|
|
1693
1819
|
self.images._set_field(fieldname, value)
|
|
@@ -1746,6 +1872,8 @@ class _Wma(TinyTag):
|
|
|
1746
1872
|
'WM/Media': 'other.media',
|
|
1747
1873
|
'WM/Barcode': 'other.barcode',
|
|
1748
1874
|
'WM/CatalogNo': 'other.catalog_number',
|
|
1875
|
+
'WM/ContentGroupDescription': 'other.grouping',
|
|
1876
|
+
'WM/Work': 'other.work'
|
|
1749
1877
|
}
|
|
1750
1878
|
_UNPACK_FORMATS = {
|
|
1751
1879
|
1: '<B',
|
|
@@ -1781,7 +1909,7 @@ class _Wma(TinyTag):
|
|
|
1781
1909
|
if object_size == 0 or object_size > self.filesize:
|
|
1782
1910
|
break # invalid object, stop parsing.
|
|
1783
1911
|
object_id = object_header[:16]
|
|
1784
|
-
if object_id == self._ASF_CONTENT_DESC
|
|
1912
|
+
if self._parse_tags and object_id == self._ASF_CONTENT_DESC:
|
|
1785
1913
|
walker = BytesIO(fh.read(object_size - header_len))
|
|
1786
1914
|
(title_length, author_length,
|
|
1787
1915
|
copyright_length, description_length,
|
|
@@ -1798,7 +1926,7 @@ class _Wma(TinyTag):
|
|
|
1798
1926
|
walker.read(length).decode('utf-16', 'replace'))
|
|
1799
1927
|
if not i_field_name.startswith('_') and value:
|
|
1800
1928
|
self._set_field(i_field_name, value)
|
|
1801
|
-
elif object_id == self._ASF_EXT_CONTENT_DESC
|
|
1929
|
+
elif self._parse_tags and object_id == self._ASF_EXT_CONTENT_DESC:
|
|
1802
1930
|
# http://web.archive.org/web/20131203084402/http://msdn.microsoft.com/en-us/library/bb643323.aspx#_Toc509555195
|
|
1803
1931
|
walker = BytesIO(fh.read(object_size - header_len))
|
|
1804
1932
|
descriptor_count = unpack('<H', walker.read(2))[0]
|
|
@@ -1831,13 +1959,13 @@ class _Wma(TinyTag):
|
|
|
1831
1959
|
self._set_field(field_name, int(value))
|
|
1832
1960
|
elif value:
|
|
1833
1961
|
self._set_field(field_name, value)
|
|
1834
|
-
elif object_id == self._ASF_FILE_PROP
|
|
1962
|
+
elif self._parse_duration and object_id == self._ASF_FILE_PROP:
|
|
1835
1963
|
data = fh.read(object_size - header_len)
|
|
1836
1964
|
play_duration = unpack('<Q', data[40:48])[0] / 10000000
|
|
1837
1965
|
preroll = unpack('<Q', data[56:64])[0] / 1000
|
|
1838
1966
|
# subtract the preroll to get the actual duration
|
|
1839
1967
|
self.duration = max(play_duration - preroll, 0.0)
|
|
1840
|
-
elif object_id == self._ASF_STREAM_PROPS
|
|
1968
|
+
elif self._parse_duration and object_id == self._ASF_STREAM_PROPS:
|
|
1841
1969
|
data = fh.read(object_size - header_len)
|
|
1842
1970
|
stream_type = data[:16]
|
|
1843
1971
|
if stream_type == self._STREAM_TYPE_ASF_AUDIO_MEDIA:
|
|
@@ -1893,11 +2021,11 @@ class _Aiff(TinyTag):
|
|
|
1893
2021
|
subchunk_size = unpack('>I', chunk_header[4:])[0]
|
|
1894
2022
|
# IFF chunks are padded to an even number of bytes
|
|
1895
2023
|
subchunk_size += subchunk_size % 2
|
|
1896
|
-
if subchunk_id in self._AIFF_MAPPING
|
|
2024
|
+
if self._parse_tags and subchunk_id in self._AIFF_MAPPING:
|
|
1897
2025
|
value = self._unpad(
|
|
1898
2026
|
fh.read(subchunk_size).decode('utf-8', 'replace'))
|
|
1899
2027
|
self._set_field(self._AIFF_MAPPING[subchunk_id], value)
|
|
1900
|
-
elif subchunk_id == b'COMM'
|
|
2028
|
+
elif self._parse_duration and subchunk_id == b'COMM':
|
|
1901
2029
|
chunk = fh.read(subchunk_size)
|
|
1902
2030
|
channels, num_frames, bitdepth = unpack('>hLh', chunk[:8])
|
|
1903
2031
|
self.channels, self.bitdepth = channels, bitdepth
|
|
@@ -1911,7 +2039,7 @@ class _Aiff(TinyTag):
|
|
|
1911
2039
|
sr, duration, bitrate)
|
|
1912
2040
|
except OverflowError:
|
|
1913
2041
|
pass
|
|
1914
|
-
elif subchunk_id in {b'id3 ', b'ID3 '}
|
|
2042
|
+
elif self._parse_tags and subchunk_id in {b'id3 ', b'ID3 '}:
|
|
1915
2043
|
# pylint: disable=protected-access
|
|
1916
2044
|
id3 = _ID3()
|
|
1917
2045
|
id3._filehandler = fh
|
|
File without changes
|
|
File without changes
|