tinytag 2.0.0__tar.gz → 2.1.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.

@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2014-2024 Tom Wallroth, Mat (mathiascode), et al.
3
+ Copyright (c) 2014-2025 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,10 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: tinytag
3
- Version: 2.0.0
3
+ Version: 2.1.0
4
4
  Summary: Read audio file metadata
5
5
  Keywords: metadata,audio,music,mp3,m4a,wav,ogg,opus,flac,wma,aiff
6
- Author: Mat (mathiascode)
7
- Author-email: Tom Wallroth <tomwallroth@gmail.com>
6
+ Author: Tom Wallroth, Mat (mathiascode)
8
7
  Requires-Python: >=3.7
9
8
  Description-Content-Type: text/markdown
10
9
  Classifier: Programming Language :: Python
@@ -15,6 +14,7 @@ Classifier: Programming Language :: Python :: 3.9
15
14
  Classifier: Programming Language :: Python :: 3.10
16
15
  Classifier: Programming Language :: Python :: 3.11
17
16
  Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
18
  Classifier: License :: OSI Approved :: MIT License
19
19
  Classifier: Development Status :: 5 - Production/Stable
20
20
  Classifier: Environment :: Web Environment
@@ -25,6 +25,7 @@ Classifier: Topic :: Multimedia
25
25
  Classifier: Topic :: Multimedia :: Sound/Audio
26
26
  Classifier: Topic :: Multimedia :: Sound/Audio :: Analysis
27
27
  Classifier: Typing :: Typed
28
+ License-File: LICENSE
28
29
  Requires-Dist: coverage ; extra == "tests"
29
30
  Requires-Dist: mypy ; extra == "tests"
30
31
  Requires-Dist: pycodestyle ; extra == "tests"
@@ -362,7 +363,7 @@ Output:
362
363
 
363
364
  > [!WARNING]
364
365
  > `tag.images.any` has replaced `tag.get_image()` in tinytag 2.0.0.
365
- > `tag.get_image()` will be removed in a future tinytag 2.x release.
366
+ > `tag.get_image()` will be removed in the future.
366
367
 
367
368
  To receive a common image, e.g. `front_cover`:
368
369
 
@@ -421,6 +422,11 @@ TinyTag.get(file_obj=your_file_obj)
421
422
 
422
423
  ## Changelog
423
424
 
425
+ ### 2.1.0 (2025-02-23)
426
+
427
+ - Opus: Calculate audio bitrate
428
+ - Opus: Take pre-skip into account when calculating the duration
429
+
424
430
  ### 2.0.0 (2024-11-03)
425
431
 
426
432
  - **BREAKING:** Store 'disc', 'disc_total', 'track' and 'track_total' values as int instead of str
@@ -327,7 +327,7 @@ Output:
327
327
 
328
328
  > [!WARNING]
329
329
  > `tag.images.any` has replaced `tag.get_image()` in tinytag 2.0.0.
330
- > `tag.get_image()` will be removed in a future tinytag 2.x release.
330
+ > `tag.get_image()` will be removed in the future.
331
331
 
332
332
  To receive a common image, e.g. `front_cover`:
333
333
 
@@ -386,6 +386,11 @@ TinyTag.get(file_obj=your_file_obj)
386
386
 
387
387
  ## Changelog
388
388
 
389
+ ### 2.1.0 (2025-02-23)
390
+
391
+ - Opus: Calculate audio bitrate
392
+ - Opus: Take pre-skip into account when calculating the duration
393
+
389
394
  ### 2.0.0 (2024-11-03)
390
395
 
391
396
  - **BREAKING:** Store 'disc', 'disc_total', 'track' and 'track_total' values as int instead of str
@@ -7,10 +7,10 @@ build-backend = "flit_core.buildapi"
7
7
 
8
8
  [project]
9
9
  name = "tinytag"
10
- version = "2.0.0"
10
+ version = "2.1.0"
11
11
  description = "Read audio file metadata"
12
12
  authors = [
13
- {name = "Tom Wallroth", email = "tomwallroth@gmail.com"},
13
+ {name = "Tom Wallroth"},
14
14
  {name = "Mat (mathiascode)"}
15
15
  ]
16
16
  keywords = [
@@ -35,6 +35,7 @@ classifiers = [
35
35
  "Programming Language :: Python :: 3.10",
36
36
  "Programming Language :: Python :: 3.11",
37
37
  "Programming Language :: Python :: 3.12",
38
+ "Programming Language :: Python :: 3.13",
38
39
  "License :: OSI Approved :: MIT License",
39
40
  "Development Status :: 5 - Production/Stable",
40
41
  "Environment :: Web Environment",
@@ -1,4 +1,4 @@
1
- # SPDX-FileCopyrightText: 2014-2024 tinytag Contributors
1
+ # SPDX-FileCopyrightText: 2014-2025 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-2024 Tom Wallroth, Mat (mathiascode), et al.
9
+ # Copyright (c) 2014-2025 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"),
@@ -36,7 +36,7 @@ from struct import unpack
36
36
 
37
37
  # Lazy imports for type checking
38
38
  if False: # pylint: disable=using-constant-test
39
- from collections.abc import Callable, Iterator
39
+ from collections.abc import Callable, Iterator # pylint: disable-all
40
40
  from typing import Any, BinaryIO, Dict, List
41
41
 
42
42
  _StringListDict = Dict[str, List[str]]
@@ -1314,14 +1314,21 @@ class _Ogg(TinyTag):
1314
1314
 
1315
1315
  def __init__(self) -> None:
1316
1316
  super().__init__()
1317
- self._max_samplenum = 0 # maximum sample position ever read
1317
+ self._granule_pos = 0
1318
+ self._pre_skip = 0 # number of samples to skip in opus stream
1319
+ self._audio_size: int | None = None # size of opus audio stream
1318
1320
 
1319
1321
  def _determine_duration(self, fh: BinaryIO) -> None:
1320
1322
  if not self._tags_parsed:
1321
1323
  self._parse_tag(fh) # determine sample rate
1322
1324
  if self.duration is not None or not self.samplerate:
1323
1325
  return # either ogg flac or invalid file
1324
- self.duration = self._max_samplenum / self.samplerate
1326
+ self.duration = max(
1327
+ (self._granule_pos - self._pre_skip) / self.samplerate, 0
1328
+ )
1329
+ if self._audio_size is None or not self.duration:
1330
+ return # not an opus file
1331
+ self.bitrate = self._audio_size * 8 / self.duration / 1000
1325
1332
 
1326
1333
  def _parse_tag(self, fh: BinaryIO) -> None:
1327
1334
  check_flac_second_packet = False
@@ -1341,15 +1348,17 @@ class _Ogg(TinyTag):
1341
1348
  if self._parse_duration: # parse opus header
1342
1349
  # https://www.videolan.org/developers/vlc/modules/codec/opus_header.c
1343
1350
  # https://mf4.xiph.org/jenkins/view/opus/job/opusfile-unix/ws/doc/html/structOpusHead.html
1344
- version, ch = unpack("BB", packet[8:10])
1351
+ version, ch, pre_skip = unpack("<BBH", packet[8:12])
1345
1352
  if (version & 0xF0) == 0: # only major version 0 supported
1346
1353
  self.channels = ch
1347
- self.samplerate = 48000 # opus always uses 48khz
1354
+ self.samplerate = 48000
1355
+ self._pre_skip = pre_skip
1348
1356
  elif packet.startswith(b'OpusTags'):
1349
1357
  if self._parse_tags: # parse opus metadata:
1350
1358
  walker = BytesIO(packet)
1351
1359
  walker.seek(8) # jump over header name
1352
1360
  self._parse_vorbis_comment(walker)
1361
+ self._audio_size = 0 # start counting size of audio stream
1353
1362
  elif packet.startswith(b'\x7fFLAC'):
1354
1363
  # https://xiph.org/flac/ogg_mapping.html
1355
1364
  walker = BytesIO(packet)
@@ -1392,7 +1401,7 @@ class _Ogg(TinyTag):
1392
1401
  check_speex_second_packet = False
1393
1402
  else:
1394
1403
  # Optimization: If we need to determine the duration, read
1395
- # max_samplenum of remaining pages, but skip contents of
1404
+ # granule_pos of remaining pages, but skip contents of
1396
1405
  # segments. If we don't need the duration, stop here.
1397
1406
  self._tags_parsed = True
1398
1407
  if not self._parse_duration:
@@ -1444,6 +1453,9 @@ class _Ogg(TinyTag):
1444
1453
  def _parse_pages(self, fh: BinaryIO) -> Iterator[bytes]:
1445
1454
  # for the spec, see: https://wiki.xiph.org/Ogg
1446
1455
  packet_data = bytearray()
1456
+ current_serial = None
1457
+ last_granule_pos = 0
1458
+ last_audio_size = 0
1447
1459
  header_len = 27
1448
1460
  page_header = fh.read(header_len) # read ogg page header
1449
1461
  while len(page_header) == header_len:
@@ -1451,26 +1463,43 @@ class _Ogg(TinyTag):
1451
1463
  if page_header[:4] != b'OggS' or version != 0:
1452
1464
  raise ParseError('Invalid OGG header')
1453
1465
  # https://xiph.org/ogg/doc/framing.html
1454
- pos = unpack('<q', page_header[6:14])[0]
1466
+ header_type = page_header[5]
1467
+ eos = header_type & 0x04
1468
+ granule_pos, serial = unpack('<qI', page_header[6:18])
1469
+ if current_serial is None:
1470
+ current_serial = serial
1471
+ serial_match = serial == current_serial
1472
+ if serial_match and granule_pos > 0:
1473
+ if eos:
1474
+ self._granule_pos = granule_pos
1475
+ else:
1476
+ self._granule_pos = last_granule_pos
1477
+ last_granule_pos = granule_pos
1455
1478
  segments = page_header[26]
1456
- # pylint: disable=consider-using-max-builtin
1457
- if pos > self._max_samplenum:
1458
- self._max_samplenum = pos
1459
1479
  seg_sizes = unpack('B' * segments, fh.read(segments))
1460
1480
  read_size = 0
1481
+ audio_size = 0
1461
1482
  for seg_size in seg_sizes: # read all segments
1462
1483
  read_size += seg_size
1484
+ if self._audio_size is not None:
1485
+ audio_size += seg_size
1463
1486
  # less than 255 bytes means end of packet
1464
- if seg_size < 255 and not self._tags_parsed:
1487
+ if seg_size < 255 and serial_match and not self._tags_parsed:
1465
1488
  packet_data += fh.read(read_size)
1466
1489
  yield packet_data
1467
1490
  packet_data.clear()
1468
1491
  read_size = 0
1469
1492
  if read_size:
1470
- if self._tags_parsed:
1493
+ if not serial_match or self._tags_parsed:
1471
1494
  fh.seek(read_size, SEEK_CUR)
1472
1495
  else: # packet continues on next page
1473
1496
  packet_data += fh.read(read_size)
1497
+ if serial_match and self._audio_size is not None:
1498
+ if eos:
1499
+ self._audio_size += last_audio_size + audio_size
1500
+ else:
1501
+ self._audio_size += last_audio_size
1502
+ last_audio_size = audio_size
1474
1503
  page_header = fh.read(header_len)
1475
1504
 
1476
1505
 
@@ -1635,7 +1664,7 @@ class _Flac(TinyTag):
1635
1664
  self.duration = duration = tot_samples / sr
1636
1665
  self.samplerate = sr
1637
1666
  if duration > 0:
1638
- self.bitrate = self.filesize / duration * 8 / 1000
1667
+ self.bitrate = self.filesize * 8 / duration / 1000
1639
1668
  elif block_type == self._VORBIS_COMMENT and self._parse_tags:
1640
1669
  # pylint: disable=protected-access
1641
1670
  walker = BytesIO(fh.read(size))
File without changes
File without changes
File without changes