audiolibrarian 0.16.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,19 @@
1
+ """The audiolibrarian package."""
2
+
3
+ #
4
+ # Copyright (c) 2020 Stephen Jibson
5
+ #
6
+ # This file is part of audiolibrarian.
7
+ #
8
+ # Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
9
+ # GNU General Public License as published by the Free Software Foundation, either version 3 of the
10
+ # License, or (at your option) any later version.
11
+ #
12
+ # Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13
+ # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
14
+ # the GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with audiolibrarian.
17
+ # If not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ __version__ = "0.16.2"
@@ -0,0 +1,23 @@
1
+ """Audio file library."""
2
+
3
+ #
4
+ # Copyright (c) 2020 Stephen Jibson
5
+ #
6
+ # This file is part of audiolibrarian.
7
+ #
8
+ # Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
9
+ # GNU General Public License as published by the Free Software Foundation, either version 3 of the
10
+ # License, or (at your option) any later version.
11
+ #
12
+ # Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13
+ # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
14
+ # the GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with audiolibrarian.
17
+ # If not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+
20
+ from audiolibrarian.audiofile.audiofile import AudioFile
21
+ from audiolibrarian.audiofile.tags import Tags
22
+
23
+ __all__ = ["AudioFile", "Tags"]
@@ -0,0 +1,114 @@
1
+ """Audio file library."""
2
+
3
+ #
4
+ # Copyright (c) 2020 Stephen Jibson
5
+ #
6
+ # This file is part of audiolibrarian.
7
+ #
8
+ # Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
9
+ # GNU General Public License as published by the Free Software Foundation, either version 3 of the
10
+ # License, or (at your option) any later version.
11
+ #
12
+ # Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13
+ # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
14
+ # the GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with audiolibrarian.
17
+ # If not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ import abc
20
+ import importlib
21
+ import pathlib
22
+ from typing import Any, ClassVar
23
+
24
+ import mutagen
25
+
26
+ from audiolibrarian import records
27
+
28
+
29
+ class AudioFile(abc.ABC):
30
+ """Abstract base class for AudioFile classes."""
31
+
32
+ _subclass_by_extension: ClassVar[dict[str, type["AudioFile"]]] = {}
33
+
34
+ def __init__(self, filepath: pathlib.Path) -> None:
35
+ """Initialize an AudioFile."""
36
+ self._filepath = filepath
37
+ self._mut_file = mutagen.File(self.filepath.absolute())
38
+ self._one_track = self.read_tags()
39
+
40
+ def __init_subclass__(cls, extensions: set[str], **kwargs: dict[str, Any]) -> None:
41
+ """Initialize an AudioFile subclass."""
42
+ super().__init_subclass__(**kwargs)
43
+ for extension in extensions:
44
+ cls._subclass_by_extension[extension] = cls
45
+
46
+ def __repr__(self) -> str:
47
+ """Return a string representation of the AudioFile."""
48
+ return f"AudioFile: {self.filepath}"
49
+
50
+ @classmethod
51
+ def extensions(cls) -> set[str]:
52
+ """Return the list of supported extensions."""
53
+ return set(cls._subclass_by_extension.keys())
54
+
55
+ @classmethod
56
+ def open(cls, filename: str | pathlib.Path) -> "AudioFile":
57
+ """Return an AudioFile object based on the filename extension (factory method).
58
+
59
+ Args:
60
+ filename: The filename of a supported audio file.
61
+
62
+ Returns:
63
+ audiofile.AudioFile: An AudioFile object.
64
+
65
+ Raises:
66
+ FileNotFoundError: If the file cannot be found or is not a file.
67
+ NotImplementedError: If the type of the file is not supported.
68
+ """
69
+ if not AudioFile._subclass_by_extension:
70
+ # Dynamically load submodules.
71
+ for module_path in (pathlib.Path(__file__).parent / "formats").glob("*.py"):
72
+ if module_path.name == "__init__.py":
73
+ continue
74
+ importlib.import_module(f"audiolibrarian.audiofile.formats.{module_path.stem}")
75
+
76
+ filepath = pathlib.Path(filename).resolve()
77
+ if not filepath.is_file():
78
+ raise FileNotFoundError(filepath)
79
+ if filepath.suffix not in AudioFile._subclass_by_extension:
80
+ msg = f"Unknown file type: {filepath}"
81
+ raise NotImplementedError(msg)
82
+ return AudioFile._subclass_by_extension[filepath.suffix](filepath=filepath)
83
+
84
+ @property
85
+ def filepath(self) -> pathlib.Path:
86
+ """Return the audio file's path."""
87
+ return self._filepath
88
+
89
+ @property
90
+ def one_track(self) -> records.OneTrack:
91
+ """Return the OneTrack representation of the audio file."""
92
+ return self._one_track
93
+
94
+ @one_track.setter
95
+ def one_track(self, one_track: records.OneTrack) -> None:
96
+ """Set the OneTrack representation of the audio file."""
97
+ self._one_track = one_track
98
+
99
+ @abc.abstractmethod
100
+ def read_tags(self) -> records.OneTrack:
101
+ """Read the tags from the audio file and return a populated OneTrack record."""
102
+
103
+ @abc.abstractmethod
104
+ def write_tags(self) -> None:
105
+ """Write the tags to the audio file."""
106
+
107
+ def _get_tag_sources(self) -> tuple[records.Release, int, records.Medium, int, records.Track]:
108
+ # Return the objects and information required to generate tags.
109
+ release = self.one_track.release or records.Release()
110
+ medium_number = self.one_track.medium_number
111
+ medium = self.one_track.medium or records.Medium()
112
+ track_number = self.one_track.track_number
113
+ track = self.one_track.track or records.Track()
114
+ return release, medium_number, medium, track_number, track
@@ -0,0 +1 @@
1
+ """AudioFile formats."""
@@ -0,0 +1,207 @@
1
+ """AudioFile support for flac files."""
2
+
3
+ #
4
+ # Copyright (c) 2020 Stephen Jibson
5
+ #
6
+ # This file is part of audiolibrarian.
7
+ #
8
+ # Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
9
+ # GNU General Public License as published by the Free Software Foundation, either version 3 of the
10
+ # License, or (at your option) any later version.
11
+ #
12
+ # Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13
+ # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
14
+ # the GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with audiolibrarian.
17
+ # If not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ import re
20
+ from typing import Any
21
+
22
+ import mutagen.flac
23
+
24
+ from audiolibrarian import audiofile, records
25
+
26
+
27
+ class FlacFile(audiofile.AudioFile, extensions={".flac"}):
28
+ """AudioFile for Flac files."""
29
+
30
+ def read_tags(self) -> records.OneTrack:
31
+ """Read the tags and return a OneTrack object."""
32
+
33
+ def listf(lst: list[Any] | None) -> records.ListF | None:
34
+ if lst is None:
35
+ return None
36
+ return records.ListF(lst)
37
+
38
+ mut = self._mut_file
39
+ front_cover = None
40
+ if self._mut_file.pictures:
41
+ cover = self._mut_file.pictures[0]
42
+ front_cover = records.FrontCover(
43
+ data=cover.data, desc=cover.desc or "", mime=cover.mime
44
+ )
45
+ medium_count = int(mut["disctotal"][0]) if mut.get("disctotal") else None
46
+ medium_number = int(mut["discnumber"][0]) if mut.get("discnumber") else None
47
+ track_count = int(mut["tracktotal"][0]) if mut.get("tracktotal") else None
48
+ track_number = int(mut["tracknumber"][0]) if mut.get("tracknumber") else None
49
+ release = (
50
+ records.Release(
51
+ album=mut.get("album", [None])[0],
52
+ album_artists=listf(mut.get("albumartist")),
53
+ album_artists_sort=listf(mut.get("albumartistsort")),
54
+ asins=mut.get("asin"),
55
+ barcodes=mut.get("barcode"),
56
+ catalog_numbers=mut.get("catalognumber"),
57
+ date=mut.get("date", [None])[0],
58
+ front_cover=front_cover,
59
+ genres=listf(mut.get("genre")),
60
+ labels=mut.get("label"),
61
+ media={
62
+ medium_number: records.Medium(
63
+ formats=listf(mut.get("media")),
64
+ titles=mut.get("discsubtitle"),
65
+ track_count=track_count,
66
+ tracks={
67
+ track_number: records.Track(
68
+ artist=mut.get("artist", [None])[0],
69
+ artists=listf(mut.get("artists")),
70
+ artists_sort=mut.get("artistsort"),
71
+ file_info=records.FileInfo(
72
+ bitrate=mut.info.bitrate // 1000,
73
+ bitrate_mode=records.BitrateMode.CBR,
74
+ path=self.filepath,
75
+ type=records.FileType.FLAC,
76
+ ),
77
+ isrcs=mut.get("isrc"),
78
+ musicbrainz_artist_ids=listf(mut.get("musicbrainz_artistid")),
79
+ musicbrainz_release_track_id=mut.get(
80
+ "musicbrainz_releasetrackid", [None]
81
+ )[0],
82
+ musicbrainz_track_id=mut.get("musicbrainz_trackid", [None])[0],
83
+ title=mut.get("title", [None])[0],
84
+ track_number=track_number,
85
+ )
86
+ }
87
+ if track_number
88
+ else None,
89
+ )
90
+ }
91
+ if medium_number
92
+ else None,
93
+ medium_count=medium_count,
94
+ musicbrainz_album_artist_ids=listf(mut.get("musicbrainz_albumartistid")),
95
+ musicbrainz_album_id=mut.get("musicbrainz_albumid", [None])[0],
96
+ musicbrainz_release_group_id=mut.get("musicbrainz_releasegroupid", [None])[0],
97
+ original_date=mut.get("originaldate", [None])[0],
98
+ original_year=mut["originalyear"][0] if mut.get("originalyear") else None,
99
+ people=(
100
+ records.People(
101
+ arrangers=mut.get("arranger"),
102
+ composers=mut.get("composer"),
103
+ conductors=mut.get("conductor"),
104
+ engineers=mut.get("engineer"),
105
+ lyricists=mut.get("lyricist"),
106
+ mixers=mut.get("mixer"),
107
+ performers=mut.get("performer")
108
+ and self._parse_performer_tag(mut["performer"]),
109
+ producers=mut.get("producer"),
110
+ writers=mut.get("writer"),
111
+ )
112
+ or None
113
+ ),
114
+ release_countries=mut.get("releasecountry"),
115
+ release_statuses=mut.get("releasestatus"),
116
+ release_types=mut.get("releasetype"),
117
+ script=mut.get("script", [None])[0],
118
+ )
119
+ or None
120
+ )
121
+ if release:
122
+ release.source = records.Source.TAGS
123
+ return records.OneTrack(
124
+ release=release, medium_number=medium_number, track_number=track_number
125
+ )
126
+
127
+ def write_tags(self) -> None:
128
+ """Write the tags."""
129
+ release, medium_number, medium, track_number, track = self._get_tag_sources()
130
+ tags_ = {
131
+ "album": [release.album],
132
+ "albumartist": release.album_artists,
133
+ "albumartistsort": release.album_artists_sort,
134
+ "arranger": release.people and release.people.arrangers,
135
+ "artist": [track.artist],
136
+ "artists": track.artists,
137
+ "artistsort": track.artists_sort,
138
+ "asin": release.asins,
139
+ "barcode": release.barcodes,
140
+ "catalognumber": release.catalog_numbers,
141
+ "composer": release.people and release.people.composers,
142
+ "conductor": release.people and release.people.conductors,
143
+ "date": [release.date],
144
+ "discnumber": [str(medium_number)],
145
+ "discsubtitle": medium.titles,
146
+ "disctotal": [str(release.medium_count)],
147
+ "engineer": release.people and release.people.engineers,
148
+ "genre": release.genres,
149
+ "isrc": track.isrcs,
150
+ "label": release.labels,
151
+ "lyricist": release.people and release.people.lyricists,
152
+ "media": medium.formats,
153
+ "mixer": release.people and release.people.mixers,
154
+ "musicbrainz_albumartistid": release.musicbrainz_album_artist_ids,
155
+ "musicbrainz_albumid": [release.musicbrainz_album_id],
156
+ "musicbrainz_artistid": track.musicbrainz_artist_ids,
157
+ "musicbrainz_releasegroupid": [release.musicbrainz_release_group_id],
158
+ "musicbrainz_releasetrackid": [track.musicbrainz_release_track_id],
159
+ "musicbrainz_trackid": [track.musicbrainz_track_id],
160
+ "originaldate": [release.original_date],
161
+ "originalyear": [str(release.original_year)],
162
+ "performer": self._make_performer_tag(release.people and release.people.performers),
163
+ "producer": release.people and release.people.producers,
164
+ "releasecountry": release.release_countries,
165
+ "releasestatus": release.release_statuses,
166
+ "releasetype": release.release_types,
167
+ "script": [release.script],
168
+ "title": [track.title],
169
+ "totaldiscs": [str(release.medium_count)],
170
+ "totaltracks": [str(medium.track_count)],
171
+ "tracknumber": [str(track_number)],
172
+ "tracktotal": [str(medium.track_count)],
173
+ "writer": release.people and release.people.writers,
174
+ }
175
+ tags_ = audiofile.Tags(tags_)
176
+ self._mut_file.delete() # Clear old tags.
177
+ self._mut_file.clear_pictures()
178
+ self._mut_file.update(tags_)
179
+
180
+ if release.front_cover is not None:
181
+ cover = mutagen.flac.Picture() # type: ignore[no-untyped-call]
182
+ cover.type = 3
183
+ cover.mime = release.front_cover.mime
184
+ cover.desc = release.front_cover.desc or ""
185
+ cover.data = release.front_cover.data
186
+ self._mut_file.add_picture(cover)
187
+
188
+ self._mut_file.save()
189
+
190
+ @staticmethod
191
+ def _make_performer_tag(performers: list[records.Performer] | None | Any) -> list[str] | None: # noqa: ANN401
192
+ # Return a list of performer tag strings "name (instrument)".
193
+ if performers is None:
194
+ return None
195
+ return [f"{p.name} ({p.instrument})" for p in performers]
196
+
197
+ @staticmethod
198
+ def _parse_performer_tag(performers_tag: list[str]) -> list[records.Performer]:
199
+ # Parse a list of performer tags and return a list of Performer objects.
200
+ performer_re = re.compile(r"(?P<name>.*)\((?P<instrument>.*)\)")
201
+ performers = []
202
+ for tag in performers_tag:
203
+ if match := performer_re.match(tag):
204
+ name = match.groupdict()["name"].strip()
205
+ instrument = match.groupdict()["instrument"].strip()
206
+ performers.append(records.Performer(name=name, instrument=instrument))
207
+ return performers
@@ -0,0 +1,221 @@
1
+ """AudioFile support for m4a files."""
2
+
3
+ #
4
+ # Copyright (c) 2020 Stephen Jibson
5
+ #
6
+ # This file is part of audiolibrarian.
7
+ #
8
+ # Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
9
+ # GNU General Public License as published by the Free Software Foundation, either version 3 of the
10
+ # License, or (at your option) any later version.
11
+ #
12
+ # Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
13
+ # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
14
+ # the GNU General Public License for more details.
15
+ #
16
+ # You should have received a copy of the GNU General Public License along with audiolibrarian.
17
+ # If not, see <https://www.gnu.org/licenses/>.
18
+ #
19
+ from logging import getLogger
20
+ from typing import Any
21
+
22
+ import mutagen.mp4
23
+
24
+ from audiolibrarian import audiofile, records
25
+
26
+ log = getLogger(__name__)
27
+ ITUNES = "----:com.apple.iTunes"
28
+
29
+
30
+ class M4aFile(audiofile.AudioFile, extensions={".m4a"}):
31
+ """AudioFile for M4A files."""
32
+
33
+ def read_tags(self) -> records.OneTrack:
34
+ """Read the tags and return a OneTrack object."""
35
+
36
+ def get_str(key: str) -> str | None:
37
+ # Return first element for the given key, utf8-decoded.
38
+ if mut.get(key) is None:
39
+ return None
40
+ return str(mut.get(key)[0].decode("utf8"))
41
+
42
+ def get_strl(key: str) -> records.ListF | None:
43
+ # Return all elements for a given key, utf8-decoded.
44
+ if mut.get(key) is None:
45
+ return None
46
+ return records.ListF([x.decode("utf8") for x in mut.get(key)])
47
+
48
+ def listf(key: str) -> records.ListF | None:
49
+ # Return a ListF object for a given key.
50
+ if mut.get(key) is None:
51
+ return None
52
+ return records.ListF(mut.get(key))
53
+
54
+ mut = self._mut_file
55
+
56
+ front_cover = None
57
+ if mut.get("covr"):
58
+ cover = mut["covr"][0]
59
+ # noinspection PyUnresolvedReferences
60
+ mime = (
61
+ "image/png" if cover.imageformat == mutagen.mp4.AtomDataType.PNG else "image/jpg"
62
+ )
63
+ front_cover = records.FrontCover(data=bytes(cover), mime=mime)
64
+ medium_count = int(mut["disk"][0][1]) if mut.get("disk") else None
65
+ medium_number = int(mut["disk"][0][0]) if mut.get("disk") else None
66
+ track_count = int(mut["trkn"][0][1]) if mut.get("trkn") else None
67
+ track_number = int(mut["trkn"][0][0]) if mut.get("trkn") else None
68
+ release = (
69
+ records.Release(
70
+ album=mut.get("\xa9alb", [None])[0],
71
+ album_artists=listf("aART"),
72
+ album_artists_sort=listf("soaa"),
73
+ asins=get_strl(f"{ITUNES}:ASIN"),
74
+ barcodes=get_strl(f"{ITUNES}:BARCODE"),
75
+ catalog_numbers=get_strl(f"{ITUNES}:CATALOGNUMBER"),
76
+ date=mut.get("\xa9day", [None])[0],
77
+ front_cover=front_cover,
78
+ genres=listf("\xa9gen"),
79
+ labels=get_strl(f"{ITUNES}:LABEL"),
80
+ media={
81
+ medium_number: records.Medium(
82
+ formats=get_strl(f"{ITUNES}:MEDIA"),
83
+ titles=get_strl(f"{ITUNES}:DISCSUBTITLE"),
84
+ track_count=track_count,
85
+ tracks={
86
+ track_number: records.Track(
87
+ artist=mut.get("\xa9ART", [None])[0],
88
+ artists=get_strl(f"{ITUNES}:ARTISTS"),
89
+ artists_sort=mut.get("soar"),
90
+ file_info=records.FileInfo(
91
+ bitrate=mut.info.bitrate // 1000,
92
+ bitrate_mode=records.BitrateMode.CBR,
93
+ path=self.filepath,
94
+ type=records.FileType.AAC,
95
+ ),
96
+ isrcs=get_strl(f"{ITUNES}:ISRC"),
97
+ musicbrainz_artist_ids=get_strl(f"{ITUNES}:MusicBrainz Artist Id"),
98
+ musicbrainz_release_track_id=get_str(
99
+ f"{ITUNES}:MusicBrainz Release Track Id"
100
+ ),
101
+ musicbrainz_track_id=get_str(f"{ITUNES}:MusicBrainz Track Id"),
102
+ title=mut.get("\xa9nam", [None])[0],
103
+ track_number=int(mut["trkn"][0][0]) if mut.get("trkn") else None,
104
+ )
105
+ }
106
+ if track_number
107
+ else None,
108
+ )
109
+ }
110
+ if medium_number
111
+ else None,
112
+ medium_count=medium_count,
113
+ musicbrainz_album_artist_ids=get_strl(f"{ITUNES}:MusicBrainz Album Artist Id"),
114
+ musicbrainz_album_id=get_str(f"{ITUNES}:MusicBrainz Album Id"),
115
+ musicbrainz_release_group_id=get_str(f"{ITUNES}:MusicBrainz Release Group Id"),
116
+ original_date=get_str(f"{ITUNES}:originaldate"),
117
+ original_year=get_str(f"{ITUNES}:originalyear") or None,
118
+ people=(
119
+ records.People(
120
+ arrangers=get_strl(f"{ITUNES}:ARRANGER"),
121
+ composers=get_strl(f"{ITUNES}:COMPOSER"),
122
+ conductors=get_strl(f"{ITUNES}:CONDUCTOR"),
123
+ engineers=get_strl(f"{ITUNES}:ENGINEER"),
124
+ lyricists=get_strl(f"{ITUNES}:LYRICIST"),
125
+ mixers=get_strl(f"{ITUNES}:MIXER"),
126
+ producers=get_strl(f"{ITUNES}:PRODUCER"),
127
+ performers=None,
128
+ writers=get_strl(f"{ITUNES}:WRITER"),
129
+ )
130
+ or None
131
+ ),
132
+ release_countries=get_strl(f"{ITUNES}:MusicBrainz Album Release Country"),
133
+ release_statuses=get_strl(f"{ITUNES}:MusicBrainz Album Status"),
134
+ release_types=get_strl(f"{ITUNES}:MusicBrainz Album Type"),
135
+ script=get_str(f"{ITUNES}:SCRIPT"),
136
+ )
137
+ or None
138
+ )
139
+ if release:
140
+ release.source = records.Source.TAGS
141
+ return records.OneTrack(
142
+ release=release, medium_number=medium_number, track_number=track_number
143
+ )
144
+
145
+ def write_tags(self) -> None:
146
+ """Write the tags."""
147
+
148
+ def ff(text: int | str | None) -> bytes | None:
149
+ if text is None:
150
+ return None
151
+ return mutagen.mp4.MP4FreeForm(bytes(str(text), "utf8")) # type: ignore[no-untyped-call]
152
+
153
+ def ffl(list_: list[str] | None | Any) -> records.ListF | None: # noqa: ANN401
154
+ if not list_:
155
+ return None
156
+ return records.ListF([ff(x) for x in list_])
157
+
158
+ # Note: We don't write "performers" to m4a files.
159
+ release, medium_number, medium, track_number, track = self._get_tag_sources()
160
+ front_cover = None
161
+ if (cover := release.front_cover) is not None:
162
+ # noinspection PyUnresolvedReferences
163
+ image_format = (
164
+ mutagen.mp4.AtomDataType.PNG
165
+ if cover.mime == "image/png"
166
+ else mutagen.mp4.AtomDataType.JPEG
167
+ )
168
+ front_cover = [
169
+ mutagen.mp4.MP4Cover(cover.data, imageformat=image_format) # type: ignore[no-untyped-call]
170
+ ]
171
+ tags_ = {
172
+ f"{ITUNES}:ARRANGER": ffl(release.people and release.people.arrangers),
173
+ f"{ITUNES}:ARTISTS": ffl(track.artists),
174
+ f"{ITUNES}:ASIN": ffl(release.asins),
175
+ f"{ITUNES}:BARCODE": ffl(release.barcodes),
176
+ f"{ITUNES}:CATALOGNUMBER": ffl(release.catalog_numbers),
177
+ f"{ITUNES}:COMPOSER": ffl(release.people and release.people.composers),
178
+ f"{ITUNES}:CONDUCTOR": ffl(release.people and release.people.conductors),
179
+ f"{ITUNES}:DISCSUBTITLE": ffl(medium.titles),
180
+ f"{ITUNES}:ENGINEER": ffl(release.people and release.people.engineers),
181
+ f"{ITUNES}:ISRC": ffl(track.isrcs),
182
+ f"{ITUNES}:LABEL": ffl(release.labels),
183
+ f"{ITUNES}:LYRICIST": ffl(release.people and release.people.lyricists),
184
+ f"{ITUNES}:MEDIA": ffl(medium.formats),
185
+ f"{ITUNES}:MIXER": ffl(release.people and release.people.mixers),
186
+ f"{ITUNES}:MusicBrainz Album Artist Id": ffl(release.musicbrainz_album_artist_ids),
187
+ f"{ITUNES}:MusicBrainz Album Id": [ff(release.musicbrainz_album_id)],
188
+ f"{ITUNES}:MusicBrainz Album Release Country": ffl(release.release_countries),
189
+ f"{ITUNES}:MusicBrainz Album Status": ffl(release.release_statuses),
190
+ f"{ITUNES}:MusicBrainz Album Type": ffl(release.release_types),
191
+ f"{ITUNES}:MusicBrainz Artist Id": ffl(track.musicbrainz_artist_ids),
192
+ f"{ITUNES}:MusicBrainz Release Group Id": [ff(release.musicbrainz_release_group_id)],
193
+ f"{ITUNES}:MusicBrainz Release Track Id": [ff(track.musicbrainz_release_track_id)],
194
+ f"{ITUNES}:MusicBrainz Track Id": [ff(track.musicbrainz_track_id)],
195
+ f"{ITUNES}:originaldate": [ff(release.original_date)],
196
+ f"{ITUNES}:originalyear": [ff(release.original_year)],
197
+ f"{ITUNES}:PRODUCER": ffl(release.people and release.people.producers),
198
+ f"{ITUNES}:SCRIPT": [ff(release.script)],
199
+ f"{ITUNES}:WRITER": ffl(release.people and release.people.writers),
200
+ "\xa9alb": [release.album],
201
+ "\xa9ART": [track.artist],
202
+ "\xa9day": [release.date],
203
+ "\xa9gen": release.genres,
204
+ "\xa9nam": [track.title],
205
+ "aART": release.album_artists,
206
+ "covr": front_cover,
207
+ "disk": [(medium_number, release.medium_count)] if medium_number else None,
208
+ "soaa": release.album_artists_sort,
209
+ "soar": track.artists_sort,
210
+ "trkn": [(track_number, medium.track_count)] if track_number else None,
211
+ }
212
+ tags_ = audiofile.Tags(tags_)
213
+
214
+ for key, value in tags_.items():
215
+ try:
216
+ self._mut_file[key] = value
217
+ except Exception: # pragma: no cover
218
+ log.critical("ERROR: %s %s", key, value)
219
+ raise
220
+
221
+ self._mut_file.save()