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,283 @@
|
|
1
|
+
"""Command line commands."""
|
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 argparse
|
20
|
+
import logging
|
21
|
+
import pathlib
|
22
|
+
import re
|
23
|
+
from typing import Any
|
24
|
+
|
25
|
+
from audiolibrarian import __version__, audiofile, audiosource, base, genremanager
|
26
|
+
|
27
|
+
log = logging.getLogger(__name__)
|
28
|
+
|
29
|
+
|
30
|
+
class _Command:
|
31
|
+
# Base class for commands.
|
32
|
+
help = ""
|
33
|
+
parser = argparse.ArgumentParser()
|
34
|
+
|
35
|
+
@staticmethod
|
36
|
+
def validate_args(args: argparse.Namespace) -> bool:
|
37
|
+
"""Validate command line arguments."""
|
38
|
+
_ = args
|
39
|
+
return True
|
40
|
+
|
41
|
+
|
42
|
+
class Convert(_Command, base.Base):
|
43
|
+
"""AudioLibrarian tool for converting and tagging audio files.
|
44
|
+
|
45
|
+
This class performs all of its tasks on instantiation and provides no public members or
|
46
|
+
methods.
|
47
|
+
"""
|
48
|
+
|
49
|
+
command = "convert"
|
50
|
+
help = "convert music from files"
|
51
|
+
parser = argparse.ArgumentParser()
|
52
|
+
parser.add_argument("--artist", "-a", help="provide artist (ignore tags)")
|
53
|
+
parser.add_argument("--album", "-m", help="provide album (ignore tags)")
|
54
|
+
parser.add_argument("--mb-artist-id", help="Musicbrainz artist ID")
|
55
|
+
parser.add_argument("--mb-release-id", help="Musicbrainz release ID")
|
56
|
+
parser.add_argument("--disc", "-d", help="format: x/y: disc x of y for multi-disc release")
|
57
|
+
parser.add_argument("filename", nargs="+", help="directory name or audio file name")
|
58
|
+
|
59
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
60
|
+
"""Initialize a Convert command handler."""
|
61
|
+
super().__init__(args)
|
62
|
+
self._source_is_cd = False
|
63
|
+
self._audio_source = audiosource.FilesAudioSource([pathlib.Path(x) for x in args.filename])
|
64
|
+
self._get_tag_info()
|
65
|
+
self._convert()
|
66
|
+
self._write_manifest()
|
67
|
+
|
68
|
+
@staticmethod
|
69
|
+
def validate_args(args: argparse.Namespace) -> bool:
|
70
|
+
"""Validate command line arguments."""
|
71
|
+
return _validate_disc_arg(args)
|
72
|
+
|
73
|
+
|
74
|
+
class Genre(_Command):
|
75
|
+
"""Do stuff with genres."""
|
76
|
+
|
77
|
+
command = "genre"
|
78
|
+
help = "manage MB genre"
|
79
|
+
parser = argparse.ArgumentParser(
|
80
|
+
description=(
|
81
|
+
"Process all audio files in the given directory(ies), allowing the user to *update* "
|
82
|
+
"the genre in Musicbrainz or *tag* audio files with the user-defined genre in "
|
83
|
+
"Musicbrainz."
|
84
|
+
)
|
85
|
+
)
|
86
|
+
parser.add_argument("directory", nargs="+", help="root of directory tree to process")
|
87
|
+
parser_action = parser.add_mutually_exclusive_group()
|
88
|
+
parser_action.add_argument("--tag", action="store_true", help="update tags")
|
89
|
+
parser_action.add_argument("--update", action="store_true", help="update Musicbrainz")
|
90
|
+
|
91
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
92
|
+
"""Initialize a Genre command handler."""
|
93
|
+
genremanager.GenreManager(args)
|
94
|
+
|
95
|
+
|
96
|
+
class Manifest(_Command, base.Base):
|
97
|
+
"""AudioLibrarian tool for writing manifest.yaml files.
|
98
|
+
|
99
|
+
This class performs all of its tasks on instantiation and provides no public members or
|
100
|
+
methods.
|
101
|
+
"""
|
102
|
+
|
103
|
+
command = "manifest"
|
104
|
+
help = "create the Manifest.yaml file"
|
105
|
+
parser = argparse.ArgumentParser()
|
106
|
+
parser.add_argument("--artist", "-a", help="provide artist (ignore tags)")
|
107
|
+
parser.add_argument("--album", "-m", help="provide album (ignore tags)")
|
108
|
+
parser.add_argument("--cd", "-c", action="store_true", help="original source was a CD")
|
109
|
+
parser.add_argument("--mb-artist-id", help="Musicbrainz artist ID")
|
110
|
+
parser.add_argument("--mb-release-id", help="Musicbrainz release ID")
|
111
|
+
parser.add_argument("--disc", "-d", help="format: x/y: disc x of y for multi-disc release")
|
112
|
+
parser.add_argument("filename", nargs="+", help="directory name or audio file name")
|
113
|
+
|
114
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
115
|
+
"""Initialize a Manifest command handler."""
|
116
|
+
super().__init__(args)
|
117
|
+
self._source_is_cd = args.cd
|
118
|
+
self._audio_source = audiosource.FilesAudioSource([pathlib.Path(x) for x in args.filename])
|
119
|
+
source_filenames = self._audio_source.get_source_filenames()
|
120
|
+
self._source_example = audiofile.AudioFile.open(source_filenames[0]).read_tags()
|
121
|
+
self._get_tag_info()
|
122
|
+
self._write_manifest()
|
123
|
+
|
124
|
+
@staticmethod
|
125
|
+
def validate_args(args: argparse.Namespace) -> bool:
|
126
|
+
"""Validate command line arguments."""
|
127
|
+
return _validate_disc_arg(args)
|
128
|
+
|
129
|
+
|
130
|
+
class Reconvert(_Command, base.Base):
|
131
|
+
"""AudioLibrarian tool for re-converting and tagging audio files from existing source files.
|
132
|
+
|
133
|
+
This class performs all of its tasks on instantiation and provides no public members or
|
134
|
+
methods.
|
135
|
+
"""
|
136
|
+
|
137
|
+
command = "reconvert"
|
138
|
+
help = "re-convert files from an existing source directory"
|
139
|
+
parser = argparse.ArgumentParser()
|
140
|
+
parser.add_argument("directories", nargs="+", help="source directories")
|
141
|
+
|
142
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
143
|
+
"""Initialize a Reconvert command handler."""
|
144
|
+
super().__init__(args)
|
145
|
+
self._source_is_cd = False
|
146
|
+
manifest_paths = self._find_manifests(args.directories)
|
147
|
+
count = len(manifest_paths)
|
148
|
+
for i, manifest_path in enumerate(manifest_paths):
|
149
|
+
print(f"Processing {i + 1} of {count} ({i / count:.0%}): {manifest_path}...")
|
150
|
+
self._audio_source = audiosource.FilesAudioSource([manifest_path.parent])
|
151
|
+
manifest = self._read_manifest(manifest_path)
|
152
|
+
self._disc_number, self._disc_count = manifest["disc_number"], manifest["disc_count"]
|
153
|
+
self._get_tag_info()
|
154
|
+
self._convert(make_source=False)
|
155
|
+
|
156
|
+
@staticmethod
|
157
|
+
def validate_args(args: argparse.Namespace) -> bool:
|
158
|
+
"""Validate command line arguments."""
|
159
|
+
return _validate_directories_arg(args)
|
160
|
+
|
161
|
+
|
162
|
+
class Rename(_Command, base.Base):
|
163
|
+
"""AudioLibrarian tool for renaming audio files based on their tags.
|
164
|
+
|
165
|
+
This class performs all of its tasks on instantiation and provides no public members or
|
166
|
+
methods.
|
167
|
+
"""
|
168
|
+
|
169
|
+
command = "rename"
|
170
|
+
help = "rename files based on tags or MusicBrainz data"
|
171
|
+
parser = argparse.ArgumentParser()
|
172
|
+
parser.add_argument("--dry-run", action="store_true", help="don't actually rename files")
|
173
|
+
parser.add_argument("directories", nargs="+", help="audio file directories")
|
174
|
+
|
175
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
176
|
+
"""Initialize a Rename command handler."""
|
177
|
+
super().__init__(args)
|
178
|
+
self._source_is_cd = False
|
179
|
+
print("Finding audio files...")
|
180
|
+
for audio_file in self._find_audio_files(args.directories):
|
181
|
+
filepath = audio_file.filepath
|
182
|
+
if audio_file.one_track.track is None:
|
183
|
+
log.warning("%s has no title", filepath)
|
184
|
+
continue
|
185
|
+
depth = 3 if filepath.parent.name.startswith("disc") else 2
|
186
|
+
old_name = filepath
|
187
|
+
new_name = (
|
188
|
+
filepath.parents[depth]
|
189
|
+
/ audio_file.one_track.get_artist_album_disc_path()
|
190
|
+
/ audio_file.one_track.track.get_filename(filepath.suffix)
|
191
|
+
)
|
192
|
+
if old_name != new_name:
|
193
|
+
print(f"Renaming:\n {old_name} -> \n {new_name}")
|
194
|
+
if args.dry_run:
|
195
|
+
continue
|
196
|
+
old_parent = old_name.parent
|
197
|
+
new_parent = new_name.parent
|
198
|
+
new_parent.mkdir(parents=True, exist_ok=True)
|
199
|
+
old_name.rename(new_name)
|
200
|
+
if not old_parent.samefile(new_parent):
|
201
|
+
# Move the Manifest if it's the only file left.
|
202
|
+
man = "Manifest.yaml"
|
203
|
+
if [f.name for f in old_parent.glob("*")] == [man]:
|
204
|
+
print(f"Renaming:\n {old_parent / man} -> \n {new_parent / man}")
|
205
|
+
(old_parent / man).rename(new_parent / man)
|
206
|
+
for idx in range(depth):
|
207
|
+
if not list(old_name.parents[idx].glob("*")):
|
208
|
+
print(f"Removing: {old_name.parents[idx]}")
|
209
|
+
old_name.parents[idx].rmdir()
|
210
|
+
|
211
|
+
else:
|
212
|
+
log.debug("Not renaming %s", filepath)
|
213
|
+
|
214
|
+
@staticmethod
|
215
|
+
def validate_args(args: argparse.Namespace) -> bool:
|
216
|
+
"""Validate command line arguments."""
|
217
|
+
return _validate_directories_arg(args)
|
218
|
+
|
219
|
+
|
220
|
+
class Rip(_Command, base.Base):
|
221
|
+
"""AudioLibrarian tool for ripping, converting and tagging audio files.
|
222
|
+
|
223
|
+
This class performs all of its tasks on instantiation and provides no public members or
|
224
|
+
methods.
|
225
|
+
"""
|
226
|
+
|
227
|
+
command = "rip"
|
228
|
+
help = "rip music from a CD"
|
229
|
+
parser = argparse.ArgumentParser()
|
230
|
+
parser.add_argument("--artist", "-a", help="provide artist")
|
231
|
+
parser.add_argument("--album", "-m", help="provide album")
|
232
|
+
parser.add_argument("--mb-artist-id", help="Musicbrainz artist ID")
|
233
|
+
parser.add_argument("--mb-release-id", help="Musicbrainz release ID")
|
234
|
+
parser.add_argument("--disc", "-d", help="x/y: disc x of y; multi-disc release")
|
235
|
+
|
236
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
237
|
+
"""Initialize a Rip command handler."""
|
238
|
+
super().__init__(args)
|
239
|
+
self._source_is_cd = True
|
240
|
+
self._audio_source = audiosource.CDAudioSource()
|
241
|
+
self._get_tag_info()
|
242
|
+
self._convert()
|
243
|
+
self._write_manifest()
|
244
|
+
|
245
|
+
@staticmethod
|
246
|
+
def validate_args(args: argparse.Namespace) -> bool:
|
247
|
+
"""Validate command line arguments."""
|
248
|
+
return _validate_disc_arg(args)
|
249
|
+
|
250
|
+
|
251
|
+
class Version(_Command):
|
252
|
+
"""Print the version."""
|
253
|
+
|
254
|
+
command = "version"
|
255
|
+
help = "display the program version"
|
256
|
+
|
257
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
258
|
+
"""Initialize a Version command handler."""
|
259
|
+
_ = args
|
260
|
+
print(f"audiolibrarian {__version__}")
|
261
|
+
|
262
|
+
|
263
|
+
def _validate_directories_arg(args: argparse.Namespace) -> bool:
|
264
|
+
for directory in args.directories:
|
265
|
+
if not pathlib.Path(directory).is_dir():
|
266
|
+
print(f"Directory not found: {directory}")
|
267
|
+
return False
|
268
|
+
return True
|
269
|
+
|
270
|
+
|
271
|
+
def _validate_disc_arg(args: argparse.Namespace) -> bool:
|
272
|
+
if "disc" in args and args.disc:
|
273
|
+
if not re.match(r"\d+/\d+", args.disc):
|
274
|
+
print("Invalid --disc specification; should be 'x/y'")
|
275
|
+
return False
|
276
|
+
x, y = args.disc.split("/")
|
277
|
+
if int(x) > int(y) or int(x) < 1 or int(y) < 1:
|
278
|
+
print("Invalid --disc specification; should be 'x/y' where x <= y and x and y >= 1")
|
279
|
+
return False
|
280
|
+
return True
|
281
|
+
|
282
|
+
|
283
|
+
COMMANDS: set[Any] = {Convert, Genre, Manifest, Reconvert, Rename, Rip, Version}
|
@@ -0,0 +1,176 @@
|
|
1
|
+
"""Genre Manager."""
|
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 argparse
|
20
|
+
import logging
|
21
|
+
import pathlib
|
22
|
+
import pickle
|
23
|
+
import typing
|
24
|
+
import webbrowser
|
25
|
+
from typing import Any
|
26
|
+
|
27
|
+
import mutagen
|
28
|
+
import mutagen.flac
|
29
|
+
import mutagen.id3
|
30
|
+
import mutagen.mp4
|
31
|
+
|
32
|
+
from audiolibrarian import musicbrainz, text
|
33
|
+
from audiolibrarian.settings import SETTINGS
|
34
|
+
|
35
|
+
log = logging.getLogger(__name__)
|
36
|
+
|
37
|
+
|
38
|
+
class GenreManager:
|
39
|
+
"""Manage genres."""
|
40
|
+
|
41
|
+
def __init__(self, args: argparse.Namespace) -> None:
|
42
|
+
"""Initialize a GenreManager instance."""
|
43
|
+
self._args = args
|
44
|
+
self._mb = musicbrainz.MusicBrainzSession()
|
45
|
+
self._paths = self._get_all_paths()
|
46
|
+
self._paths_by_artist = self._get_paths_by_artist()
|
47
|
+
_u, _c = self._get_genres_by_artist()
|
48
|
+
self._user_genres_by_artist, self._community_genres_by_artist = _u, _c
|
49
|
+
if self._args.update:
|
50
|
+
self._update_user_artists()
|
51
|
+
elif self._args.tag:
|
52
|
+
self._update_tags() # type: ignore[no-untyped-call]
|
53
|
+
|
54
|
+
@typing.no_type_check # The mutagen library doesn't provide type hints.
|
55
|
+
def _update_tags(self) -> None:
|
56
|
+
"""Set the genre tags for all songs to the user-based genre."""
|
57
|
+
for artist_id, paths in self._paths_by_artist.items():
|
58
|
+
genre = self._user_genres_by_artist.get(artist_id)
|
59
|
+
if not genre:
|
60
|
+
continue
|
61
|
+
for path in paths:
|
62
|
+
if path.suffix == ".flac":
|
63
|
+
flac_song = mutagen.flac.FLAC(str(path))
|
64
|
+
current_genre = flac_song.tags["genre"][0]
|
65
|
+
if current_genre != genre:
|
66
|
+
flac_song.tags["genre"] = genre
|
67
|
+
flac_song.save()
|
68
|
+
print(f"{path}: {current_genre} --> {genre}")
|
69
|
+
elif path.suffix == ".m4a":
|
70
|
+
m4a_song = mutagen.mp4.MP4(str(path))
|
71
|
+
current_genre = m4a_song.tags["\xa9gen"][0]
|
72
|
+
if current_genre != genre:
|
73
|
+
m4a_song.tags["\xa9gen"] = genre
|
74
|
+
m4a_song.save()
|
75
|
+
print(f"{path}: {current_genre} --> {genre}")
|
76
|
+
elif path.suffix == ".mp3":
|
77
|
+
mp3_song = mutagen.File(str(path))
|
78
|
+
current_genre = str(mp3_song.tags["TCON"])
|
79
|
+
if current_genre != genre:
|
80
|
+
id3 = mutagen.id3.ID3(str(path))
|
81
|
+
id3.add(mutagen.id3.TCON(encoding=3, text=genre))
|
82
|
+
id3.save()
|
83
|
+
print(f"{path}: {current_genre} --> {genre}")
|
84
|
+
|
85
|
+
def _update_user_artists(self) -> None:
|
86
|
+
"""Pull up a web page to allow the user to set the genre.
|
87
|
+
|
88
|
+
Update the genre for all artists that only have community-base genres.
|
89
|
+
"""
|
90
|
+
for artist_id, artist in sorted(
|
91
|
+
self._community_genres_by_artist.items(),
|
92
|
+
key=lambda x: x[1]["name"],
|
93
|
+
):
|
94
|
+
artist_name = artist["name"]
|
95
|
+
i = (
|
96
|
+
text.input_(f"Continue with {artist_name} (YES, no, skip)[Y, n, s]: ")
|
97
|
+
.lower()
|
98
|
+
.strip()
|
99
|
+
)
|
100
|
+
if i == "n":
|
101
|
+
break
|
102
|
+
if i != "s":
|
103
|
+
webbrowser.open(f"https://musicbrainz.org/artist/{artist_id}/tags")
|
104
|
+
|
105
|
+
def _get_all_paths(self) -> list[pathlib.Path]:
|
106
|
+
"""Return a list of paths for all files in the directories specified in the args."""
|
107
|
+
paths = []
|
108
|
+
for directory in self._args.directory:
|
109
|
+
paths.extend([p for p in list(pathlib.Path(directory).glob("**/*")) if p.is_file()])
|
110
|
+
return paths
|
111
|
+
|
112
|
+
def _get_paths_by_artist(self) -> dict[str, list[pathlib.Path]]:
|
113
|
+
"""Return a map of artist-IDs to paths representing audio files by that artist."""
|
114
|
+
artists: dict[str, list[pathlib.Path]] = {}
|
115
|
+
for path in self._paths:
|
116
|
+
artist_id = None
|
117
|
+
song = mutagen.File(str(path))
|
118
|
+
if path.suffix == ".flac":
|
119
|
+
artist_id = str(
|
120
|
+
song.tags.get("musicbrainz_albumartistid", [""])[0]
|
121
|
+
or song.tags.get("musicbrainz_artistid", [""])[0]
|
122
|
+
)
|
123
|
+
elif path.suffix == ".m4a":
|
124
|
+
artist_id = (
|
125
|
+
song.tags.get("----:com.apple.iTunes:MusicBrainz Album Artist Id", [""])[0]
|
126
|
+
or song.tags.get("----:com.apple.iTunes:MusicBrainz Artist Id", [""])[0]
|
127
|
+
).decode("utf8")
|
128
|
+
elif path.suffix == ".mp3":
|
129
|
+
artist_id = str(
|
130
|
+
song.tags.get("TXXX:MusicBrainz Album Artist Id", [""])[0]
|
131
|
+
or song.tags.get("TXXX:MusicBrainz Artist Id", [""])[0]
|
132
|
+
)
|
133
|
+
if artist_id:
|
134
|
+
if artist_id not in artists:
|
135
|
+
artists[artist_id] = []
|
136
|
+
artists[artist_id].append(path)
|
137
|
+
return artists
|
138
|
+
|
139
|
+
def _get_genres_by_artist(
|
140
|
+
self,
|
141
|
+
) -> tuple[dict[str, str], dict[str, dict[str, Any]]]:
|
142
|
+
"""Return two dicts mapping Musicbrainz-artist-ID to user and community.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
user: a single genre, set in Musicbrainz by this app's user
|
146
|
+
community: a list genre records (dicts) set in Musicbrainz by the community
|
147
|
+
with "name" and "count" fields
|
148
|
+
"""
|
149
|
+
user: dict[str, str] = {}
|
150
|
+
community: dict[str, dict[str, Any]] = {}
|
151
|
+
user_modified = False
|
152
|
+
cache_file = SETTINGS.work_dir / "user-genres.pickle"
|
153
|
+
if cache_file.exists():
|
154
|
+
with cache_file.open(mode="rb") as cache_file_obj:
|
155
|
+
user = pickle.load(cache_file_obj) # noqa: S301
|
156
|
+
for artist_id in self._paths_by_artist:
|
157
|
+
if artist_id in user:
|
158
|
+
log.debug("Cache hit: %s %s", artist_id, user[artist_id])
|
159
|
+
continue # Already in the cache.
|
160
|
+
artist = self._mb.get_artist_by_id(artist_id, includes=["genres", "user-genres"])
|
161
|
+
if artist["user-genres"]:
|
162
|
+
genre = artist["user-genres"][0]["name"].title()
|
163
|
+
user[artist_id] = genre
|
164
|
+
user_modified = True
|
165
|
+
elif artist["genres"]:
|
166
|
+
community[artist_id] = {
|
167
|
+
"name": artist["name"],
|
168
|
+
"genres": [{"name": x["name"], "count": x["count"]} for x in artist["genres"]],
|
169
|
+
}
|
170
|
+
if user_modified:
|
171
|
+
cache_file.parent.mkdir(exist_ok=True)
|
172
|
+
with cache_file.open(mode="wb") as cache_file_obj:
|
173
|
+
# noinspection PyTypeChecker
|
174
|
+
pickle.dump(user, cache_file_obj)
|
175
|
+
|
176
|
+
return user, community
|