tinytag 2.1.0__tar.gz → 2.1.2__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tinytag
3
- Version: 2.1.0
3
+ Version: 2.1.2
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)
@@ -30,7 +30,7 @@ Requires-Dist: coverage ; extra == "tests"
30
30
  Requires-Dist: mypy ; extra == "tests"
31
31
  Requires-Dist: pycodestyle ; extra == "tests"
32
32
  Requires-Dist: pylint ; extra == "tests"
33
- Requires-Dist: pytest ; extra == "tests"
33
+ Requires-Dist: pyright ; extra == "tests"
34
34
  Project-URL: Homepage, https://github.com/tinytag/tinytag
35
35
  Provides-Extra: tests
36
36
 
@@ -45,8 +45,6 @@ tinytag is a Python library for reading audio file metadata
45
45
 
46
46
  [![Build Status](https://img.shields.io/github/actions/workflow/status/tinytag/tinytag/tests.yml
47
47
  )](https://github.com/tinytag/tinytag/actions?query=workflow:%22Tests%22)
48
- [![Coverage Status](https://img.shields.io/coverallsCoverage/github/tinytag/tinytag
49
- )](https://coveralls.io/r/tinytag/tinytag)
50
48
  [![PyPI Version](https://img.shields.io/pypi/v/tinytag
51
49
  )](https://pypi.org/project/tinytag/)
52
50
  [![PyPI Downloads](https://img.shields.io/pypi/dm/tinytag
@@ -77,6 +75,10 @@ python3 -m pip install tinytag
77
75
  * Pure Python, no dependencies
78
76
  * Supports Python 3.7 or higher
79
77
 
78
+ > [!IMPORTANT]
79
+ > Support for changing/writing metadata will not be added. Use another library
80
+ > such as [Mutagen](https://mutagen.readthedocs.io/) for this.
81
+
80
82
 
81
83
  ## Usage
82
84
 
@@ -130,9 +132,6 @@ Alternatively you can use tinytag directly on the command line:
130
132
  Check `python3 -m tinytag --help` for all CLI options, for example other
131
133
  output formats.
132
134
 
133
- Support for changing/writing metadata will not be added. Use another library
134
- such as [Mutagen](https://mutagen.readthedocs.io/) for this.
135
-
136
135
  ### Supported Files
137
136
 
138
137
  To receive a tuple of file extensions tinytag supports, use the
@@ -422,6 +421,23 @@ TinyTag.get(file_obj=your_file_obj)
422
421
 
423
422
  ## Changelog
424
423
 
424
+ ### 2.1.2 (2025-08-14)
425
+
426
+ - M4A: Add a few missing additional metadata fields
427
+ - M4A: Support '©com' composer atom
428
+ - M4A: Fix reading of multi-value custom fields
429
+ - M4A: Use correct encoding when reading data names
430
+ - ID3: Don't read entire file to determine duration
431
+ - ID3: Skip stray null byte before image data
432
+ - Add missing `__version__` attribute
433
+ - Avoid some unnecessary work in hot code paths
434
+ - Improve a few incomplete type hints
435
+
436
+ ### 2.1.1 (2025-04-23)
437
+
438
+ - ID3: Stop removing 'b' character from strings
439
+ - Port unit tests from pytest to built-in unittest module
440
+
425
441
  ### 2.1.0 (2025-02-23)
426
442
 
427
443
  - Opus: Calculate audio bitrate
@@ -9,8 +9,6 @@ tinytag is a Python library for reading audio file metadata
9
9
 
10
10
  [![Build Status](https://img.shields.io/github/actions/workflow/status/tinytag/tinytag/tests.yml
11
11
  )](https://github.com/tinytag/tinytag/actions?query=workflow:%22Tests%22)
12
- [![Coverage Status](https://img.shields.io/coverallsCoverage/github/tinytag/tinytag
13
- )](https://coveralls.io/r/tinytag/tinytag)
14
12
  [![PyPI Version](https://img.shields.io/pypi/v/tinytag
15
13
  )](https://pypi.org/project/tinytag/)
16
14
  [![PyPI Downloads](https://img.shields.io/pypi/dm/tinytag
@@ -41,6 +39,10 @@ python3 -m pip install tinytag
41
39
  * Pure Python, no dependencies
42
40
  * Supports Python 3.7 or higher
43
41
 
42
+ > [!IMPORTANT]
43
+ > Support for changing/writing metadata will not be added. Use another library
44
+ > such as [Mutagen](https://mutagen.readthedocs.io/) for this.
45
+
44
46
 
45
47
  ## Usage
46
48
 
@@ -94,9 +96,6 @@ Alternatively you can use tinytag directly on the command line:
94
96
  Check `python3 -m tinytag --help` for all CLI options, for example other
95
97
  output formats.
96
98
 
97
- Support for changing/writing metadata will not be added. Use another library
98
- such as [Mutagen](https://mutagen.readthedocs.io/) for this.
99
-
100
99
  ### Supported Files
101
100
 
102
101
  To receive a tuple of file extensions tinytag supports, use the
@@ -386,6 +385,23 @@ TinyTag.get(file_obj=your_file_obj)
386
385
 
387
386
  ## Changelog
388
387
 
388
+ ### 2.1.2 (2025-08-14)
389
+
390
+ - M4A: Add a few missing additional metadata fields
391
+ - M4A: Support '©com' composer atom
392
+ - M4A: Fix reading of multi-value custom fields
393
+ - M4A: Use correct encoding when reading data names
394
+ - ID3: Don't read entire file to determine duration
395
+ - ID3: Skip stray null byte before image data
396
+ - Add missing `__version__` attribute
397
+ - Avoid some unnecessary work in hot code paths
398
+ - Improve a few incomplete type hints
399
+
400
+ ### 2.1.1 (2025-04-23)
401
+
402
+ - ID3: Stop removing 'b' character from strings
403
+ - Port unit tests from pytest to built-in unittest module
404
+
389
405
  ### 2.1.0 (2025-02-23)
390
406
 
391
407
  - Opus: Calculate audio bitrate
@@ -7,7 +7,6 @@ build-backend = "flit_core.buildapi"
7
7
 
8
8
  [project]
9
9
  name = "tinytag"
10
- version = "2.1.0"
11
10
  description = "Read audio file metadata"
12
11
  authors = [
13
12
  {name = "Tom Wallroth"},
@@ -50,6 +49,7 @@ classifiers = [
50
49
  license = {file = "LICENSE"}
51
50
  readme = "README.md"
52
51
  requires-python = ">=3.7"
52
+ dynamic = ["version"]
53
53
 
54
54
  [project.urls]
55
55
  Homepage = "https://github.com/tinytag/tinytag"
@@ -60,7 +60,7 @@ tests = [
60
60
  "mypy",
61
61
  "pycodestyle",
62
62
  "pylint",
63
- "pytest"
63
+ "pyright"
64
64
  ]
65
65
 
66
66
  [tool.flit.sdist]
@@ -115,3 +115,13 @@ py-version = "3.7"
115
115
 
116
116
  [tool.mypy]
117
117
  strict = true
118
+
119
+ [tool.coverage.report]
120
+ exclude_lines = [
121
+ "if TYPE_CHECKING:"
122
+ ]
123
+ precision = 2
124
+ show_missing = true
125
+
126
+ [tool.coverage.run]
127
+ relative_files = true
@@ -3,6 +3,8 @@
3
3
 
4
4
  """Audio file metadata reader."""
5
5
 
6
+ __version__ = '2.1.2'
7
+
6
8
  from .tinytag import (
7
9
  TinyTag, Image, Images, OtherFields, OtherImages,
8
10
  TinyTagException, ParseError, UnsupportedFormatError
@@ -34,15 +34,19 @@ from io import BytesIO
34
34
  from os import PathLike, SEEK_CUR, SEEK_END, environ, fsdecode
35
35
  from struct import unpack
36
36
 
37
+ TYPE_CHECKING = False
38
+
37
39
  # Lazy imports for type checking
38
- if False: # pylint: disable=using-constant-test
40
+ if TYPE_CHECKING:
39
41
  from collections.abc import Callable, Iterator # pylint: disable-all
40
- from typing import Any, BinaryIO, Dict, List
42
+ from typing import Any, BinaryIO, Dict, List, Union
41
43
 
42
44
  _StringListDict = Dict[str, List[str]]
43
45
  _ImageListDict = Dict[str, List["Image"]]
46
+ _DataTreeDict = Dict[
47
+ bytes, Union['_DataTreeDict', Callable[..., Dict[str, Any]]]]
44
48
  else:
45
- _StringListDict = _ImageListDict = dict
49
+ _StringListDict = _ImageListDict = _DataTreeDict = dict
46
50
 
47
51
  # some of the parsers can print debug info
48
52
  _DEBUG = bool(environ.get('TINYTAG_DEBUG'))
@@ -105,7 +109,7 @@ class TinyTag:
105
109
  self._parse_tags = True
106
110
  self._load_image = False
107
111
  self._tags_parsed = False
108
- self.__dict__: dict[str, str | float | Images | OtherFields]
112
+ self.__dict__: dict[str, str | float | Images | OtherFields | None]
109
113
 
110
114
  @classmethod
111
115
  def get(cls,
@@ -257,7 +261,7 @@ class TinyTag:
257
261
  self._parse_duration = duration
258
262
  self._load_image = image
259
263
  if self._filehandler is None:
260
- return
264
+ raise ValueError("File handle is required")
261
265
  if tags:
262
266
  self._parse_tag(self._filehandler)
263
267
  if duration:
@@ -271,14 +275,11 @@ class TinyTag:
271
275
  fieldname = fieldname[len(self._OTHER_PREFIX):]
272
276
  if check_conflict and fieldname in self.__dict__:
273
277
  fieldname = '_' + fieldname
274
- other_values = self.other.get(fieldname, [])
275
- if not isinstance(value, str) or value in other_values:
276
- return
277
- other_values.append(value)
278
+ if fieldname not in self.other:
279
+ self.other[fieldname] = []
280
+ self.other[fieldname].append(str(value))
278
281
  if _DEBUG:
279
- print(
280
- f'Setting other field "{fieldname}" to "{other_values!r}"')
281
- self.other[fieldname] = other_values
282
+ print(f'Adding value "{value} to field "{fieldname}"')
282
283
  return
283
284
  old_value = self.__dict__.get(fieldname)
284
285
  new_value = value
@@ -326,7 +327,7 @@ class TinyTag:
326
327
  @staticmethod
327
328
  def _unpad(s: str) -> str:
328
329
  # certain strings *may* be terminated with a zero byte at the end
329
- return s.strip('b\x00')
330
+ return s.strip('\x00')
330
331
 
331
332
  def get_image(self) -> bytes | None:
332
333
  """Deprecated, use 'images.any' instead."""
@@ -367,7 +368,7 @@ class Images:
367
368
  self.media: Image | None = None
368
369
 
369
370
  self.other: _ImageListDict = OtherImages()
370
- self.__dict__: dict[str, Image | OtherImages]
371
+ self.__dict__: dict[str, Image | OtherImages | None]
371
372
 
372
373
  @property
373
374
  def any(self) -> Image | None:
@@ -486,9 +487,10 @@ class _MP4(TinyTag):
486
487
  }
487
488
  _VERSIONED_ATOMS = {b'meta', b'stsd'} # those have an extra 4 byte header
488
489
  _FLAGGED_ATOMS = {b'stsd'} # these also have an extra 4 byte header
490
+ _ILST_PATH = [b'ftyp', b'moov', b'udta', b'meta', b'ilst']
489
491
 
490
- _audio_data_tree: dict[bytes, Any] | None = None
491
- _meta_data_tree: dict[bytes, Any] | None = None
492
+ _audio_data_tree: _DataTreeDict | None = None
493
+ _meta_data_tree: _DataTreeDict | None = None
492
494
 
493
495
  def _determine_duration(self, fh: BinaryIO) -> None:
494
496
  # https://developer.apple.com/library/mac/documentation/QuickTime/QTFF/QTFFChap3/qtff3.html
@@ -516,9 +518,8 @@ class _MP4(TinyTag):
516
518
  b'\xa9ART': {b'data': _MP4._data_parser('artist')},
517
519
  b'\xa9alb': {b'data': _MP4._data_parser('album')},
518
520
  b'\xa9cmt': {b'data': _MP4._data_parser('comment')},
521
+ b'\xa9com': {b'data': _MP4._data_parser('composer')},
519
522
  b'\xa9con': {b'data': _MP4._data_parser('other.conductor')},
520
- # need test-data for this
521
- # b'cpil': {b'data': _MP4._data_parser('other.compilation')},
522
523
  b'\xa9day': {b'data': _MP4._data_parser('year')},
523
524
  b'\xa9des': {b'data': _MP4._data_parser('other.description')},
524
525
  b'\xa9dir': {b'data': _MP4._data_parser('other.director')},
@@ -543,7 +544,7 @@ class _MP4(TinyTag):
543
544
 
544
545
  def _traverse_atoms(self,
545
546
  fh: BinaryIO,
546
- path: dict[bytes, Any],
547
+ path: _DataTreeDict,
547
548
  stop_pos: int | None = None,
548
549
  curr_path: list[bytes] | None = None) -> None:
549
550
  header_len = 8
@@ -575,13 +576,25 @@ class _MP4(TinyTag):
575
576
  for fieldname, value in sub_path(fh.read(atom_size)).items():
576
577
  if _DEBUG:
577
578
  print(' ' * 4 * len(curr_path), 'FIELD: ', fieldname)
578
- if fieldname.startswith('images.'):
579
+ if isinstance(value, Image):
579
580
  if self._load_image:
580
581
  # pylint: disable=protected-access
581
582
  self.images._set_field(
582
583
  fieldname[len('images.'):], value)
583
- elif fieldname:
584
+ elif isinstance(value, list):
585
+ for subval in value:
586
+ self._set_field(fieldname, subval)
587
+ else:
584
588
  self._set_field(fieldname, value)
589
+ # unknown data atom, try to parse it
590
+ elif curr_path == self._ILST_PATH:
591
+ atom_end_pos = fh.tell() + atom_size
592
+ field_name = self._OTHER_PREFIX + atom_type.decode('latin-1')
593
+ fh.seek(-header_len, SEEK_CUR)
594
+ self._traverse_atoms(
595
+ fh,
596
+ path={atom_type: {b'data': self._data_parser(field_name)}},
597
+ stop_pos=atom_end_pos, curr_path=curr_path + [atom_type])
585
598
  # if no action was specified using dict or callable, jump over atom
586
599
  else:
587
600
  fh.seek(atom_size, SEEK_CUR)
@@ -591,12 +604,8 @@ class _MP4(TinyTag):
591
604
  atom_header = fh.read(header_len) # read next atom
592
605
 
593
606
  @classmethod
594
- def _data_parser(
595
- cls, fieldname: str
596
- ) -> Callable[[bytes], dict[str, int | str | bytes | None]]:
597
- def _parse_data_atom(
598
- data_atom: bytes
599
- ) -> dict[str, int | str | bytes | None]:
607
+ def _data_parser(cls, fieldname: str) -> Callable[[bytes], dict[str, str]]:
608
+ def _parse_data_atom(data_atom: bytes) -> dict[str, str]:
600
609
  data_type = unpack('>I', data_atom[:4])[0]
601
610
  data = data_atom[8:]
602
611
  value = None
@@ -607,7 +616,9 @@ class _MP4(TinyTag):
607
616
  data_len = len(data)
608
617
  if data_len in fmts:
609
618
  value = str(unpack(fmts[data_len], data)[0])
610
- return {fieldname: value}
619
+ if value:
620
+ return {fieldname: value}
621
+ return {}
611
622
  return _parse_data_atom
612
623
 
613
624
  @classmethod
@@ -645,13 +656,11 @@ class _MP4(TinyTag):
645
656
  break
646
657
 
647
658
  @classmethod
648
- def _parse_custom_field(
649
- cls, data: bytes
650
- ) -> dict[str, int | str | bytes | None]:
659
+ def _parse_custom_field(cls, data: bytes) -> dict[str, list[str]]:
651
660
  fh = BytesIO(data)
652
661
  header_len = 8
653
662
  field_name = None
654
- data_atom = b''
663
+ values = []
655
664
  atom_header = fh.read(header_len)
656
665
  while len(atom_header) == header_len:
657
666
  atom_size = unpack('>I', atom_header[:4])[0] - header_len
@@ -662,15 +671,18 @@ class _MP4(TinyTag):
662
671
  # pylint: disable=protected-access
663
672
  field_name = cls._CUSTOM_FIELD_NAME_MAPPING.get(
664
673
  field_name, TinyTag._OTHER_PREFIX + field_name)
665
- elif atom_type == b'data':
674
+ elif atom_type == b'data' and field_name:
666
675
  data_atom = fh.read(atom_size)
676
+ parser = cls._data_parser(field_name)
677
+ atom_values = parser(data_atom)
678
+ if field_name in atom_values:
679
+ values.append(atom_values[field_name])
667
680
  else:
668
681
  fh.seek(atom_size, SEEK_CUR)
669
682
  atom_header = fh.read(header_len) # read next atom
670
- if len(data_atom) < 8 or field_name is None:
671
- return {}
672
- parser = cls._data_parser(field_name)
673
- return parser(data_atom)
683
+ if field_name and values:
684
+ return {field_name: values}
685
+ return {}
674
686
 
675
687
  @classmethod
676
688
  def _parse_audio_sample_entry_mp4a(cls, data: bytes) -> dict[str, int]:
@@ -917,20 +929,17 @@ class _ID3(TinyTag):
917
929
  max_estimation_frames = (
918
930
  (self._MAX_ESTIMATION_SEC * 44100) // self._SAMPLES_PER_FRAME)
919
931
  frame_size_accu = 0
920
- audio_offset = 0
932
+ audio_offset = self._bytepos_after_id3v2
921
933
  frames = 0 # count frames for determining mp3 duration
922
934
  bitrate_accu = 0 # add up bitrates to find average bitrate to detect
923
935
  last_bitrates = set() # CBR mp3s (multiple frames with same bitrates)
924
936
  # seek to first position after id3 tag (speedup for large header)
925
937
  first_mpeg_id = None
926
938
  fh.seek(self._bytepos_after_id3v2)
927
- file_offset = fh.tell()
928
- walker = BytesIO(fh.read())
929
939
  while True:
930
940
  # reading through garbage until 11 '1' sync-bits are found
931
- header = walker.read(4)
941
+ header = fh.read(4)
932
942
  header_len = len(header)
933
- walker.seek(-header_len, SEEK_CUR)
934
943
  if header_len < 4:
935
944
  if frames:
936
945
  self.bitrate = bitrate_accu / frames
@@ -949,10 +958,12 @@ class _ID3(TinyTag):
949
958
  or mpeg_id == 1):
950
959
  # invalid frame, find next sync header
951
960
  idx = header.find(b'\xFF', 1)
952
- if idx == -1:
953
- # not found: jump over the current peek buffer
954
- idx = header_len
955
- walker.seek(max(idx, 1), SEEK_CUR)
961
+ next_offset = header_len
962
+ if idx != -1:
963
+ next_offset -= idx
964
+ fh.seek(idx - header_len, SEEK_CUR)
965
+ if frames == 0:
966
+ audio_offset += next_offset
956
967
  continue
957
968
  if first_mpeg_id is None:
958
969
  first_mpeg_id = mpeg_id
@@ -964,12 +975,12 @@ class _ID3(TinyTag):
964
975
  # all the info we need, otherwise parse multiple frames to find the
965
976
  # accurate average bitrate
966
977
  if frames == 0 and self._USE_XING_HEADER:
967
- walker_offset = walker.tell()
968
- frame_content = walker.read(frame_length)
978
+ prev_offset = header_len + audio_offset
979
+ frame_content = fh.read(frame_length)
969
980
  xing_header_offset = frame_content.find(b'Xing')
970
981
  if xing_header_offset != -1:
971
- walker.seek(walker_offset + xing_header_offset)
972
- xframes, byte_count = self._parse_xing_header(walker)
982
+ fh.seek(prev_offset + xing_header_offset)
983
+ xframes, byte_count = self._parse_xing_header(fh)
973
984
  if xframes > 0 and byte_count > 0:
974
985
  # MPEG-2 Audio Layer III uses 576 samples per frame
975
986
  samples_pf = self._SAMPLES_PER_FRAME
@@ -978,12 +989,10 @@ class _ID3(TinyTag):
978
989
  self.duration = dur = xframes * samples_pf / samplerate
979
990
  self.bitrate = byte_count * 8 / dur / 1000
980
991
  return
981
- walker.seek(walker_offset)
992
+ fh.seek(prev_offset)
982
993
 
983
994
  frames += 1 # it's most probably a mp3 frame
984
995
  bitrate_accu += frame_br
985
- if frames == 1:
986
- audio_offset = file_offset + walker.tell()
987
996
  if frames <= self._CBR_DETECTION_FRAME_COUNT:
988
997
  last_bitrates.add(frame_br)
989
998
 
@@ -1002,7 +1011,7 @@ class _ID3(TinyTag):
1002
1011
  return
1003
1012
 
1004
1013
  if frame_length > 1: # jump over current frame body
1005
- walker.seek(frame_length, SEEK_CUR)
1014
+ fh.seek(frame_length - header_len, SEEK_CUR)
1006
1015
  if self.samplerate:
1007
1016
  self.duration = frames * self._SAMPLES_PER_FRAME / self.samplerate
1008
1017
 
@@ -1045,7 +1054,8 @@ class _ID3(TinyTag):
1045
1054
  fh.seek(end_pos)
1046
1055
 
1047
1056
  def _parse_id3v1(self, fh: BinaryIO) -> None:
1048
- if fh.read(3) != b'TAG': # check if this is an ID3 v1 tag
1057
+ content = fh.read(3 + 30 + 30 + 30 + 4 + 30 + 1)
1058
+ if content[:3] != b'TAG': # check if this is an ID3 v1 tag
1049
1059
  return
1050
1060
 
1051
1061
  def asciidecode(x: bytes) -> str:
@@ -1053,24 +1063,23 @@ class _ID3(TinyTag):
1053
1063
  x.decode(self._default_encoding or 'latin1', 'replace'))
1054
1064
  # Only set fields that were not set by ID3v2 tags, as ID3v1
1055
1065
  # tags are more likely to be outdated or have encoding issues
1056
- fields = fh.read(30 + 30 + 30 + 4 + 30 + 1)
1057
1066
  if not self.title:
1058
- value = asciidecode(fields[:30])
1067
+ value = asciidecode(content[3:33])
1059
1068
  if value:
1060
1069
  self._set_field('title', value)
1061
1070
  if not self.artist:
1062
- value = asciidecode(fields[30:60])
1071
+ value = asciidecode(content[33:63])
1063
1072
  if value:
1064
1073
  self._set_field('artist', value)
1065
1074
  if not self.album:
1066
- value = asciidecode(fields[60:90])
1075
+ value = asciidecode(content[63:93])
1067
1076
  if value:
1068
1077
  self._set_field('album', value)
1069
1078
  if not self.year:
1070
- value = asciidecode(fields[90:94])
1079
+ value = asciidecode(content[93:97])
1071
1080
  if value:
1072
1081
  self._set_field('year', value)
1073
- comment = fields[94:124]
1082
+ comment = content[97:127]
1074
1083
  if b'\x00\x00' < comment[-2:] < b'\x01\x00':
1075
1084
  if self.track is None:
1076
1085
  self._set_field('track', ord(comment[-1:]))
@@ -1080,7 +1089,7 @@ class _ID3(TinyTag):
1080
1089
  if value:
1081
1090
  self._set_field('comment', value)
1082
1091
  if not self.genre:
1083
- genre_id = ord(fields[124:125])
1092
+ genre_id = ord(content[127:128])
1084
1093
  if genre_id < len(self._ID3V1_GENRES):
1085
1094
  self._set_field('genre', self._ID3V1_GENRES[genre_id])
1086
1095
 
@@ -1140,14 +1149,13 @@ class _ID3(TinyTag):
1140
1149
  if frame_size > total_size:
1141
1150
  # invalid frame size, stop here
1142
1151
  return 0
1143
- content = fh.read(frame_size)
1144
- fieldname = self._ID3_MAPPING.get(frame_id)
1145
1152
  should_set_field = True
1146
- if fieldname:
1153
+ if frame_id in self._ID3_MAPPING:
1147
1154
  if not self._parse_tags:
1148
1155
  return frame_size
1156
+ fieldname = self._ID3_MAPPING[frame_id]
1149
1157
  language = fieldname in {'comment', 'other.lyrics'}
1150
- value = self._decode_string(content, language)
1158
+ value = self._decode_string(fh.read(frame_size), language)
1151
1159
  if not value:
1152
1160
  return frame_size
1153
1161
  if fieldname == "comment":
@@ -1179,12 +1187,13 @@ class _ID3(TinyTag):
1179
1187
  elif frame_id in self._CUSTOM_FRAME_IDS:
1180
1188
  # custom fields
1181
1189
  if self._parse_tags:
1182
- value = self._decode_string(content)
1190
+ value = self._decode_string(fh.read(frame_size))
1183
1191
  if value:
1184
1192
  self.__parse_custom_field(value)
1185
1193
  elif frame_id in self._IMAGE_FRAME_IDS:
1186
1194
  if self._load_image:
1187
1195
  # See section 4.14: http://id3.org/id3v2.4.0-frames
1196
+ content = fh.read(frame_size)
1188
1197
  encoding = content[:1]
1189
1198
  if frame_id == 'PIC': # ID3 v2.2:
1190
1199
  imgformat = self._decode_string(content[1:4]).lower()
@@ -1207,6 +1216,11 @@ class _ID3(TinyTag):
1207
1216
  if content[i:i + 2] == b'\x00\x00':
1208
1217
  desc_end_pos = i + 2
1209
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
1210
1224
  desc = self._decode_string(
1211
1225
  encoding + content[desc_start_pos:desc_end_pos])
1212
1226
  field_name, image = self._create_tag_image(
@@ -1216,10 +1230,12 @@ class _ID3(TinyTag):
1216
1230
  elif frame_id not in self._IGNORED_FRAME_IDS:
1217
1231
  # unknown, try to add to other dict
1218
1232
  if self._parse_tags:
1219
- value = self._decode_string(content)
1233
+ value = self._decode_string(fh.read(frame_size))
1220
1234
  if value:
1221
1235
  self._set_field(
1222
1236
  self._OTHER_PREFIX + frame_id.lower(), value)
1237
+ else: # skip frame
1238
+ fh.seek(frame_size, SEEK_CUR)
1223
1239
  return frame_size
1224
1240
 
1225
1241
  def _decode_string(self, value: bytes, language: bool = False) -> str:
@@ -1450,7 +1466,7 @@ class _Ogg(TinyTag):
1450
1466
  elif value:
1451
1467
  self._set_field(fieldname, value)
1452
1468
 
1453
- def _parse_pages(self, fh: BinaryIO) -> Iterator[bytes]:
1469
+ def _parse_pages(self, fh: BinaryIO) -> Iterator[bytearray]:
1454
1470
  # for the spec, see: https://wiki.xiph.org/Ogg
1455
1471
  packet_data = bytearray()
1456
1472
  current_serial = None
@@ -1582,8 +1598,8 @@ class _Wave(TinyTag):
1582
1598
  data_length += data_length % 2
1583
1599
  # strip zero-byte
1584
1600
  data = walker.read(data_length).split(b'\x00', 1)[0]
1585
- fieldname = self._RIFF_MAPPING.get(field)
1586
- if fieldname:
1601
+ if field in self._RIFF_MAPPING:
1602
+ fieldname = self._RIFF_MAPPING[field]
1587
1603
  value = data.decode('utf-8', 'replace')
1588
1604
  if fieldname == 'track':
1589
1605
  if value.isdecimal():
@@ -1804,8 +1820,9 @@ class _Wma(TinyTag):
1804
1820
  walker.seek(value_len, SEEK_CUR) # skip other values
1805
1821
  continue
1806
1822
  # try to get normalized field name
1807
- field_name = self._ASF_MAPPING.get(name)
1808
- if field_name is None: # custom field
1823
+ if name in self._ASF_MAPPING:
1824
+ field_name = self._ASF_MAPPING[name]
1825
+ else: # custom field
1809
1826
  if name.startswith('WM/'):
1810
1827
  name = name[3:]
1811
1828
  field_name = self._OTHER_PREFIX + name.lower()
File without changes
File without changes
File without changes