diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000..43d31b8 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,2 @@ +# Migrated to ruff +7ff9d471b23c45b6d957e7507035af39885546ab \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f4e4539..64994d7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -3,39 +3,46 @@ name: Build on: push: branches: - - master + - master pull_request: branches: - master jobs: test: - name: '${{ matrix.os }}: ${{ matrix.tox-env }}' + name: "${{ matrix.os }}: ${{ matrix.tox-env }}" runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: - tox-env: [py37-test, py38-test, py39-test, - py310-test, pypy-test] - os: [ubuntu-22.04, windows-latest] + tox-env: + [ + py39-test, + py310-test, + py311-test, + py312-test, + py313-test, + pypy-test, + ] + os: [ubuntu-24.04, windows-latest] # Only test on a couple of versions on Windows. exclude: - - os: windows-latest - tox-env: py37-test - os: windows-latest tox-env: pypy-test # Python interpreter versions. :/ include: - - tox-env: py37-test - python: '3.7' - - tox-env: py38-test - python: '3.8' - tox-env: py39-test - python: '3.9' + python: "3.9" - tox-env: py310-test - python: '3.10' + python: "3.10" + - tox-env: py311-test + python: "3.11" + - tox-env: py312-test + python: "3.12" + - tox-env: py313-test + python: "3.13" - tox-env: pypy-test python: pypy3.9 @@ -54,5 +61,20 @@ jobs: name: Style runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - uses: TrueBrain/actions-flake8@master + - uses: actions/checkout@v4 + - name: Install Python + uses: actions/setup-python@v5 + with: + python-version: "3.9" + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install .[dev] + - name: Check style with Ruff + id: ruff + run: | + ruff check --output-format=github . + - name: Check format with Ruff + id: ruff-format + run: | + ruff format --check . diff --git a/.github/workflows/changelog_reminder.yaml b/.github/workflows/changelog_reminder.yaml new file mode 100644 index 0000000..34da326 --- /dev/null +++ b/.github/workflows/changelog_reminder.yaml @@ -0,0 +1,33 @@ +name: Verify changelog updated + +on: + pull_request_target: + types: + - opened + - ready_for_review + +jobs: + check_changes: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Get all updated Python files + id: changed-python-files + uses: tj-actions/changed-files@v46 + with: + files: | + **.py + + - name: Check for the changelog update + id: changelog-update + uses: tj-actions/changed-files@v46 + with: + files: docs/changelog.rst + + - name: Comment under the PR with a reminder + if: steps.changed-python-files.outputs.any_changed == 'true' && steps.changelog-update.outputs.any_changed == 'false' + uses: thollander/actions-comment-pull-request@v2 + with: + message: "Thank you for the PR! The changelog has not been updated, so here is a friendly reminder to check if you need to add an entry." + GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..4afc5d1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to mediafile + +First off, thanks for taking the time to contribute! ❤️ + +Please follow these guidelines to ensure a smooth contribution process. + +## Pre-requisites + +- Python 3.9 or higher +- Git + +## Setup the Development Environment + +We recommend using a virtual environment to manage dependencies. You can use `venv`, `conda`, or any other tool of your choice. + +1. Fork/Clone the repository on GitHub +```bash +git clone +cd mediafile +``` +2. Install dependencies and set up the development environment +```bash +pip install -e '.[dev]' +``` + +## Before submitting a Pull Request + +Verify that your code adheres to the project standards and conventions. Run +ruff and pytest to ensure your code is properly formatted and all tests pass. + +```bash +ruff check . +ruff format . +pytest . +``` diff --git a/docs/changelog.rst b/docs/changelog.rst new file mode 100644 index 0000000..a619b05 --- /dev/null +++ b/docs/changelog.rst @@ -0,0 +1,120 @@ +Changelog +--------- + +Upcoming +'''''''' + +- Dropped support for Python 3.7 and 3.8 +- Added minimal contribution guidelines to CONTRIBUTING.md +- Changed project linter and formatter from ``flake8`` to ``ruff``. Reformatted + the codebase with ``ruff``. +- Moved changelog into its own file, ``changelog.rst``. Also added github workflow + for automatic changelog reminders. + +v0.13.0 +''''''' + +- Add a mapping compatible with Plex and ffmpeg for the "original date" + fields. +- Remove an unnecessary dependency on `six`. +- Replace `imghdr` with `filetype` to support Python 3.13. + +v0.12.0 +''''''' + +- Add the multiple-valued properties ``artists_credit``, ``artists_sort``, + ``albumartists_credit``, and ``albumartists_sort``. + +v0.11.0 +''''''' + +- List-valued properties now return ``None`` instead of an empty list when the + underlying tags are missing altogether. + +v0.10.1 +''''''' + +- Fix a test failure that arose with Mutagen 1.46. +- Require Python 3.7 or later. + +v0.10.0 +''''''' + +- Add the multiple-valued properties ``albumtypes``, ``catalognums`` and + ``languages``. +- The ``catalognum`` property now refers to additional file tags named + ``CATALOGID`` and ``DISCOGS_CATALOG`` (but only for reading, not writing). +- The multi-valued ``albumartists`` property now refers to additional file + tags named ``ALBUM_ARTIST`` and ``ALBUM ARTISTS``. (The latter + is used only for reading.) +- The ``ListMediaField`` class now doesn't concatenate multiple lists if + found. The first available tag is used instead, like with other kinds of + fields. + +v0.9.0 +'''''' + +- Add the properties ``bitrate_mode``, ``encoder_info`` and + ``encoder_settings``. + +v0.8.1 +'''''' + +- Fix a regression in v0.8.0 that caused a crash on Python versions below 3.8. + +v0.8.0 +'''''' + +- MediaFile now requires Python 3.6 or later. +- Added support for Wave (`.wav`) files. + +v0.7.0 +'''''' + +- Mutagen 1.45.0 or later is now required. +- MediaFile can now use file-like objects (instead of just the filesystem, via + filenames). + +v0.6.0 +'''''' + +- Enforce a minimum value for SoundCheck gain values. + +v0.5.0 +'''''' + +- Refactored the distribution to use `Flit`_. + +.. _Flit: https://flit.readthedocs.io/ + +v0.4.0 +'''''' + +- Added a ``barcode`` field. +- Added new tag mappings for ``albumtype`` and ``albumstatus``. + +v0.3.0 +'''''' + +- Fixed tests for compatibility with Mutagen 1.43. +- Fix the MPEG-4 tag mapping for the ``label`` field to use the right + capitalization. + +v0.2.0 +'''''' + +- R128 gain tags are now stored in Q7.8 integer format, as per + `the relevant standard`_. +- Added an ``mb_workid`` field. +- The Python source distribution now includes an ``__init__.py`` file that + makes it easier to run the tests. + +.. _the relevant standard: https://tools.ietf.org/html/rfc7845.html#page-25 + +v0.1.0 +'''''' + +This is the first independent release of MediaFile. +It is now synchronised with the embedded version released with `beets`_ v1.4.8. + +.. _beets: https://beets.io diff --git a/docs/index.rst b/docs/index.rst index c31655b..97e3683 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -16,6 +16,9 @@ an exception. .. toctree:: :maxdepth: 2 + :hidden: + + changelog Supported Metadata Fields ------------------------- @@ -131,114 +134,3 @@ To copy tags from one MediaFile to another: g.save() - -Changelog ---------- - -v0.13.0 -''''''' - -- Add a mapping compatible with Plex and ffmpeg for the "original date" - fields. -- Remove an unnecessary dependency on `six`. -- Replace `imghdr` with `filetype` to support Python 3.13. - -v0.12.0 -''''''' - -- Add the multiple-valued properties ``artists_credit``, ``artists_sort``, - ``albumartists_credit``, and ``albumartists_sort``. - -v0.11.0 -''''''' - -- List-valued properties now return ``None`` instead of an empty list when the - underlying tags are missing altogether. - -v0.10.1 -''''''' - -- Fix a test failure that arose with Mutagen 1.46. -- Require Python 3.7 or later. - -v0.10.0 -''''''' - -- Add the multiple-valued properties ``albumtypes``, ``catalognums`` and - ``languages``. -- The ``catalognum`` property now refers to additional file tags named - ``CATALOGID`` and ``DISCOGS_CATALOG`` (but only for reading, not writing). -- The multi-valued ``albumartists`` property now refers to additional file - tags named ``ALBUM_ARTIST`` and ``ALBUM ARTISTS``. (The latter - is used only for reading.) -- The ``ListMediaField`` class now doesn't concatenate multiple lists if - found. The first available tag is used instead, like with other kinds of - fields. - -v0.9.0 -'''''' - -- Add the properties ``bitrate_mode``, ``encoder_info`` and - ``encoder_settings``. - -v0.8.1 -'''''' - -- Fix a regression in v0.8.0 that caused a crash on Python versions below 3.8. - -v0.8.0 -'''''' - -- MediaFile now requires Python 3.6 or later. -- Added support for Wave (`.wav`) files. - -v0.7.0 -'''''' - -- Mutagen 1.45.0 or later is now required. -- MediaFile can now use file-like objects (instead of just the filesystem, via - filenames). - -v0.6.0 -'''''' - -- Enforce a minimum value for SoundCheck gain values. - -v0.5.0 -'''''' - -- Refactored the distribution to use `Flit`_. - -.. _Flit: https://flit.readthedocs.io/ - -v0.4.0 -'''''' - -- Added a ``barcode`` field. -- Added new tag mappings for ``albumtype`` and ``albumstatus``. - -v0.3.0 -'''''' - -- Fixed tests for compatibility with Mutagen 1.43. -- Fix the MPEG-4 tag mapping for the ``label`` field to use the right - capitalization. - -v0.2.0 -'''''' - -- R128 gain tags are now stored in Q7.8 integer format, as per - `the relevant standard`_. -- Added an ``mb_workid`` field. -- The Python source distribution now includes an ``__init__.py`` file that - makes it easier to run the tests. - -.. _the relevant standard: https://tools.ietf.org/html/rfc7845.html#page-25 - -v0.1.0 -'''''' - -This is the first independent release of MediaFile. -It is now synchronised with the embedded version released with `beets`_ v1.4.8. - -.. _beets: https://beets.io diff --git a/mediafile.py b/mediafile.py index d4e3ded..03df854 100644 --- a/mediafile.py +++ b/mediafile.py @@ -33,20 +33,12 @@ data from the tags. In turn ``MediaField`` uses a number of ``StorageStyle`` strategies to handle format specific logic. """ -import mutagen -import mutagen.id3 -import mutagen.mp3 -import mutagen.mp4 -import mutagen.flac -import mutagen.asf -import mutagen._util import base64 import binascii import codecs import datetime import enum -import filetype import functools import logging import math @@ -55,35 +47,44 @@ import struct import traceback +import filetype +import mutagen +import mutagen._util +import mutagen.asf +import mutagen.flac +import mutagen.id3 +import mutagen.mp3 +import mutagen.mp4 -__version__ = '0.13.0' -__all__ = ['UnreadableFileError', 'FileTypeError', 'MediaFile'] +__version__ = "0.13.0" +__all__ = ["UnreadableFileError", "FileTypeError", "MediaFile"] log = logging.getLogger(__name__) # Human-readable type names. TYPES = { - 'mp3': 'MP3', - 'aac': 'AAC', - 'alac': 'ALAC', - 'ogg': 'OGG', - 'opus': 'Opus', - 'flac': 'FLAC', - 'ape': 'APE', - 'wv': 'WavPack', - 'mpc': 'Musepack', - 'asf': 'Windows Media', - 'aiff': 'AIFF', - 'dsf': 'DSD Stream File', - 'wav': 'WAVE', + "mp3": "MP3", + "aac": "AAC", + "alac": "ALAC", + "ogg": "OGG", + "opus": "Opus", + "flac": "FLAC", + "ape": "APE", + "wv": "WavPack", + "mpc": "Musepack", + "asf": "Windows Media", + "aiff": "AIFF", + "dsf": "DSD Stream File", + "wav": "WAVE", } # Exceptions. + class UnreadableFileError(Exception): - """Mutagen is not able to extract information from the file. - """ + """Mutagen is not able to extract information from the file.""" + def __init__(self, filename, msg): Exception.__init__(self, msg if msg else repr(filename)) @@ -94,21 +95,20 @@ class FileTypeError(UnreadableFileError): If passed the `mutagen_type` argument this indicates that the mutagen type is not supported by `Mediafile`. """ + def __init__(self, filename, mutagen_type=None): if mutagen_type is None: - msg = u'{0!r}: not in a recognized format'.format(filename) + msg = "{0!r}: not in a recognized format".format(filename) else: - msg = u'{0}: of mutagen type {1}'.format( - repr(filename), mutagen_type - ) + msg = "{0}: of mutagen type {1}".format(repr(filename), mutagen_type) Exception.__init__(self, msg) class MutagenError(UnreadableFileError): - """Raised when Mutagen fails unexpectedly---probably due to a bug. - """ + """Raised when Mutagen fails unexpectedly---probably due to a bug.""" + def __init__(self, filename, mutagen_exc): - msg = u'{0}: {1}'.format(repr(filename), mutagen_exc) + msg = "{0}: {1}".format(repr(filename), mutagen_exc) Exception.__init__(self, msg) @@ -131,7 +131,7 @@ def mutagen_call(action, filename, func, *args, **kwargs): try: return func(*args, **kwargs) except mutagen.MutagenError as exc: - log.debug(u'%s failed: %s', action, str(exc)) + log.debug("%s failed: %s", action, str(exc)) raise UnreadableFileError(filename, str(exc)) except UnreadableFileError: # Reraise our errors without changes. @@ -139,8 +139,8 @@ def mutagen_call(action, filename, func, *args, **kwargs): raise except Exception as exc: # Isolate bugs in Mutagen. - log.debug(u'%s', traceback.format_exc()) - log.error(u'uncaught Mutagen exception in %s: %s', action, exc) + log.debug("%s", traceback.format_exc()) + log.error("uncaught Mutagen exception in %s: %s", action, exc) raise MutagenError(filename, exc) @@ -152,18 +152,22 @@ def loadfile(method=True, writable=False, create=False): decorated function. Should be used as a decorator for functions using a `filething` parameter. """ + def decorator(func): f = mutagen._util.loadfile(method, writable, create)(func) @functools.wraps(func) def wrapper(*args, **kwargs): - return mutagen_call('loadfile', '', f, *args, **kwargs) + return mutagen_call("loadfile", "", f, *args, **kwargs) + return wrapper + return decorator # Utility. + def _update_filething(filething): """Reopen a `filething` if it's a local file. @@ -171,9 +175,7 @@ def _update_filething(filething): filething with a filename is reopened and a new object is returned. """ if filething.filename: - return mutagen._util.FileThing( - None, filething.filename, filething.name - ) + return mutagen._util.FileThing(None, filething.filename, filething.name) else: return filething @@ -196,11 +198,11 @@ def _safe_cast(out_type, val): else: # Process any other type as a string. if isinstance(val, bytes): - val = val.decode('utf-8', 'ignore') + val = val.decode("utf-8", "ignore") elif not isinstance(val, str): val = str(val) # Get a number from the front of the string. - match = re.match(r'[\+-]?[0-9]+', val.strip()) + match = re.match(r"[\+-]?[0-9]+", val.strip()) return int(match.group(0)) if match else 0 elif out_type is bool: @@ -212,7 +214,7 @@ def _safe_cast(out_type, val): elif out_type is str: if isinstance(val, bytes): - return val.decode('utf-8', 'ignore') + return val.decode("utf-8", "ignore") elif isinstance(val, str): return val else: @@ -223,11 +225,10 @@ def _safe_cast(out_type, val): return float(val) else: if isinstance(val, bytes): - val = val.decode('utf-8', 'ignore') + val = val.decode("utf-8", "ignore") else: val = str(val) - match = re.match(r'[\+-]?([0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)', - val.strip()) + match = re.match(r"[\+-]?([0-9]+\.?[0-9]*|[0-9]*\.[0-9]+)", val.strip()) if match: val = match.group(0) if val: @@ -240,6 +241,7 @@ def _safe_cast(out_type, val): # Image coding for ASF/WMA. + def _unpack_asf_image(data): """Unpack image data from a WM/Picture tag. Return a tuple containing the MIME type, the raw image data, a type indicator, and @@ -249,35 +251,34 @@ def _unpack_asf_image(data): of exceptions (out-of-bounds, etc.). We should clean this up sometime so that the failure modes are well-defined. """ - type, size = struct.unpack_from(' 0: comment = frame.value[0:text_delimiter_index] - comment = comment.decode('utf-8', 'replace') + comment = comment.decode("utf-8", "replace") else: comment = None - image_data = frame.value[text_delimiter_index + 1:] - images.append(Image(data=image_data, type=cover_type, - desc=comment)) + image_data = frame.value[text_delimiter_index + 1 :] + images.append(Image(data=image_data, type=cover_type, desc=comment)) except KeyError: pass @@ -1202,14 +1202,13 @@ def set_list(self, mutagen_file, values): for image in values: image_type = image.type or ImageType.other - comment = image.desc or '' - image_data = comment.encode('utf-8') + b'\x00' + image.data + comment = image.desc or "" + image_data = comment.encode("utf-8") + b"\x00" + image.data cover_tag = self.TAG_NAMES[image_type] mutagen_file[cover_tag] = image_data def delete(self, mutagen_file): - """Remove all images from the file. - """ + """Remove all images from the file.""" for cover_tag in self.TAG_NAMES.values(): try: del mutagen_file[cover_tag] @@ -1221,10 +1220,12 @@ def delete(self, mutagen_file): # aggregates several StorageStyles describing how to access the data for # each file type. + class MediaField(object): """A descriptor providing access to a particular (abstract) metadata field. """ + def __init__(self, *styles, **kwargs): """Creates a new MediaField. @@ -1237,7 +1238,7 @@ def __init__(self, *styles, **kwargs): getting this property. """ - self.out_type = kwargs.get('out_type', str) + self.out_type = kwargs.get("out_type", str) self._styles = styles def styles(self, mutagen_file): @@ -1278,7 +1279,7 @@ def _none_value(self): elif self.out_type is bool: return False elif self.out_type is str: - return u'' + return "" class ListMediaField(MediaField): @@ -1288,6 +1289,7 @@ class ListMediaField(MediaField): Uses ``get_list`` and set_list`` methods of its ``StorageStyle`` strategies to do the actual work. """ + def __get__(self, mediafile, _=None): for style in self.styles(mediafile.mgfile): values = style.get_list(mediafile.mgfile) @@ -1304,7 +1306,7 @@ def single_field(self): """Returns a ``MediaField`` descriptor that gets and sets the first item. """ - options = {'out_type': self.out_type} + options = {"out_type": self.out_type} return MediaField(*self._styles, **options) @@ -1317,6 +1319,7 @@ class DateField(MediaField): For granular access to year, month, and day, use the ``*_field`` methods to create corresponding `DateItemField`s. """ + def __init__(self, *date_styles, **kwargs): """``date_styles`` is a list of ``StorageStyle``s to store and retrieve the whole date from. The ``year`` option is an @@ -1325,7 +1328,7 @@ def __init__(self, *date_styles, **kwargs): storage styles do not return a value. """ super(DateField, self).__init__(*date_styles) - year_style = kwargs.get('year', None) + year_style = kwargs.get("year", None) if year_style: self._year_field = MediaField(*year_style) @@ -1334,11 +1337,7 @@ def __get__(self, mediafile, owner=None): if not year: return None try: - return datetime.date( - year, - month or 1, - day or 1 - ) + return datetime.date(year, month or 1, day or 1) except ValueError: # Out of range values. return None @@ -1350,7 +1349,7 @@ def __set__(self, mediafile, date): def __delete__(self, mediafile): super(DateField, self).__delete__(mediafile) - if hasattr(self, '_year_field'): + if hasattr(self, "_year_field"): self._year_field.__delete__(mediafile) def _get_date_tuple(self, mediafile): @@ -1361,8 +1360,8 @@ def _get_date_tuple(self, mediafile): # Get the underlying data and split on hyphens and slashes. datestring = super(DateField, self).__get__(mediafile, None) if isinstance(datestring, str): - datestring = re.sub(r'[Tt ].*$', '', str(datestring)) - items = re.split('[-/]', str(datestring)) + datestring = re.sub(r"[Tt ].*$", "", str(datestring)) + items = re.split("[-/]", str(datestring)) else: items = [] @@ -1373,7 +1372,7 @@ def _get_date_tuple(self, mediafile): items += [None] * (3 - len(items)) # Use year field if year is missing. - if not items[0] and hasattr(self, '_year_field'): + if not items[0] and hasattr(self, "_year_field"): items[0] = self._year_field.__get__(mediafile) # Convert each component to an integer if possible. @@ -1394,15 +1393,15 @@ def _set_date_tuple(self, mediafile, year, month=None, day=None): self.__delete__(mediafile) return - date = [u'{0:04d}'.format(int(year))] + date = ["{0:04d}".format(int(year))] if month: - date.append(u'{0:02d}'.format(int(month))) + date.append("{0:02d}".format(int(month))) if month and day: - date.append(u'{0:02d}'.format(int(day))) + date.append("{0:02d}".format(int(day))) date = map(str, date) - super(DateField, self).__set__(mediafile, u'-'.join(date)) + super(DateField, self).__set__(mediafile, "-".join(date)) - if hasattr(self, '_year_field'): + if hasattr(self, "_year_field"): self._year_field.__set__(mediafile, year) def year_field(self): @@ -1419,6 +1418,7 @@ class DateItemField(MediaField): """Descriptor that gets and sets constituent parts of a `DateField`: the month, day, or year. """ + def __init__(self, date_field, item_pos): self.date_field = date_field self.item_pos = item_pos @@ -1443,6 +1443,7 @@ class CoverArtField(MediaField): When there are multiple images we try to pick the most likely to be a front cover. """ + def __init__(self): pass @@ -1469,7 +1470,7 @@ def __set__(self, mediafile, data): mediafile.images = [] def __delete__(self, mediafile): - delattr(mediafile, 'images') + delattr(mediafile, "images") class QNumberField(MediaField): @@ -1479,6 +1480,7 @@ class QNumberField(MediaField): `fraction_bits` binary digits to the left and then rounded, yielding a simple integer. """ + def __init__(self, fraction_bits, *args, **kwargs): super(QNumberField, self).__init__(out_type=int, *args, **kwargs) self.__fraction_bits = fraction_bits @@ -1502,6 +1504,7 @@ class ImageListField(ListMediaField): the tags. The setter accepts a list of `Image` instances to be written to the tags. """ + def __init__(self): # The storage styles used here must implement the # `ListStorageStyle` interface and get and set lists of @@ -1519,10 +1522,12 @@ def __init__(self): # MediaFile is a collection of fields. + class MediaFile(object): """Represents a multimedia file on disk and provides access to its metadata. """ + @loadfile() def __init__(self, filething, id3v23=False): """Constructs a new `MediaFile` reflecting the provided file. @@ -1537,41 +1542,39 @@ def __init__(self, filething, id3v23=False): """ self.filething = filething - self.mgfile = mutagen_call( - 'open', self.filename, mutagen.File, filething - ) + self.mgfile = mutagen_call("open", self.filename, mutagen.File, filething) if self.mgfile is None: # Mutagen couldn't guess the type raise FileTypeError(self.filename) - elif type(self.mgfile).__name__ in ['M4A', 'MP4']: + elif type(self.mgfile).__name__ in ["M4A", "MP4"]: info = self.mgfile.info - if info.codec and info.codec.startswith('alac'): - self.type = 'alac' + if info.codec and info.codec.startswith("alac"): + self.type = "alac" else: - self.type = 'aac' - elif type(self.mgfile).__name__ in ['ID3', 'MP3']: - self.type = 'mp3' - elif type(self.mgfile).__name__ == 'FLAC': - self.type = 'flac' - elif type(self.mgfile).__name__ == 'OggOpus': - self.type = 'opus' - elif type(self.mgfile).__name__ == 'OggVorbis': - self.type = 'ogg' - elif type(self.mgfile).__name__ == 'MonkeysAudio': - self.type = 'ape' - elif type(self.mgfile).__name__ == 'WavPack': - self.type = 'wv' - elif type(self.mgfile).__name__ == 'Musepack': - self.type = 'mpc' - elif type(self.mgfile).__name__ == 'ASF': - self.type = 'asf' - elif type(self.mgfile).__name__ == 'AIFF': - self.type = 'aiff' - elif type(self.mgfile).__name__ == 'DSF': - self.type = 'dsf' - elif type(self.mgfile).__name__ == 'WAVE': - self.type = 'wav' + self.type = "aac" + elif type(self.mgfile).__name__ in ["ID3", "MP3"]: + self.type = "mp3" + elif type(self.mgfile).__name__ == "FLAC": + self.type = "flac" + elif type(self.mgfile).__name__ == "OggOpus": + self.type = "opus" + elif type(self.mgfile).__name__ == "OggVorbis": + self.type = "ogg" + elif type(self.mgfile).__name__ == "MonkeysAudio": + self.type = "ape" + elif type(self.mgfile).__name__ == "WavPack": + self.type = "wv" + elif type(self.mgfile).__name__ == "Musepack": + self.type = "mpc" + elif type(self.mgfile).__name__ == "ASF": + self.type = "asf" + elif type(self.mgfile).__name__ == "AIFF": + self.type = "aiff" + elif type(self.mgfile).__name__ == "DSF": + self.type = "dsf" + elif type(self.mgfile).__name__ == "WAVE": + self.type = "wav" else: raise FileTypeError(self.filename, type(self.mgfile).__name__) @@ -1580,7 +1583,7 @@ def __init__(self, filething, id3v23=False): self.mgfile.add_tags() # Set the ID3v2.3 flag only for MP3s. - self.id3v23 = id3v23 and self.type == 'mp3' + self.id3v23 = id3v23 and self.type == "mp3" @property def filename(self): @@ -1609,11 +1612,10 @@ def path(self): @property def filesize(self): - """The size (in bytes) of the underlying file. - """ + """The size (in bytes) of the underlying file.""" if self.filething.filename: return os.path.getsize(self.filething.filename) - if hasattr(self.filething.fileobj, '__len__'): + if hasattr(self.filething.fileobj, "__len__"): return len(self.filething.fileobj) else: tell = self.filething.fileobj.tell() @@ -1630,21 +1632,30 @@ def save(self, **kwargs): # Possibly save the tags to ID3v2.3. if self.id3v23: id3 = self.mgfile - if hasattr(id3, 'tags'): + if hasattr(id3, "tags"): # In case this is an MP3 object, not an ID3 object. id3 = id3.tags id3.update_to_v23() - kwargs['v2_version'] = 3 - - mutagen_call('save', self.filename, self.mgfile.save, - _update_filething(self.filething), **kwargs) + kwargs["v2_version"] = 3 + + mutagen_call( + "save", + self.filename, + self.mgfile.save, + _update_filething(self.filething), + **kwargs, + ) def delete(self): """Remove the current metadata tag from the file. May throw `UnreadableFileError`. """ - mutagen_call('delete', self.filename, self.mgfile.delete, - _update_filething(self.filething)) + mutagen_call( + "delete", + self.filename, + self.mgfile.delete, + _update_filething(self.filething), + ) # Convenient access to the set of available fields. @@ -1659,7 +1670,7 @@ def fields(cls): if isinstance(property, bytes): # On Python 2, class field names are bytes. This method # produces text strings. - yield property.decode('utf8', 'ignore') + yield property.decode("utf8", "ignore") else: yield property @@ -1674,9 +1685,9 @@ def _field_sort_name(cls, name): make them appear in that order. """ if isinstance(cls.__dict__[name], DateItemField): - name = re.sub('year', 'date0', name) - name = re.sub('month', 'date1', name) - name = re.sub('day', 'date2', name) + name = re.sub("year", "date0", name) + name = re.sub("month", "date1", name) + name = re.sub("day", "date2", name) return name @classmethod @@ -1698,9 +1709,17 @@ def readable_fields(cls): """ for property in cls.fields(): yield property - for property in ('length', 'samplerate', 'bitdepth', 'bitrate', - 'bitrate_mode', 'channels', 'encoder_info', - 'encoder_settings', 'format'): + for property in ( + "length", + "samplerate", + "bitdepth", + "bitrate", + "bitrate_mode", + "channels", + "encoder_info", + "encoder_settings", + "format", + ): yield property @classmethod @@ -1713,11 +1732,9 @@ def add_field(cls, name, descriptor): :param descriptor: an instance of :class:`MediaField`. """ if not isinstance(descriptor, MediaField): - raise ValueError( - u'{0} must be an instance of MediaField'.format(descriptor)) + raise ValueError("{0} must be an instance of MediaField".format(descriptor)) if name in cls.__dict__: - raise ValueError( - u'property "{0}" already exists on MediaFile'.format(name)) + raise ValueError('property "{0}" already exists on MediaFile'.format(name)) setattr(cls, name, descriptor) def update(self, dict): @@ -1745,304 +1762,298 @@ def as_dict(self): # Field definitions. title = MediaField( - MP3StorageStyle('TIT2'), - MP4StorageStyle('\xa9nam'), - StorageStyle('TITLE'), - ASFStorageStyle('Title'), + MP3StorageStyle("TIT2"), + MP4StorageStyle("\xa9nam"), + StorageStyle("TITLE"), + ASFStorageStyle("Title"), ) artist = MediaField( - MP3StorageStyle('TPE1'), - MP4StorageStyle('\xa9ART'), - StorageStyle('ARTIST'), - ASFStorageStyle('Author'), + MP3StorageStyle("TPE1"), + MP4StorageStyle("\xa9ART"), + StorageStyle("ARTIST"), + ASFStorageStyle("Author"), ) artists = ListMediaField( - MP3ListDescStorageStyle(desc=u'ARTISTS'), - MP4ListStorageStyle('----:com.apple.iTunes:ARTISTS'), - ListStorageStyle('ARTISTS'), - ASFStorageStyle('WM/ARTISTS'), + MP3ListDescStorageStyle(desc="ARTISTS"), + MP4ListStorageStyle("----:com.apple.iTunes:ARTISTS"), + ListStorageStyle("ARTISTS"), + ASFStorageStyle("WM/ARTISTS"), ) album = MediaField( - MP3StorageStyle('TALB'), - MP4StorageStyle('\xa9alb'), - StorageStyle('ALBUM'), - ASFStorageStyle('WM/AlbumTitle'), + MP3StorageStyle("TALB"), + MP4StorageStyle("\xa9alb"), + StorageStyle("ALBUM"), + ASFStorageStyle("WM/AlbumTitle"), ) genres = ListMediaField( - MP3ListStorageStyle('TCON'), - MP4ListStorageStyle('\xa9gen'), - ListStorageStyle('GENRE'), - ASFStorageStyle('WM/Genre'), + MP3ListStorageStyle("TCON"), + MP4ListStorageStyle("\xa9gen"), + ListStorageStyle("GENRE"), + ASFStorageStyle("WM/Genre"), ) genre = genres.single_field() lyricist = MediaField( - MP3StorageStyle('TEXT'), - MP4StorageStyle('----:com.apple.iTunes:LYRICIST'), - StorageStyle('LYRICIST'), - ASFStorageStyle('WM/Writer'), + MP3StorageStyle("TEXT"), + MP4StorageStyle("----:com.apple.iTunes:LYRICIST"), + StorageStyle("LYRICIST"), + ASFStorageStyle("WM/Writer"), ) composer = MediaField( - MP3StorageStyle('TCOM'), - MP4StorageStyle('\xa9wrt'), - StorageStyle('COMPOSER'), - ASFStorageStyle('WM/Composer'), + MP3StorageStyle("TCOM"), + MP4StorageStyle("\xa9wrt"), + StorageStyle("COMPOSER"), + ASFStorageStyle("WM/Composer"), ) composer_sort = MediaField( - MP3StorageStyle('TSOC'), - MP4StorageStyle('soco'), - StorageStyle('COMPOSERSORT'), - ASFStorageStyle('WM/Composersortorder'), + MP3StorageStyle("TSOC"), + MP4StorageStyle("soco"), + StorageStyle("COMPOSERSORT"), + ASFStorageStyle("WM/Composersortorder"), ) arranger = MediaField( - MP3PeopleStorageStyle('TIPL', involvement='arranger'), - MP4StorageStyle('----:com.apple.iTunes:Arranger'), - StorageStyle('ARRANGER'), - ASFStorageStyle('beets/Arranger'), + MP3PeopleStorageStyle("TIPL", involvement="arranger"), + MP4StorageStyle("----:com.apple.iTunes:Arranger"), + StorageStyle("ARRANGER"), + ASFStorageStyle("beets/Arranger"), ) grouping = MediaField( - MP3StorageStyle('TIT1'), - MP4StorageStyle('\xa9grp'), - StorageStyle('GROUPING'), - ASFStorageStyle('WM/ContentGroupDescription'), + MP3StorageStyle("TIT1"), + MP4StorageStyle("\xa9grp"), + StorageStyle("GROUPING"), + ASFStorageStyle("WM/ContentGroupDescription"), ) subtitle = MediaField( - MP3StorageStyle('TIT3'), - StorageStyle('SUBTITLE'), - ASFStorageStyle('Subtitle'), + MP3StorageStyle("TIT3"), + StorageStyle("SUBTITLE"), + ASFStorageStyle("Subtitle"), ) track = MediaField( - MP3SlashPackStorageStyle('TRCK', pack_pos=0), - MP4TupleStorageStyle('trkn', index=0), - StorageStyle('TRACK'), - StorageStyle('TRACKNUMBER'), - ASFStorageStyle('WM/TrackNumber'), + MP3SlashPackStorageStyle("TRCK", pack_pos=0), + MP4TupleStorageStyle("trkn", index=0), + StorageStyle("TRACK"), + StorageStyle("TRACKNUMBER"), + ASFStorageStyle("WM/TrackNumber"), out_type=int, ) tracktotal = MediaField( - MP3SlashPackStorageStyle('TRCK', pack_pos=1), - MP4TupleStorageStyle('trkn', index=1), - StorageStyle('TRACKTOTAL'), - StorageStyle('TRACKC'), - StorageStyle('TOTALTRACKS'), - ASFStorageStyle('TotalTracks'), + MP3SlashPackStorageStyle("TRCK", pack_pos=1), + MP4TupleStorageStyle("trkn", index=1), + StorageStyle("TRACKTOTAL"), + StorageStyle("TRACKC"), + StorageStyle("TOTALTRACKS"), + ASFStorageStyle("TotalTracks"), out_type=int, ) disc = MediaField( - MP3SlashPackStorageStyle('TPOS', pack_pos=0), - MP4TupleStorageStyle('disk', index=0), - StorageStyle('DISC'), - StorageStyle('DISCNUMBER'), - ASFStorageStyle('WM/PartOfSet'), + MP3SlashPackStorageStyle("TPOS", pack_pos=0), + MP4TupleStorageStyle("disk", index=0), + StorageStyle("DISC"), + StorageStyle("DISCNUMBER"), + ASFStorageStyle("WM/PartOfSet"), out_type=int, ) disctotal = MediaField( - MP3SlashPackStorageStyle('TPOS', pack_pos=1), - MP4TupleStorageStyle('disk', index=1), - StorageStyle('DISCTOTAL'), - StorageStyle('DISCC'), - StorageStyle('TOTALDISCS'), - ASFStorageStyle('TotalDiscs'), + MP3SlashPackStorageStyle("TPOS", pack_pos=1), + MP4TupleStorageStyle("disk", index=1), + StorageStyle("DISCTOTAL"), + StorageStyle("DISCC"), + StorageStyle("TOTALDISCS"), + ASFStorageStyle("TotalDiscs"), out_type=int, ) url = MediaField( - MP3DescStorageStyle(key='WXXX', attr='url', multispec=False), - MP4StorageStyle('\xa9url'), - StorageStyle('URL'), - ASFStorageStyle('WM/URL'), + MP3DescStorageStyle(key="WXXX", attr="url", multispec=False), + MP4StorageStyle("\xa9url"), + StorageStyle("URL"), + ASFStorageStyle("WM/URL"), ) lyrics = MediaField( - MP3DescStorageStyle(key='USLT', multispec=False), - MP4StorageStyle('\xa9lyr'), - StorageStyle('LYRICS'), - ASFStorageStyle('WM/Lyrics'), + MP3DescStorageStyle(key="USLT", multispec=False), + MP4StorageStyle("\xa9lyr"), + StorageStyle("LYRICS"), + ASFStorageStyle("WM/Lyrics"), ) comments = MediaField( - MP3DescStorageStyle(key='COMM'), - MP4StorageStyle('\xa9cmt'), - StorageStyle('DESCRIPTION'), - StorageStyle('COMMENT'), - ASFStorageStyle('WM/Comments'), - ASFStorageStyle('Description') + MP3DescStorageStyle(key="COMM"), + MP4StorageStyle("\xa9cmt"), + StorageStyle("DESCRIPTION"), + StorageStyle("COMMENT"), + ASFStorageStyle("WM/Comments"), + ASFStorageStyle("Description"), ) copyright = MediaField( - MP3StorageStyle('TCOP'), - MP4StorageStyle('cprt'), - StorageStyle('COPYRIGHT'), - ASFStorageStyle('Copyright'), + MP3StorageStyle("TCOP"), + MP4StorageStyle("cprt"), + StorageStyle("COPYRIGHT"), + ASFStorageStyle("Copyright"), ) bpm = MediaField( - MP3StorageStyle('TBPM'), - MP4StorageStyle('tmpo', as_type=int), - StorageStyle('BPM'), - ASFStorageStyle('WM/BeatsPerMinute'), + MP3StorageStyle("TBPM"), + MP4StorageStyle("tmpo", as_type=int), + StorageStyle("BPM"), + ASFStorageStyle("WM/BeatsPerMinute"), out_type=int, ) comp = MediaField( - MP3StorageStyle('TCMP'), - MP4BoolStorageStyle('cpil'), - StorageStyle('COMPILATION'), - ASFStorageStyle('WM/IsCompilation', as_type=bool), + MP3StorageStyle("TCMP"), + MP4BoolStorageStyle("cpil"), + StorageStyle("COMPILATION"), + ASFStorageStyle("WM/IsCompilation", as_type=bool), out_type=bool, ) albumartist = MediaField( - MP3StorageStyle('TPE2'), - MP4StorageStyle('aART'), - StorageStyle('ALBUM ARTIST'), - StorageStyle('ALBUM_ARTIST'), - StorageStyle('ALBUMARTIST'), - ASFStorageStyle('WM/AlbumArtist'), + MP3StorageStyle("TPE2"), + MP4StorageStyle("aART"), + StorageStyle("ALBUM ARTIST"), + StorageStyle("ALBUM_ARTIST"), + StorageStyle("ALBUMARTIST"), + ASFStorageStyle("WM/AlbumArtist"), ) albumartists = ListMediaField( - MP3ListDescStorageStyle(desc=u'ALBUMARTISTS'), - MP3ListDescStorageStyle(desc=u'ALBUM_ARTISTS'), - MP3ListDescStorageStyle(desc=u'ALBUM ARTISTS', read_only=True), - MP4ListStorageStyle('----:com.apple.iTunes:ALBUMARTISTS'), - MP4ListStorageStyle('----:com.apple.iTunes:ALBUM_ARTISTS'), - MP4ListStorageStyle( - '----:com.apple.iTunes:ALBUM ARTISTS', read_only=True - ), - ListStorageStyle('ALBUMARTISTS'), - ListStorageStyle('ALBUM_ARTISTS'), - ListStorageStyle('ALBUM ARTISTS', read_only=True), - ASFStorageStyle('WM/AlbumArtists'), + MP3ListDescStorageStyle(desc="ALBUMARTISTS"), + MP3ListDescStorageStyle(desc="ALBUM_ARTISTS"), + MP3ListDescStorageStyle(desc="ALBUM ARTISTS", read_only=True), + MP4ListStorageStyle("----:com.apple.iTunes:ALBUMARTISTS"), + MP4ListStorageStyle("----:com.apple.iTunes:ALBUM_ARTISTS"), + MP4ListStorageStyle("----:com.apple.iTunes:ALBUM ARTISTS", read_only=True), + ListStorageStyle("ALBUMARTISTS"), + ListStorageStyle("ALBUM_ARTISTS"), + ListStorageStyle("ALBUM ARTISTS", read_only=True), + ASFStorageStyle("WM/AlbumArtists"), ) albumtypes = ListMediaField( - MP3ListDescStorageStyle('MusicBrainz Album Type', split_v23=True), - MP4ListStorageStyle('----:com.apple.iTunes:MusicBrainz Album Type'), - ListStorageStyle('RELEASETYPE'), - ListStorageStyle('MUSICBRAINZ_ALBUMTYPE'), - ASFStorageStyle('MusicBrainz/Album Type'), + MP3ListDescStorageStyle("MusicBrainz Album Type", split_v23=True), + MP4ListStorageStyle("----:com.apple.iTunes:MusicBrainz Album Type"), + ListStorageStyle("RELEASETYPE"), + ListStorageStyle("MUSICBRAINZ_ALBUMTYPE"), + ASFStorageStyle("MusicBrainz/Album Type"), ) albumtype = albumtypes.single_field() label = MediaField( - MP3StorageStyle('TPUB'), - MP4StorageStyle('----:com.apple.iTunes:LABEL'), - MP4StorageStyle('----:com.apple.iTunes:publisher'), - MP4StorageStyle('----:com.apple.iTunes:Label', read_only=True), - StorageStyle('LABEL'), - StorageStyle('PUBLISHER'), # Traktor - ASFStorageStyle('WM/Publisher'), + MP3StorageStyle("TPUB"), + MP4StorageStyle("----:com.apple.iTunes:LABEL"), + MP4StorageStyle("----:com.apple.iTunes:publisher"), + MP4StorageStyle("----:com.apple.iTunes:Label", read_only=True), + StorageStyle("LABEL"), + StorageStyle("PUBLISHER"), # Traktor + ASFStorageStyle("WM/Publisher"), ) artist_sort = MediaField( - MP3StorageStyle('TSOP'), - MP4StorageStyle('soar'), - StorageStyle('ARTISTSORT'), - ASFStorageStyle('WM/ArtistSortOrder'), + MP3StorageStyle("TSOP"), + MP4StorageStyle("soar"), + StorageStyle("ARTISTSORT"), + ASFStorageStyle("WM/ArtistSortOrder"), ) albumartist_sort = MediaField( - MP3DescStorageStyle(u'ALBUMARTISTSORT'), - MP4StorageStyle('soaa'), - StorageStyle('ALBUMARTISTSORT'), - ASFStorageStyle('WM/AlbumArtistSortOrder'), + MP3DescStorageStyle("ALBUMARTISTSORT"), + MP4StorageStyle("soaa"), + StorageStyle("ALBUMARTISTSORT"), + ASFStorageStyle("WM/AlbumArtistSortOrder"), ) asin = MediaField( - MP3DescStorageStyle(u'ASIN'), - MP4StorageStyle('----:com.apple.iTunes:ASIN'), - StorageStyle('ASIN'), - ASFStorageStyle('MusicBrainz/ASIN'), + MP3DescStorageStyle("ASIN"), + MP4StorageStyle("----:com.apple.iTunes:ASIN"), + StorageStyle("ASIN"), + ASFStorageStyle("MusicBrainz/ASIN"), ) catalognums = ListMediaField( - MP3ListDescStorageStyle('CATALOGNUMBER', split_v23=True), - MP3ListDescStorageStyle('CATALOGID', read_only=True), - MP3ListDescStorageStyle('DISCOGS_CATALOG', read_only=True), - MP4ListStorageStyle('----:com.apple.iTunes:CATALOGNUMBER'), - MP4ListStorageStyle( - '----:com.apple.iTunes:CATALOGID', read_only=True - ), - MP4ListStorageStyle( - '----:com.apple.iTunes:DISCOGS_CATALOG', read_only=True - ), - ListStorageStyle('CATALOGNUMBER'), - ListStorageStyle('CATALOGID', read_only=True), - ListStorageStyle('DISCOGS_CATALOG', read_only=True), - ASFStorageStyle('WM/CatalogNo'), - ASFStorageStyle('CATALOGID', read_only=True), - ASFStorageStyle('DISCOGS_CATALOG', read_only=True), + MP3ListDescStorageStyle("CATALOGNUMBER", split_v23=True), + MP3ListDescStorageStyle("CATALOGID", read_only=True), + MP3ListDescStorageStyle("DISCOGS_CATALOG", read_only=True), + MP4ListStorageStyle("----:com.apple.iTunes:CATALOGNUMBER"), + MP4ListStorageStyle("----:com.apple.iTunes:CATALOGID", read_only=True), + MP4ListStorageStyle("----:com.apple.iTunes:DISCOGS_CATALOG", read_only=True), + ListStorageStyle("CATALOGNUMBER"), + ListStorageStyle("CATALOGID", read_only=True), + ListStorageStyle("DISCOGS_CATALOG", read_only=True), + ASFStorageStyle("WM/CatalogNo"), + ASFStorageStyle("CATALOGID", read_only=True), + ASFStorageStyle("DISCOGS_CATALOG", read_only=True), ) catalognum = catalognums.single_field() barcode = MediaField( - MP3DescStorageStyle(u'BARCODE'), - MP4StorageStyle('----:com.apple.iTunes:BARCODE'), - StorageStyle('BARCODE'), - StorageStyle('UPC', read_only=True), - StorageStyle('EAN/UPN', read_only=True), - StorageStyle('EAN', read_only=True), - StorageStyle('UPN', read_only=True), - ASFStorageStyle('WM/Barcode'), + MP3DescStorageStyle("BARCODE"), + MP4StorageStyle("----:com.apple.iTunes:BARCODE"), + StorageStyle("BARCODE"), + StorageStyle("UPC", read_only=True), + StorageStyle("EAN/UPN", read_only=True), + StorageStyle("EAN", read_only=True), + StorageStyle("UPN", read_only=True), + ASFStorageStyle("WM/Barcode"), ) isrc = MediaField( - MP3StorageStyle(u'TSRC'), - MP4StorageStyle('----:com.apple.iTunes:ISRC'), - StorageStyle('ISRC'), - ASFStorageStyle('WM/ISRC'), + MP3StorageStyle("TSRC"), + MP4StorageStyle("----:com.apple.iTunes:ISRC"), + StorageStyle("ISRC"), + ASFStorageStyle("WM/ISRC"), ) disctitle = MediaField( - MP3StorageStyle('TSST'), - MP4StorageStyle('----:com.apple.iTunes:DISCSUBTITLE'), - StorageStyle('DISCSUBTITLE'), - ASFStorageStyle('WM/SetSubTitle'), + MP3StorageStyle("TSST"), + MP4StorageStyle("----:com.apple.iTunes:DISCSUBTITLE"), + StorageStyle("DISCSUBTITLE"), + ASFStorageStyle("WM/SetSubTitle"), ) encoder = MediaField( - MP3StorageStyle('TENC'), - MP4StorageStyle('\xa9too'), - StorageStyle('ENCODEDBY'), - StorageStyle('ENCODER'), - ASFStorageStyle('WM/EncodedBy'), + MP3StorageStyle("TENC"), + MP4StorageStyle("\xa9too"), + StorageStyle("ENCODEDBY"), + StorageStyle("ENCODER"), + ASFStorageStyle("WM/EncodedBy"), ) script = MediaField( - MP3DescStorageStyle(u'Script'), - MP4StorageStyle('----:com.apple.iTunes:SCRIPT'), - StorageStyle('SCRIPT'), - ASFStorageStyle('WM/Script'), + MP3DescStorageStyle("Script"), + MP4StorageStyle("----:com.apple.iTunes:SCRIPT"), + StorageStyle("SCRIPT"), + ASFStorageStyle("WM/Script"), ) languages = ListMediaField( - MP3ListStorageStyle('TLAN'), - MP4ListStorageStyle('----:com.apple.iTunes:LANGUAGE'), - ListStorageStyle('LANGUAGE'), - ASFStorageStyle('WM/Language'), + MP3ListStorageStyle("TLAN"), + MP4ListStorageStyle("----:com.apple.iTunes:LANGUAGE"), + ListStorageStyle("LANGUAGE"), + ASFStorageStyle("WM/Language"), ) language = languages.single_field() country = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Release Country'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz ' - 'Album Release Country'), - StorageStyle('RELEASECOUNTRY'), - ASFStorageStyle('MusicBrainz/Album Release Country'), + MP3DescStorageStyle("MusicBrainz Album Release Country"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album Release Country"), + StorageStyle("RELEASECOUNTRY"), + ASFStorageStyle("MusicBrainz/Album Release Country"), ) albumstatus = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Status'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Status'), - StorageStyle('RELEASESTATUS'), - StorageStyle('MUSICBRAINZ_ALBUMSTATUS'), - ASFStorageStyle('MusicBrainz/Album Status'), + MP3DescStorageStyle("MusicBrainz Album Status"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album Status"), + StorageStyle("RELEASESTATUS"), + StorageStyle("MUSICBRAINZ_ALBUMSTATUS"), + ASFStorageStyle("MusicBrainz/Album Status"), ) media = MediaField( - MP3StorageStyle('TMED'), - MP4StorageStyle('----:com.apple.iTunes:MEDIA'), - StorageStyle('MEDIA'), - ASFStorageStyle('WM/Media'), + MP3StorageStyle("TMED"), + MP4StorageStyle("----:com.apple.iTunes:MEDIA"), + StorageStyle("MEDIA"), + ASFStorageStyle("WM/Media"), ) albumdisambig = MediaField( # This tag mapping was invented for beets (not used by Picard, etc). - MP3DescStorageStyle(u'MusicBrainz Album Comment'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Comment'), - StorageStyle('MUSICBRAINZ_ALBUMCOMMENT'), - ASFStorageStyle('MusicBrainz/Album Comment'), + MP3DescStorageStyle("MusicBrainz Album Comment"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album Comment"), + StorageStyle("MUSICBRAINZ_ALBUMCOMMENT"), + ASFStorageStyle("MusicBrainz/Album Comment"), ) # Release date. date = DateField( - MP3StorageStyle('TDRC'), - MP4StorageStyle('\xa9day'), - StorageStyle('DATE'), - ASFStorageStyle('WM/Year'), - year=(StorageStyle('YEAR'),)) + MP3StorageStyle("TDRC"), + MP4StorageStyle("\xa9day"), + StorageStyle("DATE"), + ASFStorageStyle("WM/Year"), + year=(StorageStyle("YEAR"),), + ) year = date.year_field() month = date.month_field() @@ -2050,11 +2061,12 @@ def as_dict(self): # *Original* release date. original_date = DateField( - MP3StorageStyle('TDOR'), - MP4StorageStyle('----:com.apple.iTunes:ORIGINAL YEAR'), - MP4StorageStyle('----:com.apple.iTunes:ORIGINALDATE'), - StorageStyle('ORIGINALDATE'), - ASFStorageStyle('WM/OriginalReleaseYear')) + MP3StorageStyle("TDOR"), + MP4StorageStyle("----:com.apple.iTunes:ORIGINAL YEAR"), + MP4StorageStyle("----:com.apple.iTunes:ORIGINALDATE"), + StorageStyle("ORIGINALDATE"), + ASFStorageStyle("WM/OriginalReleaseYear"), + ) original_year = original_date.year_field() original_month = original_date.month_field() @@ -2062,40 +2074,40 @@ def as_dict(self): # Nonstandard metadata. artist_credit = MediaField( - MP3DescStorageStyle(u'Artist Credit'), - MP4StorageStyle('----:com.apple.iTunes:Artist Credit'), - StorageStyle('ARTIST_CREDIT'), - ASFStorageStyle('beets/Artist Credit'), + MP3DescStorageStyle("Artist Credit"), + MP4StorageStyle("----:com.apple.iTunes:Artist Credit"), + StorageStyle("ARTIST_CREDIT"), + ASFStorageStyle("beets/Artist Credit"), ) artists_credit = ListMediaField( - MP3ListDescStorageStyle(desc=u'ARTISTS_CREDIT'), - MP4ListStorageStyle('----:com.apple.iTunes:ARTISTS_CREDIT'), - ListStorageStyle('ARTISTS_CREDIT'), - ASFStorageStyle('beets/ArtistsCredit'), + MP3ListDescStorageStyle(desc="ARTISTS_CREDIT"), + MP4ListStorageStyle("----:com.apple.iTunes:ARTISTS_CREDIT"), + ListStorageStyle("ARTISTS_CREDIT"), + ASFStorageStyle("beets/ArtistsCredit"), ) artists_sort = ListMediaField( - MP3ListDescStorageStyle(desc=u'ARTISTS_SORT'), - MP4ListStorageStyle('----:com.apple.iTunes:ARTISTS_SORT'), - ListStorageStyle('ARTISTS_SORT'), - ASFStorageStyle('beets/ArtistsSort'), + MP3ListDescStorageStyle(desc="ARTISTS_SORT"), + MP4ListStorageStyle("----:com.apple.iTunes:ARTISTS_SORT"), + ListStorageStyle("ARTISTS_SORT"), + ASFStorageStyle("beets/ArtistsSort"), ) albumartist_credit = MediaField( - MP3DescStorageStyle(u'Album Artist Credit'), - MP4StorageStyle('----:com.apple.iTunes:Album Artist Credit'), - StorageStyle('ALBUMARTIST_CREDIT'), - ASFStorageStyle('beets/Album Artist Credit'), + MP3DescStorageStyle("Album Artist Credit"), + MP4StorageStyle("----:com.apple.iTunes:Album Artist Credit"), + StorageStyle("ALBUMARTIST_CREDIT"), + ASFStorageStyle("beets/Album Artist Credit"), ) albumartists_credit = ListMediaField( - MP3ListDescStorageStyle(desc=u'ALBUMARTISTS_CREDIT'), - MP4ListStorageStyle('----:com.apple.iTunes:ALBUMARTISTS_CREDIT'), - ListStorageStyle('ALBUMARTISTS_CREDIT'), - ASFStorageStyle('beets/AlbumArtistsCredit'), + MP3ListDescStorageStyle(desc="ALBUMARTISTS_CREDIT"), + MP4ListStorageStyle("----:com.apple.iTunes:ALBUMARTISTS_CREDIT"), + ListStorageStyle("ALBUMARTISTS_CREDIT"), + ASFStorageStyle("beets/AlbumArtistsCredit"), ) albumartists_sort = ListMediaField( - MP3ListDescStorageStyle(desc=u'ALBUMARTISTS_SORT'), - MP4ListStorageStyle('----:com.apple.iTunes:ALBUMARTISTS_SORT'), - ListStorageStyle('ALBUMARTISTS_SORT'), - ASFStorageStyle('beets/AlbumArtistsSort'), + MP3ListDescStorageStyle(desc="ALBUMARTISTS_SORT"), + MP4ListStorageStyle("----:com.apple.iTunes:ALBUMARTISTS_SORT"), + ListStorageStyle("ALBUMARTISTS_SORT"), + ASFStorageStyle("beets/AlbumArtistsSort"), ) # Legacy album art field @@ -2106,208 +2118,134 @@ def as_dict(self): # MusicBrainz IDs. mb_trackid = MediaField( - MP3UFIDStorageStyle(owner='http://musicbrainz.org'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Track Id'), - StorageStyle('MUSICBRAINZ_TRACKID'), - ASFStorageStyle('MusicBrainz/Track Id'), + MP3UFIDStorageStyle(owner="http://musicbrainz.org"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Track Id"), + StorageStyle("MUSICBRAINZ_TRACKID"), + ASFStorageStyle("MusicBrainz/Track Id"), ) mb_releasetrackid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Release Track Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Track Id'), - StorageStyle('MUSICBRAINZ_RELEASETRACKID'), - ASFStorageStyle('MusicBrainz/Release Track Id'), + MP3DescStorageStyle("MusicBrainz Release Track Id"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Release Track Id"), + StorageStyle("MUSICBRAINZ_RELEASETRACKID"), + ASFStorageStyle("MusicBrainz/Release Track Id"), ) mb_workid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Work Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Work Id'), - StorageStyle('MUSICBRAINZ_WORKID'), - ASFStorageStyle('MusicBrainz/Work Id'), + MP3DescStorageStyle("MusicBrainz Work Id"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Work Id"), + StorageStyle("MUSICBRAINZ_WORKID"), + ASFStorageStyle("MusicBrainz/Work Id"), ) mb_albumid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Album Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Album Id'), - StorageStyle('MUSICBRAINZ_ALBUMID'), - ASFStorageStyle('MusicBrainz/Album Id'), + MP3DescStorageStyle("MusicBrainz Album Id"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Album Id"), + StorageStyle("MUSICBRAINZ_ALBUMID"), + ASFStorageStyle("MusicBrainz/Album Id"), ) mb_artistids = ListMediaField( - MP3ListDescStorageStyle(u'MusicBrainz Artist Id', split_v23=True), - MP4ListStorageStyle('----:com.apple.iTunes:MusicBrainz Artist Id'), - ListStorageStyle('MUSICBRAINZ_ARTISTID'), - ASFStorageStyle('MusicBrainz/Artist Id'), + MP3ListDescStorageStyle("MusicBrainz Artist Id", split_v23=True), + MP4ListStorageStyle("----:com.apple.iTunes:MusicBrainz Artist Id"), + ListStorageStyle("MUSICBRAINZ_ARTISTID"), + ASFStorageStyle("MusicBrainz/Artist Id"), ) mb_artistid = mb_artistids.single_field() mb_albumartistids = ListMediaField( MP3ListDescStorageStyle( - u'MusicBrainz Album Artist Id', + "MusicBrainz Album Artist Id", split_v23=True, ), MP4ListStorageStyle( - '----:com.apple.iTunes:MusicBrainz Album Artist Id', + "----:com.apple.iTunes:MusicBrainz Album Artist Id", ), - ListStorageStyle('MUSICBRAINZ_ALBUMARTISTID'), - ASFStorageStyle('MusicBrainz/Album Artist Id'), + ListStorageStyle("MUSICBRAINZ_ALBUMARTISTID"), + ASFStorageStyle("MusicBrainz/Album Artist Id"), ) mb_albumartistid = mb_albumartistids.single_field() mb_releasegroupid = MediaField( - MP3DescStorageStyle(u'MusicBrainz Release Group Id'), - MP4StorageStyle('----:com.apple.iTunes:MusicBrainz Release Group Id'), - StorageStyle('MUSICBRAINZ_RELEASEGROUPID'), - ASFStorageStyle('MusicBrainz/Release Group Id'), + MP3DescStorageStyle("MusicBrainz Release Group Id"), + MP4StorageStyle("----:com.apple.iTunes:MusicBrainz Release Group Id"), + StorageStyle("MUSICBRAINZ_RELEASEGROUPID"), + ASFStorageStyle("MusicBrainz/Release Group Id"), ) # Acoustid fields. acoustid_fingerprint = MediaField( - MP3DescStorageStyle(u'Acoustid Fingerprint'), - MP4StorageStyle('----:com.apple.iTunes:Acoustid Fingerprint'), - StorageStyle('ACOUSTID_FINGERPRINT'), - ASFStorageStyle('Acoustid/Fingerprint'), + MP3DescStorageStyle("Acoustid Fingerprint"), + MP4StorageStyle("----:com.apple.iTunes:Acoustid Fingerprint"), + StorageStyle("ACOUSTID_FINGERPRINT"), + ASFStorageStyle("Acoustid/Fingerprint"), ) acoustid_id = MediaField( - MP3DescStorageStyle(u'Acoustid Id'), - MP4StorageStyle('----:com.apple.iTunes:Acoustid Id'), - StorageStyle('ACOUSTID_ID'), - ASFStorageStyle('Acoustid/Id'), + MP3DescStorageStyle("Acoustid Id"), + MP4StorageStyle("----:com.apple.iTunes:Acoustid Id"), + StorageStyle("ACOUSTID_ID"), + ASFStorageStyle("Acoustid/Id"), ) # ReplayGain fields. rg_track_gain = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_TRACK_GAIN', - float_places=2, suffix=u' dB' - ), - MP3DescStorageStyle( - u'replaygain_track_gain', - float_places=2, suffix=u' dB' - ), - MP3SoundCheckStorageStyle( - key='COMM', - index=0, desc=u'iTunNORM', - id3_lang='eng' - ), + MP3DescStorageStyle("REPLAYGAIN_TRACK_GAIN", float_places=2, suffix=" dB"), + MP3DescStorageStyle("replaygain_track_gain", float_places=2, suffix=" dB"), + MP3SoundCheckStorageStyle(key="COMM", index=0, desc="iTunNORM", id3_lang="eng"), MP4StorageStyle( - '----:com.apple.iTunes:replaygain_track_gain', - float_places=2, suffix=' dB' - ), - MP4SoundCheckStorageStyle( - '----:com.apple.iTunes:iTunNORM', - index=0 - ), - StorageStyle( - u'REPLAYGAIN_TRACK_GAIN', - float_places=2, suffix=u' dB' - ), - ASFStorageStyle( - u'replaygain_track_gain', - float_places=2, suffix=u' dB' + "----:com.apple.iTunes:replaygain_track_gain", float_places=2, suffix=" dB" ), - out_type=float + MP4SoundCheckStorageStyle("----:com.apple.iTunes:iTunNORM", index=0), + StorageStyle("REPLAYGAIN_TRACK_GAIN", float_places=2, suffix=" dB"), + ASFStorageStyle("replaygain_track_gain", float_places=2, suffix=" dB"), + out_type=float, ) rg_album_gain = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_ALBUM_GAIN', - float_places=2, suffix=u' dB' - ), - MP3DescStorageStyle( - u'replaygain_album_gain', - float_places=2, suffix=u' dB' - ), + MP3DescStorageStyle("REPLAYGAIN_ALBUM_GAIN", float_places=2, suffix=" dB"), + MP3DescStorageStyle("replaygain_album_gain", float_places=2, suffix=" dB"), MP4StorageStyle( - '----:com.apple.iTunes:replaygain_album_gain', - float_places=2, suffix=' dB' - ), - StorageStyle( - u'REPLAYGAIN_ALBUM_GAIN', - float_places=2, suffix=u' dB' - ), - ASFStorageStyle( - u'replaygain_album_gain', - float_places=2, suffix=u' dB' + "----:com.apple.iTunes:replaygain_album_gain", float_places=2, suffix=" dB" ), - out_type=float + StorageStyle("REPLAYGAIN_ALBUM_GAIN", float_places=2, suffix=" dB"), + ASFStorageStyle("replaygain_album_gain", float_places=2, suffix=" dB"), + out_type=float, ) rg_track_peak = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_TRACK_PEAK', - float_places=6 - ), - MP3DescStorageStyle( - u'replaygain_track_peak', - float_places=6 - ), - MP3SoundCheckStorageStyle( - key=u'COMM', - index=1, desc=u'iTunNORM', - id3_lang='eng' - ), - MP4StorageStyle( - '----:com.apple.iTunes:replaygain_track_peak', - float_places=6 - ), - MP4SoundCheckStorageStyle( - '----:com.apple.iTunes:iTunNORM', - index=1 - ), - StorageStyle(u'REPLAYGAIN_TRACK_PEAK', float_places=6), - ASFStorageStyle(u'replaygain_track_peak', float_places=6), + MP3DescStorageStyle("REPLAYGAIN_TRACK_PEAK", float_places=6), + MP3DescStorageStyle("replaygain_track_peak", float_places=6), + MP3SoundCheckStorageStyle(key="COMM", index=1, desc="iTunNORM", id3_lang="eng"), + MP4StorageStyle("----:com.apple.iTunes:replaygain_track_peak", float_places=6), + MP4SoundCheckStorageStyle("----:com.apple.iTunes:iTunNORM", index=1), + StorageStyle("REPLAYGAIN_TRACK_PEAK", float_places=6), + ASFStorageStyle("replaygain_track_peak", float_places=6), out_type=float, ) rg_album_peak = MediaField( - MP3DescStorageStyle( - u'REPLAYGAIN_ALBUM_PEAK', - float_places=6 - ), - MP3DescStorageStyle( - u'replaygain_album_peak', - float_places=6 - ), - MP4StorageStyle( - '----:com.apple.iTunes:replaygain_album_peak', - float_places=6 - ), - StorageStyle(u'REPLAYGAIN_ALBUM_PEAK', float_places=6), - ASFStorageStyle(u'replaygain_album_peak', float_places=6), + MP3DescStorageStyle("REPLAYGAIN_ALBUM_PEAK", float_places=6), + MP3DescStorageStyle("replaygain_album_peak", float_places=6), + MP4StorageStyle("----:com.apple.iTunes:replaygain_album_peak", float_places=6), + StorageStyle("REPLAYGAIN_ALBUM_PEAK", float_places=6), + ASFStorageStyle("replaygain_album_peak", float_places=6), out_type=float, ) # EBU R128 fields. r128_track_gain = QNumberField( 8, - MP3DescStorageStyle( - u'R128_TRACK_GAIN' - ), - MP4StorageStyle( - '----:com.apple.iTunes:R128_TRACK_GAIN' - ), - StorageStyle( - u'R128_TRACK_GAIN' - ), - ASFStorageStyle( - u'R128_TRACK_GAIN' - ), + MP3DescStorageStyle("R128_TRACK_GAIN"), + MP4StorageStyle("----:com.apple.iTunes:R128_TRACK_GAIN"), + StorageStyle("R128_TRACK_GAIN"), + ASFStorageStyle("R128_TRACK_GAIN"), ) r128_album_gain = QNumberField( 8, - MP3DescStorageStyle( - u'R128_ALBUM_GAIN' - ), - MP4StorageStyle( - '----:com.apple.iTunes:R128_ALBUM_GAIN' - ), - StorageStyle( - u'R128_ALBUM_GAIN' - ), - ASFStorageStyle( - u'R128_ALBUM_GAIN' - ), + MP3DescStorageStyle("R128_ALBUM_GAIN"), + MP4StorageStyle("----:com.apple.iTunes:R128_ALBUM_GAIN"), + StorageStyle("R128_ALBUM_GAIN"), + ASFStorageStyle("R128_ALBUM_GAIN"), ) initial_key = MediaField( - MP3StorageStyle('TKEY'), - MP4StorageStyle('----:com.apple.iTunes:initialkey'), - StorageStyle('INITIALKEY'), - ASFStorageStyle('INITIALKEY'), + MP3StorageStyle("TKEY"), + MP4StorageStyle("----:com.apple.iTunes:initialkey"), + StorageStyle("INITIALKEY"), + ASFStorageStyle("INITIALKEY"), ) @property @@ -2318,9 +2256,9 @@ def length(self): @property def samplerate(self): """The audio's sample rate (an int).""" - if hasattr(self.mgfile.info, 'sample_rate'): + if hasattr(self.mgfile.info, "sample_rate"): return self.mgfile.info.sample_rate - elif self.type == 'opus': + elif self.type == "opus": # Opus is always 48kHz internally. return 48000 return 0 @@ -2331,14 +2269,14 @@ def bitdepth(self): Only available for certain file formats (zero where unavailable). """ - if hasattr(self.mgfile.info, 'bits_per_sample'): + if hasattr(self.mgfile.info, "bits_per_sample"): return self.mgfile.info.bits_per_sample return 0 @property def channels(self): """The number of channels in the audio (an int).""" - if hasattr(self.mgfile.info, 'channels'): + if hasattr(self.mgfile.info, "channels"): return self.mgfile.info.channels return 0 @@ -2351,7 +2289,7 @@ def bitrate(self): imprecision is possible because the file header is incorporated in the file size. """ - if hasattr(self.mgfile.info, 'bitrate') and self.mgfile.info.bitrate: + if hasattr(self.mgfile.info, "bitrate") and self.mgfile.info.bitrate: # Many formats provide it explicitly. return self.mgfile.info.bitrate else: @@ -2368,14 +2306,14 @@ def bitrate_mode(self): (a string, eg. "CBR", "VBR" or "ABR"). Only available for the MP3 file format (empty where unavailable). """ - if hasattr(self.mgfile.info, 'bitrate_mode'): + if hasattr(self.mgfile.info, "bitrate_mode"): return { - mutagen.mp3.BitrateMode.CBR: 'CBR', - mutagen.mp3.BitrateMode.VBR: 'VBR', - mutagen.mp3.BitrateMode.ABR: 'ABR', - }.get(self.mgfile.info.bitrate_mode, '') + mutagen.mp3.BitrateMode.CBR: "CBR", + mutagen.mp3.BitrateMode.VBR: "VBR", + mutagen.mp3.BitrateMode.ABR: "ABR", + }.get(self.mgfile.info.bitrate_mode, "") else: - return '' + return "" @property def encoder_info(self): @@ -2383,20 +2321,20 @@ def encoder_info(self): (a string, eg. "LAME 3.97.0"). Only available for some formats (empty where unavailable). """ - if hasattr(self.mgfile.info, 'encoder_info'): + if hasattr(self.mgfile.info, "encoder_info"): return self.mgfile.info.encoder_info else: - return '' + return "" @property def encoder_settings(self): """A guess of the settings used for the encoder (a string, eg. "-V2"). Only available for the MP3 file format (empty where unavailable). """ - if hasattr(self.mgfile.info, 'encoder_settings'): + if hasattr(self.mgfile.info, "encoder_settings"): return self.mgfile.info.encoder_settings else: - return '' + return "" @property def format(self): diff --git a/pyproject.toml b/pyproject.toml index 1f08159..9586d90 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,3 +28,28 @@ classifiers = [ test = [ "tox" ] +dev = [ + "ruff", + "pytest" +] + +[tool.ruff] +target-version = "py39" +include = ["mediafile.py", "test/**/*.py"] + +[tool.ruff.lint] +select = [ + "E", # pycodestyle + "F", # pyflakes + "G", # flake8-logging-format + "I", # isort + "ISC", # flake8-implicit-str-concat + "N", # pep8-naming + "W", # pycodestyle +] +ignore = [ + "TC006", # no need to quote 'cast's since we use 'from __future__ import annotations' +] + +[tool.ruff.format] +quote-style = "double" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index 36d7505..de582f2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -2,12 +2,3 @@ verbosity=1 logging-clear-handlers=1 eval-attr="!=slow" - -[flake8] -min-version=3.7 -# Default pyflakes errors we ignore: -# - E241: missing whitespace after ',' (used to align visually) -# - E221: multiple spaces before operator (used to align visually) -# - E731: do not assign a lambda expression, use a def -# - C901: function/method complexity -ignore=C901,E241,E221,E731 diff --git a/test/_common.py b/test/_common.py index 16d126d..e65ccc2 100644 --- a/test/_common.py +++ b/test/_common.py @@ -14,24 +14,23 @@ # included in all copies or substantial portions of the Software. """Some common functionality for mediafile tests.""" + import os -import tempfile import shutil import sys - +import tempfile # Test resources path. -RSRC = os.path.join(os.path.dirname(__file__), 'rsrc').encode('utf8') +RSRC = os.path.join(os.path.dirname(__file__), "rsrc").encode("utf8") # OS feature test. -HAVE_SYMLINK = sys.platform != 'win32' +HAVE_SYMLINK = sys.platform != "win32" # Convenience methods for setting up a temporary sandbox directory for tests # that need to interact with the filesystem. class TempDirMixin(object): - """Text mixin for creating and deleting a temporary directory. - """ + """Text mixin for creating and deleting a temporary directory.""" def create_temp_dir(self): """Create a temporary directory and assign it into `self.temp_dir`. @@ -39,11 +38,10 @@ def create_temp_dir(self): """ path = tempfile.mkdtemp() if not isinstance(path, bytes): - path = path.encode('utf8') + path = path.encode("utf8") self.temp_dir = path def remove_temp_dir(self): - """Delete the temporary directory created by `create_temp_dir`. - """ + """Delete the temporary directory created by `create_temp_dir`.""" if os.path.isdir(self.temp_dir): shutil.rmtree(self.temp_dir) diff --git a/test/test_mediafile.py b/test/test_mediafile.py index 25790aa..0b05456 100644 --- a/test/test_mediafile.py +++ b/test/test_mediafile.py @@ -16,51 +16,54 @@ """Automatically-generated blanket testing for the MediaFile metadata layer. """ + +import datetime import os import shutil -import datetime import time import unittest -from test import _common -from mediafile import MediaFile, Image, \ - ImageType, CoverArtField, UnreadableFileError import mutagen +from mediafile import CoverArtField, Image, ImageType, MediaFile, UnreadableFileError +from test import _common + class ArtTestMixin(object): - """Test reads and writes of the ``art`` property. - """ + """Test reads and writes of the ``art`` property.""" @property def png_data(self): if not self._png_data: - image_file = os.path.join(_common.RSRC, b'image-2x3.png') - with open(image_file, 'rb') as f: + image_file = os.path.join(_common.RSRC, b"image-2x3.png") + with open(image_file, "rb") as f: self._png_data = f.read() return self._png_data + _png_data = None @property def jpg_data(self): if not self._jpg_data: - image_file = os.path.join(_common.RSRC, b'image-2x3.jpg') - with open(image_file, 'rb') as f: + image_file = os.path.join(_common.RSRC, b"image-2x3.jpg") + with open(image_file, "rb") as f: self._jpg_data = f.read() return self._jpg_data + _jpg_data = None @property def tiff_data(self): if not self._jpg_data: - image_file = os.path.join(_common.RSRC, b'image-2x3.tiff') - with open(image_file, 'rb') as f: + image_file = os.path.join(_common.RSRC, b"image-2x3.tiff") + with open(image_file, "rb") as f: self._jpg_data = f.read() return self._jpg_data + _jpg_data = None def test_set_png_art(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.art = self.png_data mediafile.save() @@ -68,7 +71,7 @@ def test_set_png_art(self): self.assertEqual(mediafile.art, self.png_data) def test_set_jpg_art(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.art = self.jpg_data mediafile.save() @@ -76,7 +79,7 @@ def test_set_jpg_art(self): self.assertEqual(mediafile.art, self.jpg_data) def test_delete_art(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.art = self.jpg_data mediafile.save() @@ -99,26 +102,25 @@ class ImageStructureTestMixin(ArtTestMixin): """ def test_read_image_structures(self): - mediafile = self._mediafile_fixture('image') + mediafile = self._mediafile_fixture("image") self.assertEqual(len(mediafile.images), 2) - image = next(i for i in mediafile.images - if i.mime_type == 'image/png') + image = next(i for i in mediafile.images if i.mime_type == "image/png") self.assertEqual(image.data, self.png_data) - self.assertExtendedImageAttributes(image, desc=u'album cover', - type=ImageType.front) + self.assertExtendedImageAttributes( + image, desc="album cover", type=ImageType.front + ) - image = next(i for i in mediafile.images - if i.mime_type == 'image/jpeg') + image = next(i for i in mediafile.images if i.mime_type == "image/jpeg") self.assertEqual(image.data, self.jpg_data) - self.assertExtendedImageAttributes(image, desc=u'the artist', - type=ImageType.artist) + self.assertExtendedImageAttributes( + image, desc="the artist", type=ImageType.artist + ) def test_set_image_structure(self): - mediafile = self._mediafile_fixture('empty') - image = Image(data=self.png_data, desc=u'album cover', - type=ImageType.front) + mediafile = self._mediafile_fixture("empty") + image = Image(data=self.png_data, desc="album cover", type=ImageType.front) mediafile.images = [image] mediafile.save() @@ -127,30 +129,30 @@ def test_set_image_structure(self): image = mediafile.images[0] self.assertEqual(image.data, self.png_data) - self.assertEqual(image.mime_type, 'image/png') - self.assertExtendedImageAttributes(image, desc=u'album cover', - type=ImageType.front) + self.assertEqual(image.mime_type, "image/png") + self.assertExtendedImageAttributes( + image, desc="album cover", type=ImageType.front + ) def test_add_image_structure(self): - mediafile = self._mediafile_fixture('image') + mediafile = self._mediafile_fixture("image") self.assertEqual(len(mediafile.images), 2) - image = Image(data=self.png_data, desc=u'the composer', - type=ImageType.composer) + image = Image(data=self.png_data, desc="the composer", type=ImageType.composer) mediafile.images += [image] mediafile.save() mediafile = MediaFile(mediafile.filename) self.assertEqual(len(mediafile.images), 3) - images = (i for i in mediafile.images if i.desc == u'the composer') + images = (i for i in mediafile.images if i.desc == "the composer") image = next(images, None) self.assertExtendedImageAttributes( - image, desc=u'the composer', type=ImageType.composer + image, desc="the composer", type=ImageType.composer ) def test_delete_image_structures(self): - mediafile = self._mediafile_fixture('image') + mediafile = self._mediafile_fixture("image") self.assertEqual(len(mediafile.images), 2) del mediafile.images @@ -160,15 +162,14 @@ def test_delete_image_structures(self): self.assertIsNone(mediafile.images) def test_guess_cover(self): - mediafile = self._mediafile_fixture('image') + mediafile = self._mediafile_fixture("image") self.assertEqual(len(mediafile.images), 2) cover = CoverArtField.guess_cover_image(mediafile.images) - self.assertEqual(cover.desc, u'album cover') + self.assertEqual(cover.desc, "album cover") self.assertEqual(mediafile.art, cover.data) def assertExtendedImageAttributes(self, image, **kwargs): # noqa - """Ignore extended image attributes in the base tests. - """ + """Ignore extended image attributes in the base tests.""" pass @@ -184,11 +185,10 @@ def assertExtendedImageAttributes(self, image, desc=None, type=None): # noqa self.assertEqual(image.type, type) def test_add_tiff_image(self): - mediafile = self._mediafile_fixture('image') + mediafile = self._mediafile_fixture("image") self.assertEqual(len(mediafile.images), 2) - image = Image(data=self.tiff_data, desc=u'the composer', - type=ImageType.composer) + image = Image(data=self.tiff_data, desc="the composer", type=ImageType.composer) mediafile.images += [image] mediafile.save() @@ -196,28 +196,27 @@ def test_add_tiff_image(self): self.assertEqual(len(mediafile.images), 3) # WMA does not preserve the order, so we have to work around this - image = list(filter(lambda i: i.mime_type == 'image/tiff', - mediafile.images))[0] + image = list(filter(lambda i: i.mime_type == "image/tiff", mediafile.images))[0] self.assertExtendedImageAttributes( - image, desc=u'the composer', type=ImageType.composer) + image, desc="the composer", type=ImageType.composer + ) class LazySaveTestMixin(object): - """Mediafile should only write changes when tags have changed - """ + """Mediafile should only write changes when tags have changed""" - @unittest.skip(u'not yet implemented') + @unittest.skip("not yet implemented") def test_unmodified(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mtime = self._set_past_mtime(mediafile.filename) self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) mediafile.save() self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) - @unittest.skip(u'not yet implemented') + @unittest.skip("not yet implemented") def test_same_tag_value(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mtime = self._set_past_mtime(mediafile.filename) self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) @@ -226,31 +225,31 @@ def test_same_tag_value(self): self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) def test_update_same_tag_value(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mtime = self._set_past_mtime(mediafile.filename) self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) - mediafile.update({'title': mediafile.title}) + mediafile.update({"title": mediafile.title}) mediafile.save() self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) - @unittest.skip(u'not yet implemented') + @unittest.skip("not yet implemented") def test_tag_value_change(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mtime = self._set_past_mtime(mediafile.filename) self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) mediafile.title = mediafile.title - mediafile.album = u'another' + mediafile.album = "another" mediafile.save() self.assertNotEqual(os.stat(mediafile.filename).st_mtime, mtime) def test_update_changed_tag_value(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mtime = self._set_past_mtime(mediafile.filename) self.assertEqual(os.stat(mediafile.filename).st_mtime, mtime) - mediafile.update({'title': mediafile.title, 'album': u'another'}) + mediafile.update({"title": mediafile.title, "album": "another"}) mediafile.save() self.assertNotEqual(os.stat(mediafile.filename).st_mtime, mtime) @@ -261,41 +260,39 @@ def _set_past_mtime(self, path): class GenreListTestMixin(object): - """Tests access to the ``genres`` property as a list. - """ + """Tests access to the ``genres`` property as a list.""" def test_read_genre_list(self): - mediafile = self._mediafile_fixture('full') - self.assertCountEqual(mediafile.genres, ['the genre']) + mediafile = self._mediafile_fixture("full") + self.assertCountEqual(mediafile.genres, ["the genre"]) def test_write_genre_list(self): - mediafile = self._mediafile_fixture('empty') - mediafile.genres = [u'one', u'two'] + mediafile = self._mediafile_fixture("empty") + mediafile.genres = ["one", "two"] mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertCountEqual(mediafile.genres, [u'one', u'two']) + self.assertCountEqual(mediafile.genres, ["one", "two"]) def test_write_genre_list_get_first(self): - mediafile = self._mediafile_fixture('empty') - mediafile.genres = [u'one', u'two'] + mediafile = self._mediafile_fixture("empty") + mediafile.genres = ["one", "two"] mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertEqual(mediafile.genre, u'one') + self.assertEqual(mediafile.genre, "one") def test_append_genre_list(self): - mediafile = self._mediafile_fixture('full') - self.assertEqual(mediafile.genre, u'the genre') - mediafile.genres += [u'another'] + mediafile = self._mediafile_fixture("full") + self.assertEqual(mediafile.genre, "the genre") + mediafile.genres += ["another"] mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertCountEqual(mediafile.genres, [u'the genre', u'another']) + self.assertCountEqual(mediafile.genres, ["the genre", "another"]) -class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, - _common.TempDirMixin): +class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, _common.TempDirMixin): """Test writing and reading tags. Subclasses must set ``extension`` and ``audio_properties``. @@ -316,93 +313,93 @@ class ReadWriteTestBase(ArtTestMixin, GenreListTestMixin, """ full_initial_tags = { - 'title': u'full', - 'artist': u'the artist', - 'album': u'the album', - 'genre': u'the genre', - 'composer': u'the composer', - 'grouping': u'the grouping', - 'year': 2001, - 'month': None, - 'day': None, - 'date': datetime.date(2001, 1, 1), - 'track': 2, - 'tracktotal': 3, - 'disc': 4, - 'disctotal': 5, - 'lyrics': u'the lyrics', - 'comments': u'the comments', - 'bpm': 6, - 'comp': True, - 'mb_trackid': '8b882575-08a5-4452-a7a7-cbb8a1531f9e', - 'mb_releasetrackid': 'c29f3a57-b439-46fd-a2e2-93776b1371e0', - 'mb_albumid': '9e873859-8aa4-4790-b985-5a953e8ef628', - 'mb_artistid': '7cf0ea9d-86b9-4dad-ba9e-2355a64899ea', - 'art': None, - 'label': u'the label', + "title": "full", + "artist": "the artist", + "album": "the album", + "genre": "the genre", + "composer": "the composer", + "grouping": "the grouping", + "year": 2001, + "month": None, + "day": None, + "date": datetime.date(2001, 1, 1), + "track": 2, + "tracktotal": 3, + "disc": 4, + "disctotal": 5, + "lyrics": "the lyrics", + "comments": "the comments", + "bpm": 6, + "comp": True, + "mb_trackid": "8b882575-08a5-4452-a7a7-cbb8a1531f9e", + "mb_releasetrackid": "c29f3a57-b439-46fd-a2e2-93776b1371e0", + "mb_albumid": "9e873859-8aa4-4790-b985-5a953e8ef628", + "mb_artistid": "7cf0ea9d-86b9-4dad-ba9e-2355a64899ea", + "art": None, + "label": "the label", } tag_fields = [ - 'title', - 'artist', - 'album', - 'genre', - 'lyricist', - 'composer', - 'composer_sort', - 'arranger', - 'grouping', - 'year', - 'month', - 'day', - 'date', - 'track', - 'tracktotal', - 'disc', - 'disctotal', - 'lyrics', - 'comments', - 'copyright', - 'bpm', - 'comp', - 'mb_trackid', - 'mb_releasetrackid', - 'mb_workid', - 'mb_albumid', - 'mb_artistid', - 'art', - 'label', - 'rg_track_peak', - 'rg_track_gain', - 'rg_album_peak', - 'rg_album_gain', - 'r128_track_gain', - 'r128_album_gain', - 'albumartist', - 'mb_albumartistid', - 'artist_sort', - 'albumartist_sort', - 'acoustid_fingerprint', - 'acoustid_id', - 'mb_releasegroupid', - 'asin', - 'catalognum', - 'barcode', - 'isrc', - 'disctitle', - 'script', - 'language', - 'country', - 'albumstatus', - 'media', - 'albumdisambig', - 'artist_credit', - 'albumartist_credit', - 'original_year', - 'original_month', - 'original_day', - 'original_date', - 'initial_key', + "title", + "artist", + "album", + "genre", + "lyricist", + "composer", + "composer_sort", + "arranger", + "grouping", + "year", + "month", + "day", + "date", + "track", + "tracktotal", + "disc", + "disctotal", + "lyrics", + "comments", + "copyright", + "bpm", + "comp", + "mb_trackid", + "mb_releasetrackid", + "mb_workid", + "mb_albumid", + "mb_artistid", + "art", + "label", + "rg_track_peak", + "rg_track_gain", + "rg_album_peak", + "rg_album_gain", + "r128_track_gain", + "r128_album_gain", + "albumartist", + "mb_albumartistid", + "artist_sort", + "albumartist_sort", + "acoustid_fingerprint", + "acoustid_id", + "mb_releasegroupid", + "asin", + "catalognum", + "barcode", + "isrc", + "disctitle", + "script", + "language", + "country", + "albumstatus", + "media", + "albumdisambig", + "artist_credit", + "albumartist_credit", + "original_year", + "original_month", + "original_day", + "original_date", + "initial_key", ] def setUp(self): @@ -412,12 +409,12 @@ def tearDown(self): self.remove_temp_dir() def test_read_nonexisting(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") os.remove(mediafile.filename) self.assertRaises(UnreadableFileError, MediaFile, mediafile.filename) def test_save_nonexisting(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") os.remove(mediafile.filename) try: mediafile.save() @@ -425,7 +422,7 @@ def test_save_nonexisting(self): pass def test_delete_nonexisting(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") os.remove(mediafile.filename) try: mediafile.delete() @@ -433,20 +430,19 @@ def test_delete_nonexisting(self): pass def test_read_audio_properties(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") for key, value in self.audio_properties.items(): if isinstance(value, float): - self.assertAlmostEqual(getattr(mediafile, key), value, - delta=0.1) + self.assertAlmostEqual(getattr(mediafile, key), value, delta=0.1) else: self.assertEqual(getattr(mediafile, key), value) def test_read_full(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") self.assertTags(mediafile, self.full_initial_tags) def test_read_empty(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") for field in self.tag_fields: value = getattr(mediafile, field) if isinstance(value, list): @@ -455,7 +451,7 @@ def test_read_empty(self): self.assertIsNone(value) def test_write_empty(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") tags = self._generate_tags() for key, value in tags.items(): @@ -466,7 +462,7 @@ def test_write_empty(self): self.assertTags(mediafile, tags) def test_update_empty(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") tags = self._generate_tags() mediafile.update(tags) @@ -476,7 +472,7 @@ def test_update_empty(self): self.assertTags(mediafile, tags) def test_overwrite_full(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") tags = self._generate_tags() for key, value in tags.items(): @@ -492,7 +488,7 @@ def test_overwrite_full(self): self.assertTags(mediafile, tags) def test_update_full(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") tags = self._generate_tags() mediafile.update(tags) @@ -505,7 +501,7 @@ def test_update_full(self): self.assertTags(mediafile, tags) def test_write_date_components(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mediafile.year = 2001 mediafile.month = 1 mediafile.day = 2 @@ -525,7 +521,7 @@ def test_write_date_components(self): self.assertEqual(mediafile.original_date, datetime.date(1999, 12, 30)) def test_write_incomplete_date_components(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.year = 2001 mediafile.month = None mediafile.day = 2 @@ -538,7 +534,7 @@ def test_write_incomplete_date_components(self): self.assertEqual(mediafile.date, datetime.date(2001, 1, 1)) def test_write_dates(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") mediafile.date = datetime.date(2001, 1, 2) mediafile.original_date = datetime.date(1999, 12, 30) mediafile.save() @@ -558,7 +554,7 @@ def round_trip(x): q_num = round(x * pow(2, 8)) return q_num / pow(2, 8) - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") track = -1.1 self.assertNotEqual(track, round_trip(track)) @@ -571,7 +567,7 @@ def round_trip(x): self.assertEqual(mediafile.r128_album_gain, round_trip(album)) def test_write_packed(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.tracktotal = 2 mediafile.track = 1 @@ -582,16 +578,16 @@ def test_write_packed(self): self.assertEqual(mediafile.tracktotal, 2) def test_write_counters_without_total(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") self.assertEqual(mediafile.track, 2) self.assertEqual(mediafile.tracktotal, 3) self.assertEqual(mediafile.disc, 4) self.assertEqual(mediafile.disctotal, 5) mediafile.track = 10 - delattr(mediafile, 'tracktotal') + delattr(mediafile, "tracktotal") mediafile.disc = 10 - delattr(mediafile, 'disctotal') + delattr(mediafile, "disctotal") mediafile.save() mediafile = MediaFile(mediafile.filename) @@ -604,7 +600,7 @@ def test_unparseable_date(self): """The `unparseable.*` fixture should not crash but should return None for all parts of the release date. """ - mediafile = self._mediafile_fixture('unparseable') + mediafile = self._mediafile_fixture("unparseable") self.assertIsNone(mediafile.date) self.assertIsNone(mediafile.year) @@ -612,10 +608,10 @@ def test_unparseable_date(self): self.assertIsNone(mediafile.day) def test_delete_tag(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") keys = self.full_initial_tags.keys() - for key in set(keys) - set(['art', 'month', 'day']): + for key in set(keys) - set(["art", "month", "day"]): self.assertIsNotNone(getattr(mediafile, key)) for key in keys: delattr(mediafile, key) @@ -630,18 +626,18 @@ def test_delete_tag(self): self.assertIsNone(value) def test_delete_packed_total(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") - delattr(mediafile, 'tracktotal') - delattr(mediafile, 'disctotal') + delattr(mediafile, "tracktotal") + delattr(mediafile, "disctotal") mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertEqual(mediafile.track, self.full_initial_tags['track']) - self.assertEqual(mediafile.disc, self.full_initial_tags['disc']) + self.assertEqual(mediafile.track, self.full_initial_tags["track"]) + self.assertEqual(mediafile.disc, self.full_initial_tags["disc"]) def test_delete_partial_date(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.date = datetime.date(2001, 12, 3) mediafile.save() @@ -651,7 +647,7 @@ def test_delete_partial_date(self): self.assertIsNotNone(mediafile.month) self.assertIsNotNone(mediafile.day) - delattr(mediafile, 'month') + delattr(mediafile, "month") mediafile.save() mediafile = MediaFile(mediafile.filename) self.assertIsNotNone(mediafile.date) @@ -660,12 +656,12 @@ def test_delete_partial_date(self): self.assertIsNone(mediafile.day) def test_delete_year(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") self.assertIsNotNone(mediafile.date) self.assertIsNotNone(mediafile.year) - delattr(mediafile, 'year') + delattr(mediafile, "year") mediafile.save() mediafile = MediaFile(mediafile.filename) self.assertIsNone(mediafile.date) @@ -677,133 +673,140 @@ def assertTags(self, mediafile, tags): # noqa try: value2 = getattr(mediafile, key) except AttributeError: - errors.append(u'Tag %s does not exist' % key) + errors.append("Tag %s does not exist" % key) else: if value2 != value: - errors.append(u'Tag %s: %r != %r' % (key, value2, value)) + errors.append("Tag %s: %r != %r" % (key, value2, value)) if any(errors): - errors = [u'Tags did not match'] + errors - self.fail('\n '.join(errors)) + errors = ["Tags did not match"] + errors + self.fail("\n ".join(errors)) def _mediafile_fixture(self, name): - name = name + '.' + self.extension + name = name + "." + self.extension if not isinstance(name, bytes): - name = name.encode('utf8') + name = name.encode("utf8") src = os.path.join(_common.RSRC, name) target = os.path.join(self.temp_dir, name) shutil.copy(src, target) return MediaFile(target) def _generate_tags(self, base=None): - """Return dictionary of tags, mapping tag names to values. - """ + """Return dictionary of tags, mapping tag names to values.""" tags = {} for key in self.tag_fields: - if key.startswith('rg_'): + if key.startswith("rg_"): # ReplayGain is float tags[key] = 1.0 - elif key.startswith('r128_'): + elif key.startswith("r128_"): # R128 is int tags[key] = -1 else: - tags[key] = 'value\u2010%s' % key + tags[key] = "value\u2010%s" % key - for key in ['disc', 'disctotal', 'track', 'tracktotal', 'bpm']: + for key in ["disc", "disctotal", "track", "tracktotal", "bpm"]: tags[key] = 1 for key in [ - 'artists', 'albumartists', 'artists_credit', - 'albumartists_credit', 'artists_sort', 'albumartists_sort' + "artists", + "albumartists", + "artists_credit", + "albumartists_credit", + "artists_sort", + "albumartists_sort", ]: - tags[key] = ['multival', 'test'] + tags[key] = ["multival", "test"] - tags['art'] = self.jpg_data - tags['comp'] = True + tags["art"] = self.jpg_data + tags["comp"] = True - tags['url'] = "https://example.com/" + tags["url"] = "https://example.com/" date = datetime.date(2001, 4, 3) - tags['date'] = date - tags['year'] = date.year - tags['month'] = date.month - tags['day'] = date.day + tags["date"] = date + tags["year"] = date.year + tags["month"] = date.month + tags["day"] = date.day original_date = datetime.date(1999, 5, 6) - tags['original_date'] = original_date - tags['original_year'] = original_date.year - tags['original_month'] = original_date.month - tags['original_day'] = original_date.day + tags["original_date"] = original_date + tags["original_year"] = original_date.year + tags["original_month"] = original_date.month + tags["original_day"] = original_date.day return tags class PartialTestMixin(object): tags_without_total = { - 'track': 2, - 'tracktotal': 0, - 'disc': 4, - 'disctotal': 0, + "track": 2, + "tracktotal": 0, + "disc": 4, + "disctotal": 0, } def test_read_track_without_total(self): - mediafile = self._mediafile_fixture('partial') + mediafile = self._mediafile_fixture("partial") self.assertEqual(mediafile.track, 2) self.assertIsNone(mediafile.tracktotal) self.assertEqual(mediafile.disc, 4) self.assertIsNone(mediafile.disctotal) -class MP3Test(ReadWriteTestBase, PartialTestMixin, - ExtendedImageStructureTestMixin, - unittest.TestCase): - extension = 'mp3' +class MP3Test( + ReadWriteTestBase, + PartialTestMixin, + ExtendedImageStructureTestMixin, + unittest.TestCase, +): + extension = "mp3" audio_properties = { - 'length': 1.0, - 'bitrate': 80000, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': 'MP3', - 'samplerate': 44100, - 'bitdepth': 0, - 'channels': 1, + "length": 1.0, + "bitrate": 80000, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "MP3", + "samplerate": 44100, + "bitdepth": 0, + "channels": 1, } def test_unknown_apic_type(self): - mediafile = self._mediafile_fixture('image_unknown_type') + mediafile = self._mediafile_fixture("image_unknown_type") self.assertEqual(mediafile.images[0].type, ImageType.other) def test_bitrate_mode(self): - mediafile = self._mediafile_fixture('cbr') - self.assertEqual(mediafile.bitrate_mode, 'CBR') + mediafile = self._mediafile_fixture("cbr") + self.assertEqual(mediafile.bitrate_mode, "CBR") def test_encoder_info(self): - mediafile = self._mediafile_fixture('cbr') - self.assertEqual(mediafile.encoder_info, 'LAME 3.100.0+') + mediafile = self._mediafile_fixture("cbr") + self.assertEqual(mediafile.encoder_info, "LAME 3.100.0+") def test_encoder_settings(self): - mediafile = self._mediafile_fixture('cbr') - self.assertEqual(mediafile.encoder_settings, '-b 80') + mediafile = self._mediafile_fixture("cbr") + self.assertEqual(mediafile.encoder_settings, "-b 80") -class MP4Test(ReadWriteTestBase, PartialTestMixin, - ImageStructureTestMixin, unittest.TestCase): - extension = 'm4a' +class MP4Test( + ReadWriteTestBase, PartialTestMixin, ImageStructureTestMixin, unittest.TestCase +): + extension = "m4a" audio_properties = { - 'length': 1.0, - 'bitrate': 64000, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': 'AAC', - 'samplerate': 44100, - 'bitdepth': 16, - 'channels': 2, + "length": 1.0, + "bitrate": 64000, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "AAC", + "samplerate": 44100, + "bitdepth": 16, + "channels": 2, } def test_add_tiff_image_fails(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") with self.assertRaises(ValueError): mediafile.images = [Image(data=self.tiff_data)] @@ -813,223 +816,223 @@ def test_guess_cover(self): class AlacTest(ReadWriteTestBase, unittest.TestCase): - extension = 'alac.m4a' + extension = "alac.m4a" audio_properties = { - 'length': 1.0, - 'bitrate': 21830, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', + "length": 1.0, + "bitrate": 21830, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", # 'format': 'ALAC', - 'samplerate': 44100, - 'bitdepth': 16, - 'channels': 1, + "samplerate": 44100, + "bitdepth": 16, + "channels": 1, } class MusepackTest(ReadWriteTestBase, unittest.TestCase): - extension = 'mpc' + extension = "mpc" audio_properties = { - 'length': 1.0, - 'bitrate': 24023, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'Musepack', - 'samplerate': 44100, - 'bitdepth': 0, - 'channels': 2, + "length": 1.0, + "bitrate": 24023, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "Musepack", + "samplerate": 44100, + "bitdepth": 0, + "channels": 2, } -class WMATest(ReadWriteTestBase, ExtendedImageStructureTestMixin, - unittest.TestCase): - extension = 'wma' +class WMATest(ReadWriteTestBase, ExtendedImageStructureTestMixin, unittest.TestCase): + extension = "wma" audio_properties = { - 'length': 1.0, - 'bitrate': 128000, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'Windows Media', - 'samplerate': 44100, - 'bitdepth': 0, - 'channels': 1, + "length": 1.0, + "bitrate": 128000, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "Windows Media", + "samplerate": 44100, + "bitdepth": 0, + "channels": 1, } def test_write_genre_list_get_first(self): # WMA does not preserve list order - mediafile = self._mediafile_fixture('empty') - mediafile.genres = [u'one', u'two'] + mediafile = self._mediafile_fixture("empty") + mediafile.genres = ["one", "two"] mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertIn(mediafile.genre, [u'one', u'two']) + self.assertIn(mediafile.genre, ["one", "two"]) def test_read_pure_tags(self): - mediafile = self._mediafile_fixture('pure') - self.assertEqual(mediafile.comments, u'the comments') - self.assertEqual(mediafile.title, u'the title') - self.assertEqual(mediafile.artist, u'the artist') + mediafile = self._mediafile_fixture("pure") + self.assertEqual(mediafile.comments, "the comments") + self.assertEqual(mediafile.title, "the title") + self.assertEqual(mediafile.artist, "the artist") -class OggTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, - unittest.TestCase): - extension = 'ogg' +class OggTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, unittest.TestCase): + extension = "ogg" audio_properties = { - 'length': 1.0, - 'bitrate': 48000, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'OGG', - 'samplerate': 44100, - 'bitdepth': 0, - 'channels': 1, + "length": 1.0, + "bitrate": 48000, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "OGG", + "samplerate": 44100, + "bitdepth": 0, + "channels": 1, } def test_read_date_from_year_tag(self): - mediafile = self._mediafile_fixture('year') + mediafile = self._mediafile_fixture("year") self.assertEqual(mediafile.year, 2000) self.assertEqual(mediafile.date, datetime.date(2000, 1, 1)) def test_write_date_to_year_tag(self): - mediafile = self._mediafile_fixture('empty') + mediafile = self._mediafile_fixture("empty") mediafile.year = 2000 mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertEqual(mediafile.mgfile['YEAR'], [u'2000']) + self.assertEqual(mediafile.mgfile["YEAR"], ["2000"]) def test_legacy_coverart_tag(self): - mediafile = self._mediafile_fixture('coverart') - self.assertTrue('coverart' in mediafile.mgfile) + mediafile = self._mediafile_fixture("coverart") + self.assertTrue("coverart" in mediafile.mgfile) self.assertEqual(mediafile.art, self.png_data) mediafile.art = self.png_data mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertFalse('coverart' in mediafile.mgfile) + self.assertFalse("coverart" in mediafile.mgfile) def test_date_tag_with_slashes(self): - mediafile = self._mediafile_fixture('date_with_slashes') + mediafile = self._mediafile_fixture("date_with_slashes") self.assertEqual(mediafile.year, 2005) self.assertEqual(mediafile.month, 6) self.assertEqual(mediafile.day, 5) -class FlacTest(ReadWriteTestBase, PartialTestMixin, - ExtendedImageStructureTestMixin, - unittest.TestCase): - extension = 'flac' +class FlacTest( + ReadWriteTestBase, + PartialTestMixin, + ExtendedImageStructureTestMixin, + unittest.TestCase, +): + extension = "flac" audio_properties = { - 'length': 1.0, - 'bitrate': 108688, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'FLAC', - 'samplerate': 44100, - 'bitdepth': 16, - 'channels': 1, + "length": 1.0, + "bitrate": 108688, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "FLAC", + "samplerate": 44100, + "bitdepth": 16, + "channels": 1, } -class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, - unittest.TestCase): - extension = 'ape' +class ApeTest(ReadWriteTestBase, ExtendedImageStructureTestMixin, unittest.TestCase): + extension = "ape" audio_properties = { - 'length': 1.0, - 'bitrate': 112608, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'APE', - 'samplerate': 44100, - 'bitdepth': 16, - 'channels': 1, + "length": 1.0, + "bitrate": 112608, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "APE", + "samplerate": 44100, + "bitdepth": 16, + "channels": 1, } class WavpackTest(ReadWriteTestBase, unittest.TestCase): - extension = 'wv' + extension = "wv" audio_properties = { - 'length': 1.0, - 'bitrate': 109312, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'WavPack', - 'samplerate': 44100, - 'bitdepth': 16 if mutagen.version >= (1, 45, 0) else 0, - 'channels': 1, + "length": 1.0, + "bitrate": 109312, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "WavPack", + "samplerate": 44100, + "bitdepth": 16 if mutagen.version >= (1, 45, 0) else 0, + "channels": 1, } class OpusTest(ReadWriteTestBase, unittest.TestCase): - extension = 'opus' + extension = "opus" audio_properties = { - 'length': 1.0, - 'bitrate': 66792, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'Opus', - 'samplerate': 48000, - 'bitdepth': 0, - 'channels': 1, + "length": 1.0, + "bitrate": 66792, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "Opus", + "samplerate": 48000, + "bitdepth": 0, + "channels": 1, } class AIFFTest(ReadWriteTestBase, unittest.TestCase): - extension = 'aiff' + extension = "aiff" audio_properties = { - 'length': 1.0, - 'bitrate': 705600, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'AIFF', - 'samplerate': 44100, - 'bitdepth': 16, - 'channels': 1, + "length": 1.0, + "bitrate": 705600, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "AIFF", + "samplerate": 44100, + "bitdepth": 16, + "channels": 1, } class WAVETest(ReadWriteTestBase, unittest.TestCase): - extension = 'wav' + extension = "wav" audio_properties = { - 'length': 1.0, - 'bitrate': 705600, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'WAVE', - 'samplerate': 44100, - 'bitdepth': 16, - 'channels': 1, + "length": 1.0, + "bitrate": 705600, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "WAVE", + "samplerate": 44100, + "bitdepth": 16, + "channels": 1, } full_initial_tags = { - 'title': u'full', - 'artist': u'the artist', - 'album': u'the album', - 'genre': u'the genre', - 'track': 2, - 'tracktotal': 3, + "title": "full", + "artist": "the artist", + "album": "the album", + "genre": "the genre", + "track": 2, + "tracktotal": 3, } tag_fields = [ - 'title', - 'artist', - 'album', - 'genre', - 'track', - 'original_year', - 'original_month', - 'original_day', - 'original_date', + "title", + "artist", + "album", + "genre", + "track", + "original_year", + "original_month", + "original_day", + "original_date", ] # Only a small subset of fields are supported by LIST/INFO @@ -1039,30 +1042,30 @@ class WAVETest(ReadWriteTestBase, unittest.TestCase): # Missing fields: disc, disctotal def test_write_counters_without_total(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") self.assertEqual(mediafile.track, 2) self.assertEqual(mediafile.tracktotal, 3) # Missing fields: date, year def test_delete_year(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") self.assertIsNotNone(mediafile.original_year) - delattr(mediafile, 'original_year') + delattr(mediafile, "original_year") mediafile.save() mediafile = MediaFile(mediafile.filename) self.assertIsNone(mediafile.original_year) # Missing fields: disctotal def test_delete_packed_total(self): - mediafile = self._mediafile_fixture('full') + mediafile = self._mediafile_fixture("full") - delattr(mediafile, 'tracktotal') + delattr(mediafile, "tracktotal") mediafile.save() mediafile = MediaFile(mediafile.filename) - self.assertEqual(mediafile.track, self.full_initial_tags['track']) + self.assertEqual(mediafile.track, self.full_initial_tags["track"]) # Check whether we have a Mutagen version with DSF support. We can @@ -1077,30 +1080,29 @@ def test_delete_packed_total(self): @unittest.skipIf(not HAVE_DSF, "Mutagen does not have DSF support") class DSFTest(ReadWriteTestBase, unittest.TestCase): - extension = 'dsf' + extension = "dsf" audio_properties = { - 'length': 0.01, - 'bitrate': 11289600, - 'bitrate_mode': '', - 'encoder_info': '', - 'encoder_settings': '', - 'format': u'DSD Stream File', - 'samplerate': 5644800, - 'bitdepth': 1, - 'channels': 2, + "length": 0.01, + "bitrate": 11289600, + "bitrate_mode": "", + "encoder_info": "", + "encoder_settings": "", + "format": "DSD Stream File", + "samplerate": 5644800, + "bitdepth": 1, + "channels": 2, } class MediaFieldTest(unittest.TestCase): - def test_properties_from_fields(self): - path = os.path.join(_common.RSRC, b'full.mp3') + path = os.path.join(_common.RSRC, b"full.mp3") mediafile = MediaFile(path) for field in MediaFile.fields(): self.assertTrue(hasattr(mediafile, field)) def test_properties_from_readable_fields(self): - path = os.path.join(_common.RSRC, b'full.mp3') + path = os.path.join(_common.RSRC, b"full.mp3") mediafile = MediaFile(path) for field in MediaFile.readable_fields(): self.assertTrue(hasattr(mediafile, field)) @@ -1108,11 +1110,25 @@ def test_properties_from_readable_fields(self): def test_known_fields(self): fields = list(ReadWriteTestBase.tag_fields) fields.extend( - ('encoder', 'images', 'genres', 'albumtype', 'artists', - 'albumartists', 'url', 'mb_artistids', 'mb_albumartistids', - 'albumtypes', 'catalognums', 'languages', 'artists_credit', - 'artists_sort', 'albumartists_credit', 'albumartists_sort', - 'subtitle') + ( + "encoder", + "images", + "genres", + "albumtype", + "artists", + "albumartists", + "url", + "mb_artistids", + "mb_albumartistids", + "albumtypes", + "catalognums", + "languages", + "artists_credit", + "artists_sort", + "albumartists_credit", + "albumartists_sort", + "subtitle", + ) ) self.assertCountEqual(MediaFile.fields(), fields) @@ -1126,5 +1142,5 @@ def suite(): return unittest.TestLoader().loadTestsFromName(__name__) -if __name__ == '__main__': - unittest.main(defaultTest='suite') +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/test/test_mediafile_edge.py b/test/test_mediafile_edge.py index 4796525..7dd40a1 100644 --- a/test/test_mediafile_edge.py +++ b/test/test_mediafile_edge.py @@ -13,17 +13,16 @@ # The above copyright notice and this permission notice shall be # included in all copies or substantial portions of the Software. -"""Specific, edge-case tests for the MediaFile metadata layer. -""" +"""Specific, edge-case tests for the MediaFile metadata layer.""" + import os import shutil import unittest -import mutagen.id3 -from test import _common +import mutagen.id3 import mediafile - +from test import _common _sc = mediafile._safe_cast @@ -33,18 +32,14 @@ def test_emptylist(self): # Some files have an ID3 frame that has a list with no elements. # This is very hard to produce, so this is just the first 8192 # bytes of a file found "in the wild". - emptylist = mediafile.MediaFile( - os.path.join(_common.RSRC, b'emptylist.mp3') - ) + emptylist = mediafile.MediaFile(os.path.join(_common.RSRC, b"emptylist.mp3")) genre = emptylist.genre self.assertEqual(genre, None) def test_release_time_with_space(self): # Ensures that release times delimited by spaces are ignored. # Amie Street produces such files. - space_time = mediafile.MediaFile( - os.path.join(_common.RSRC, b'space_time.mp3') - ) + space_time = mediafile.MediaFile(os.path.join(_common.RSRC, b"space_time.mp3")) self.assertEqual(space_time.year, 2009) self.assertEqual(space_time.month, 9) self.assertEqual(space_time.day, 4) @@ -52,9 +47,7 @@ def test_release_time_with_space(self): def test_release_time_with_t(self): # Ensures that release times delimited by Ts are ignored. # The iTunes Store produces such files. - t_time = mediafile.MediaFile( - os.path.join(_common.RSRC, b't_time.m4a') - ) + t_time = mediafile.MediaFile(os.path.join(_common.RSRC, b"t_time.m4a")) self.assertEqual(t_time.year, 1987) self.assertEqual(t_time.month, 3) self.assertEqual(t_time.day, 31) @@ -62,74 +55,72 @@ def test_release_time_with_t(self): def test_tempo_with_bpm(self): # Some files have a string like "128 BPM" in the tempo field # rather than just a number. - f = mediafile.MediaFile(os.path.join(_common.RSRC, b'bpm.mp3')) + f = mediafile.MediaFile(os.path.join(_common.RSRC, b"bpm.mp3")) self.assertEqual(f.bpm, 128) def test_discc_alternate_field(self): # Different taggers use different vorbis comments to reflect # the disc and disc count fields: ensure that the alternative # style works. - f = mediafile.MediaFile(os.path.join(_common.RSRC, b'discc.ogg')) + f = mediafile.MediaFile(os.path.join(_common.RSRC, b"discc.ogg")) self.assertEqual(f.disc, 4) self.assertEqual(f.disctotal, 5) def test_old_ape_version_bitrate(self): - media_file = os.path.join(_common.RSRC, b'oldape.ape') + media_file = os.path.join(_common.RSRC, b"oldape.ape") f = mediafile.MediaFile(media_file) self.assertEqual(f.bitrate, 0) def test_soundcheck_non_ascii(self): # Make sure we don't crash when the iTunes SoundCheck field contains # non-ASCII binary data. - f = mediafile.MediaFile(os.path.join(_common.RSRC, - b'soundcheck-nonascii.m4a')) + f = mediafile.MediaFile(os.path.join(_common.RSRC, b"soundcheck-nonascii.m4a")) self.assertEqual(f.rg_track_gain, 0.0) class InvalidValueToleranceTest(unittest.TestCase): - def test_safe_cast_string_to_int(self): - self.assertEqual(_sc(int, u'something'), 0) + self.assertEqual(_sc(int, "something"), 0) def test_safe_cast_string_to_int_with_no_numbers(self): - self.assertEqual(_sc(int, u'-'), 0) + self.assertEqual(_sc(int, "-"), 0) def test_safe_cast_int_string_to_int(self): - self.assertEqual(_sc(int, u'20'), 20) + self.assertEqual(_sc(int, "20"), 20) def test_safe_cast_string_to_bool(self): - self.assertEqual(_sc(bool, u'whatever'), False) + self.assertEqual(_sc(bool, "whatever"), False) def test_safe_cast_intstring_to_bool(self): - self.assertEqual(_sc(bool, u'5'), True) + self.assertEqual(_sc(bool, "5"), True) def test_safe_cast_string_to_float(self): - self.assertAlmostEqual(_sc(float, u'1.234'), 1.234) + self.assertAlmostEqual(_sc(float, "1.234"), 1.234) def test_safe_cast_int_to_float(self): self.assertAlmostEqual(_sc(float, 2), 2.0) def test_safe_cast_string_with_cruft_to_float(self): - self.assertAlmostEqual(_sc(float, u'1.234stuff'), 1.234) + self.assertAlmostEqual(_sc(float, "1.234stuff"), 1.234) def test_safe_cast_negative_string_to_float(self): - self.assertAlmostEqual(_sc(float, u'-1.234'), -1.234) + self.assertAlmostEqual(_sc(float, "-1.234"), -1.234) def test_safe_cast_special_chars_to_unicode(self): - us = _sc(str, 'caf\xc3\xa9') + us = _sc(str, "caf\xc3\xa9") self.assertTrue(isinstance(us, str)) - self.assertTrue(us.startswith(u'caf')) + self.assertTrue(us.startswith("caf")) def test_safe_cast_float_with_no_numbers(self): - v = _sc(float, u'+') + v = _sc(float, "+") self.assertEqual(v, 0.0) def test_safe_cast_float_with_dot_only(self): - v = _sc(float, u'.') + v = _sc(float, ".") self.assertEqual(v, 0.0) def test_safe_cast_float_with_multiple_dots(self): - v = _sc(float, u'1.0.0') + v = _sc(float, "1.0.0") self.assertEqual(v, 1.0) @@ -140,9 +131,9 @@ def setUp(self): def tearDown(self): self.remove_temp_dir() - def _exccheck(self, fn, exc, data=''): + def _exccheck(self, fn, exc, data=""): fn = os.path.join(self.temp_dir, fn) - with open(fn, 'w') as f: + with open(fn, "w") as f: f.write(data) try: self.assertRaises(exc, mediafile.MediaFile, fn) @@ -151,45 +142,42 @@ def _exccheck(self, fn, exc, data=''): def test_corrupt_mp3_raises_unreadablefileerror(self): # Make sure we catch Mutagen reading errors appropriately. - self._exccheck(b'corrupt.mp3', mediafile.UnreadableFileError) + self._exccheck(b"corrupt.mp3", mediafile.UnreadableFileError) def test_corrupt_mp4_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.m4a', mediafile.UnreadableFileError) + self._exccheck(b"corrupt.m4a", mediafile.UnreadableFileError) def test_corrupt_flac_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.flac', mediafile.UnreadableFileError) + self._exccheck(b"corrupt.flac", mediafile.UnreadableFileError) def test_corrupt_ogg_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.ogg', mediafile.UnreadableFileError) + self._exccheck(b"corrupt.ogg", mediafile.UnreadableFileError) def test_invalid_ogg_header_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.ogg', mediafile.UnreadableFileError, - 'OggS\x01vorbis') + self._exccheck(b"corrupt.ogg", mediafile.UnreadableFileError, "OggS\x01vorbis") def test_corrupt_monkeys_raises_unreadablefileerror(self): - self._exccheck(b'corrupt.ape', mediafile.UnreadableFileError) + self._exccheck(b"corrupt.ape", mediafile.UnreadableFileError) def test_invalid_extension_raises_filetypeerror(self): - self._exccheck(b'something.unknown', mediafile.FileTypeError) + self._exccheck(b"something.unknown", mediafile.FileTypeError) def test_magic_xml_raises_unreadablefileerror(self): - self._exccheck(b'nothing.xml', mediafile.UnreadableFileError, - "ftyp") + self._exccheck(b"nothing.xml", mediafile.UnreadableFileError, "ftyp") - @unittest.skipUnless(_common.HAVE_SYMLINK, u'platform lacks symlink') + @unittest.skipUnless(_common.HAVE_SYMLINK, "platform lacks symlink") def test_broken_symlink(self): - fn = os.path.join(_common.RSRC, b'brokenlink') - os.symlink('does_not_exist', fn) + fn = os.path.join(_common.RSRC, b"brokenlink") + os.symlink("does_not_exist", fn) try: - self.assertRaises(mediafile.UnreadableFileError, - mediafile.MediaFile, fn) + self.assertRaises(mediafile.UnreadableFileError, mediafile.MediaFile, fn) finally: os.unlink(fn) class SideEffectsTest(unittest.TestCase): def setUp(self): - self.empty = os.path.join(_common.RSRC, b'empty.mp3') + self.empty = os.path.join(_common.RSRC, b"empty.mp3") def test_opening_tagless_file_leaves_untouched(self): old_mtime = os.stat(self.empty).st_mtime @@ -201,8 +189,8 @@ def test_opening_tagless_file_leaves_untouched(self): class MP4EncodingTest(unittest.TestCase, _common.TempDirMixin): def setUp(self): self.create_temp_dir() - src = os.path.join(_common.RSRC, b'full.m4a') - self.path = os.path.join(self.temp_dir, b'test.m4a') + src = os.path.join(_common.RSRC, b"full.m4a") + self.path = os.path.join(self.temp_dir, b"test.m4a") shutil.copy(src, self.path) self.mf = mediafile.MediaFile(self.path) @@ -211,17 +199,17 @@ def tearDown(self): self.remove_temp_dir() def test_unicode_label_in_m4a(self): - self.mf.label = u'foo\xe8bar' + self.mf.label = "foo\xe8bar" self.mf.save() new_mf = mediafile.MediaFile(self.path) - self.assertEqual(new_mf.label, u'foo\xe8bar') + self.assertEqual(new_mf.label, "foo\xe8bar") class MP3EncodingTest(unittest.TestCase, _common.TempDirMixin): def setUp(self): self.create_temp_dir() - src = os.path.join(_common.RSRC, b'full.mp3') - self.path = os.path.join(self.temp_dir, b'test.mp3') + src = os.path.join(_common.RSRC, b"full.mp3") + self.path = os.path.join(self.temp_dir, b"test.mp3") shutil.copy(src, self.path) self.mf = mediafile.MediaFile(self.path) @@ -230,10 +218,10 @@ def test_comment_with_latin1_encoding(self): # Set up the test file with a Latin1-encoded COMM frame. The encoding # indices defined by MP3 are listed here: # http://id3.org/id3v2.4.0-structure - self.mf.mgfile['COMM::eng'].encoding = 0 + self.mf.mgfile["COMM::eng"].encoding = 0 # Try to store non-Latin1 text. - self.mf.comments = u'\u2028' + self.mf.comments = "\u2028" self.mf.save() @@ -246,7 +234,7 @@ def length(self): class MissingAudioDataTest(unittest.TestCase): def setUp(self): super(MissingAudioDataTest, self).setUp() - path = os.path.join(_common.RSRC, b'full.mp3') + path = os.path.join(_common.RSRC, b"full.mp3") self.mf = ZeroLengthMediaFile(path) def test_bitrate_with_zero_length(self): @@ -257,11 +245,11 @@ def test_bitrate_with_zero_length(self): class TypeTest(unittest.TestCase): def setUp(self): super(TypeTest, self).setUp() - path = os.path.join(_common.RSRC, b'full.mp3') + path = os.path.join(_common.RSRC, b"full.mp3") self.mf = mediafile.MediaFile(path) def test_year_integer_in_string(self): - self.mf.year = u'2009' + self.mf.year = "2009" self.assertEqual(self.mf.year, 2009) def test_set_replaygain_gain_to_none(self): @@ -296,26 +284,28 @@ def test_round_trip(self): self.assertEqual(peak, 1.0) def test_decode_zero(self): - data = b' 80000000 80000000 00000000 00000000 00000000 00000000 ' \ - b'00000000 00000000 00000000 00000000' + data = ( + b" 80000000 80000000 00000000 00000000 00000000 00000000 " + b"00000000 00000000 00000000 00000000" + ) gain, peak = mediafile._sc_decode(data) self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) def test_malformatted(self): - gain, peak = mediafile._sc_decode(b'foo') + gain, peak = mediafile._sc_decode(b"foo") self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) def test_special_characters(self): - gain, peak = mediafile._sc_decode(u'caf\xe9'.encode('utf-8')) + gain, peak = mediafile._sc_decode("caf\xe9".encode("utf-8")) self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) def test_decode_handles_unicode(self): # Most of the time, we expect to decode the raw bytes. But some formats # might give us text strings, which we need to handle. - gain, peak = mediafile._sc_decode(u'caf\xe9') + gain, peak = mediafile._sc_decode("caf\xe9") self.assertEqual(gain, 0.0) self.assertEqual(peak, 0.0) @@ -327,12 +317,10 @@ def test_encode_excessive_gain(self): class ID3v23Test(unittest.TestCase, _common.TempDirMixin): - def _make_test(self, ext=b'mp3', id3v23=False): + def _make_test(self, ext=b"mp3", id3v23=False): self.create_temp_dir() - src = os.path.join(_common.RSRC, - b'full.' + ext) - self.path = os.path.join(self.temp_dir, - b'test.' + ext) + src = os.path.join(_common.RSRC, b"full." + ext) + self.path = os.path.join(self.temp_dir, b"test." + ext) shutil.copy(src, self.path) return mediafile.MediaFile(self.path, id3v23=id3v23) @@ -344,9 +332,9 @@ def test_v24_year_tag(self): try: mf.year = 2013 mf.save() - frame = mf.mgfile['TDRC'] - self.assertTrue('2013' in str(frame)) - self.assertTrue('TYER' not in mf.mgfile) + frame = mf.mgfile["TDRC"] + self.assertTrue("2013" in str(frame)) + self.assertTrue("TYER" not in mf.mgfile) finally: self._delete_test() @@ -355,14 +343,14 @@ def test_v23_year_tag(self): try: mf.year = 2013 mf.save() - frame = mf.mgfile['TYER'] - self.assertTrue('2013' in str(frame)) - self.assertTrue('TDRC' not in mf.mgfile) + frame = mf.mgfile["TYER"] + self.assertTrue("2013" in str(frame)) + self.assertTrue("TDRC" not in mf.mgfile) finally: self._delete_test() def test_v23_on_non_mp3_is_noop(self): - mf = self._make_test(b'm4a', id3v23=True) + mf = self._make_test(b"m4a", id3v23=True) try: mf.year = 2013 mf.save() @@ -379,18 +367,21 @@ def test_image_encoding(self): mf = self._make_test(id3v23=v23) try: mf.images = [ - mediafile.Image(b'data', desc=u""), - mediafile.Image(b'data', desc=u"foo"), - mediafile.Image(b'data', desc=u"\u0185"), + mediafile.Image(b"data", desc=""), + mediafile.Image(b"data", desc="foo"), + mediafile.Image(b"data", desc="\u0185"), ] mf.save() - apic_frames = mf.mgfile.tags.getall('APIC') + apic_frames = mf.mgfile.tags.getall("APIC") encodings = dict([(f.desc, f.encoding) for f in apic_frames]) - self.assertEqual(encodings, { - u"": mutagen.id3.Encoding.LATIN1, - u"foo": mutagen.id3.Encoding.LATIN1, - u"\u0185": mutagen.id3.Encoding.UTF16, - }) + self.assertEqual( + encodings, + { + "": mutagen.id3.Encoding.LATIN1, + "foo": mutagen.id3.Encoding.LATIN1, + "\u0185": mutagen.id3.Encoding.UTF16, + }, + ) finally: self._delete_test() @@ -402,7 +393,8 @@ def setUp(self): self.field = mediafile.MediaField( mediafile.MP3StorageStyle(self.key, read_only=True), mediafile.MP4StorageStyle( - "----:com.apple.iTunes:" + self.key, read_only=True), + "----:com.apple.iTunes:" + self.key, read_only=True + ), mediafile.StorageStyle(self.key, read_only=True), mediafile.ASFStorageStyle(self.key, read_only=True), ) @@ -411,14 +403,14 @@ def setUp(self): mediafile.MediaFile.add_field("read_only_test", self.field) def test_read(self): - path = os.path.join(_common.RSRC, b'empty.flac') + path = os.path.join(_common.RSRC, b"empty.flac") mf = mediafile.MediaFile(path) mf.mgfile.tags[self.key] = "don't" self.assertEqual("don't", mf.read_only_test) def test_write(self): - src = os.path.join(_common.RSRC, b'empty.flac') - path = os.path.join(self.temp_dir, b'test.flac') + src = os.path.join(_common.RSRC, b"empty.flac") + path = os.path.join(self.temp_dir, b"test.flac") shutil.copy(src, path) mf = mediafile.MediaFile(path) mf.read_only_field = "something terrible" @@ -437,5 +429,5 @@ def suite(): return unittest.TestLoader().loadTestsFromName(__name__) -if __name__ == '__main__': - unittest.main(defaultTest='suite') +if __name__ == "__main__": + unittest.main(defaultTest="suite") diff --git a/tox.ini b/tox.ini index 7afd960..e356eb0 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py310-test, py310-flake8 +envlist = py310-test isolated_build = True [tox:.package] @@ -14,22 +14,8 @@ basepython = python3 deps = pytest -[_flake8] -deps = - flake8 - flake8-future-import - pep8-naming -files = mediafile.py test setup.py - [testenv] deps = {test,cov}: {[_test]deps} - py{36,37,38,39,310,311}-flake8: {[_flake8]deps} commands = - py3{6,7,8,9,10,11}-test: python -bb -m pytest {posargs} - py36-flake8: flake8 --min-version 3.6 {posargs} {[_flake8]files} - py37-flake8: flake8 --min-version 3.7 {posargs} {[_flake8]files} - py38-flake8: flake8 --min-version 3.8 {posargs} {[_flake8]files} - py39-flake8: flake8 --min-version 3.9 {posargs} {[_flake8]files} - py310-flake8: flake8 --min-version 3.10 {posargs} {[_flake8]files} - py311-flake8: flake8 --min-version 3.11 {posargs} {[_flake8]files} + py3{9,10,11,12,13}-test: python -bb -m pytest {posargs} \ No newline at end of file