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.
- audiolibrarian/__init__.py +19 -0
- audiolibrarian/audiofile/__init__.py +23 -0
- audiolibrarian/audiofile/audiofile.py +114 -0
- audiolibrarian/audiofile/formats/__init__.py +1 -0
- audiolibrarian/audiofile/formats/flac.py +207 -0
- audiolibrarian/audiofile/formats/m4a.py +221 -0
- audiolibrarian/audiofile/formats/mp3.py +259 -0
- audiolibrarian/audiofile/tags.py +48 -0
- audiolibrarian/audiosource.py +215 -0
- audiolibrarian/base.py +433 -0
- audiolibrarian/cli.py +123 -0
- audiolibrarian/commands.py +283 -0
- audiolibrarian/genremanager.py +176 -0
- audiolibrarian/musicbrainz.py +465 -0
- audiolibrarian/output.py +57 -0
- audiolibrarian/records.py +259 -0
- audiolibrarian/settings.py +79 -0
- audiolibrarian/sh.py +55 -0
- audiolibrarian/text.py +115 -0
- audiolibrarian-0.16.2.dist-info/METADATA +334 -0
- audiolibrarian-0.16.2.dist-info/RECORD +28 -0
- audiolibrarian-0.16.2.dist-info/WHEEL +4 -0
- audiolibrarian-0.16.2.dist-info/entry_points.txt +2 -0
- audiolibrarian-0.16.2.dist-info/licenses/COPYING +674 -0
- audiolibrarian-0.16.2.dist-info/licenses/LICENSE +674 -0
- picard_src/README.md +11 -0
- picard_src/__init__.py +7 -0
- picard_src/textencoding.py +495 -0
@@ -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()
|