audiolibrarian 0.16.4__py3-none-any.whl → 0.17.0__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 +2 -2
- audiolibrarian/audiofile/__init__.py +1 -2
- audiolibrarian/audiofile/audiofile.py +1 -1
- audiolibrarian/audiofile/formats/__init__.py +16 -0
- audiolibrarian/audiofile/formats/flac.py +1 -1
- audiolibrarian/audiofile/formats/m4a.py +1 -1
- audiolibrarian/audiofile/formats/mp3.py +1 -1
- audiolibrarian/audiofile/tags.py +1 -1
- audiolibrarian/audiosource.py +3 -3
- audiolibrarian/base.py +86 -39
- audiolibrarian/cli.py +1 -2
- audiolibrarian/commands.py +10 -10
- audiolibrarian/genremanager.py +4 -4
- audiolibrarian/musicbrainz.py +2 -2
- audiolibrarian/output.py +1 -1
- audiolibrarian/records.py +1 -1
- audiolibrarian/settings.py +53 -18
- audiolibrarian/sh.py +1 -1
- audiolibrarian/text.py +1 -1
- audiolibrarian-0.17.0.dist-info/METADATA +74 -0
- audiolibrarian-0.17.0.dist-info/RECORD +28 -0
- audiolibrarian-0.16.4.dist-info/METADATA +0 -333
- audiolibrarian-0.16.4.dist-info/RECORD +0 -28
- {audiolibrarian-0.16.4.dist-info → audiolibrarian-0.17.0.dist-info}/WHEEL +0 -0
- {audiolibrarian-0.16.4.dist-info → audiolibrarian-0.17.0.dist-info}/entry_points.txt +0 -0
- {audiolibrarian-0.16.4.dist-info → audiolibrarian-0.17.0.dist-info}/licenses/COPYING +0 -0
- {audiolibrarian-0.16.4.dist-info → audiolibrarian-0.17.0.dist-info}/licenses/LICENSE +0 -0
audiolibrarian/__init__.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""The audiolibrarian package."""
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
5
5
|
#
|
6
6
|
# This file is part of audiolibrarian.
|
7
7
|
#
|
@@ -16,4 +16,4 @@
|
|
16
16
|
# You should have received a copy of the GNU General Public License along with audiolibrarian.
|
17
17
|
# If not, see <https://www.gnu.org/licenses/>.
|
18
18
|
#
|
19
|
-
__version__ = "0.
|
19
|
+
__version__ = "0.17.0"
|
@@ -1 +1,17 @@
|
|
1
1
|
"""AudioFile formats."""
|
2
|
+
#
|
3
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
4
|
+
#
|
5
|
+
# This file is part of audiolibrarian.
|
6
|
+
#
|
7
|
+
# Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
|
8
|
+
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
9
|
+
# License, or (at your option) any later version.
|
10
|
+
#
|
11
|
+
# Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
12
|
+
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
13
|
+
# the GNU General Public License for more details.
|
14
|
+
#
|
15
|
+
# You should have received a copy of the GNU General Public License along with audiolibrarian.
|
16
|
+
# If not, see <https://www.gnu.org/licenses/>.
|
17
|
+
#
|
audiolibrarian/audiofile/tags.py
CHANGED
audiolibrarian/audiosource.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""AudioSource."""
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
5
5
|
#
|
6
6
|
# This file is part of audiolibrarian.
|
7
7
|
#
|
@@ -118,10 +118,10 @@ class CDAudioSource(AudioSource):
|
|
118
118
|
cwd = pathlib.Path.cwd()
|
119
119
|
os.chdir(self._temp_dir)
|
120
120
|
try:
|
121
|
-
subprocess.run(("cd-paranoia", "-B"), check=True)
|
121
|
+
subprocess.run(("cd-paranoia", "-B"), check=True)
|
122
122
|
finally:
|
123
123
|
os.chdir(cwd)
|
124
|
-
subprocess.run(("eject",), check=False)
|
124
|
+
subprocess.run(("eject",), check=False)
|
125
125
|
|
126
126
|
|
127
127
|
class FilesAudioSource(AudioSource):
|
audiolibrarian/base.py
CHANGED
@@ -4,7 +4,7 @@ Useful stuff: https://help.mp3tag.de/main_tags.html
|
|
4
4
|
"""
|
5
5
|
|
6
6
|
#
|
7
|
-
# Copyright (c)
|
7
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
8
8
|
#
|
9
9
|
# This file is part of audiolibrarian.
|
10
10
|
#
|
@@ -27,9 +27,10 @@ import subprocess
|
|
27
27
|
import sys
|
28
28
|
import warnings
|
29
29
|
from collections.abc import Iterable
|
30
|
-
from typing import Any
|
30
|
+
from typing import Any, Final
|
31
31
|
|
32
32
|
import colors
|
33
|
+
import ffmpeg_normalize
|
33
34
|
import filelock
|
34
35
|
import yaml
|
35
36
|
|
@@ -46,6 +47,7 @@ class Base:
|
|
46
47
|
"""
|
47
48
|
|
48
49
|
command: str | None = None
|
50
|
+
_manifest_file: Final[str] = "Manifest.yaml"
|
49
51
|
|
50
52
|
def __init__(self, args: argparse.Namespace) -> None:
|
51
53
|
"""Initialize the base."""
|
@@ -66,9 +68,10 @@ class Base:
|
|
66
68
|
self._source_dir = self._work_dir / "source"
|
67
69
|
self._wav_dir = self._work_dir / "wav"
|
68
70
|
|
69
|
-
self._manifest_file = "Manifest.yaml"
|
70
71
|
self._lock = filelock.FileLock(str(self._work_dir) + ".lock")
|
71
72
|
|
73
|
+
self._normalizer = self._which_normalizer()
|
74
|
+
|
72
75
|
# Initialize stuff that will be defined later.
|
73
76
|
self._audio_source: audiosource.AudioSource | None = None
|
74
77
|
self._release: records.Release | None = None
|
@@ -126,24 +129,6 @@ class Base:
|
|
126
129
|
self._make_mp3()
|
127
130
|
self._move_files(move_source=make_source)
|
128
131
|
|
129
|
-
@staticmethod
|
130
|
-
def _find_audio_files(directories: list[str | pathlib.Path]) -> Iterable[audiofile.AudioFile]:
|
131
|
-
"""Yield audiofile objects found in the given directories."""
|
132
|
-
paths: list[pathlib.Path] = []
|
133
|
-
# Grab all the paths first because thing may change as files are renamed.
|
134
|
-
for directory in directories:
|
135
|
-
path = pathlib.Path(directory)
|
136
|
-
for ext in audiofile.AudioFile.extensions():
|
137
|
-
paths.extend(path.rglob(f"*{ext}"))
|
138
|
-
paths = sorted(set(paths))
|
139
|
-
# Using yield rather than returning a list saves us from simultaneously storing
|
140
|
-
# potentially thousands of AudioFile objects in memory at the same time.
|
141
|
-
for path in paths:
|
142
|
-
try:
|
143
|
-
yield audiofile.AudioFile.open(path)
|
144
|
-
except FileNotFoundError:
|
145
|
-
continue
|
146
|
-
|
147
132
|
def _find_manifests(self, directories: list[str | pathlib.Path]) -> list[pathlib.Path]:
|
148
133
|
"""Return a sorted, unique list of manifest files anywhere in the given directories."""
|
149
134
|
manifests = set()
|
@@ -283,25 +268,36 @@ class Base:
|
|
283
268
|
path.rename(source_dir / path.name)
|
284
269
|
|
285
270
|
def _normalize(self) -> None:
|
286
|
-
"""Normalize the wav files using
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
271
|
+
"""Normalize the wav files using the selected normalizer."""
|
272
|
+
if self._normalizer == "none":
|
273
|
+
return
|
274
|
+
print(f"Normalizing wav files using {self._normalizer}...")
|
275
|
+
|
276
|
+
if self._normalizer == "wavegain":
|
277
|
+
command = [
|
278
|
+
"wavegain",
|
279
|
+
f"--{SETTINGS.normalize.wavegain.preset}",
|
280
|
+
f"--gain={SETTINGS.normalize.wavegain.gain}",
|
281
|
+
"--apply",
|
282
|
+
]
|
283
|
+
command.extend(str(f) for f in self._wav_filenames)
|
284
|
+
result = subprocess.run(command, capture_output=True, check=False) # noqa: S603
|
285
|
+
for line in str(result.stderr).split(r"\n"):
|
286
|
+
line_trunc = line[:137] + "..." if len(line) > 140 else line # noqa: PLR2004
|
287
|
+
log.info("WAVEGAIN: %s", line_trunc)
|
288
|
+
result.check_returncode()
|
289
|
+
return
|
300
290
|
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
291
|
+
normalizer = ffmpeg_normalize.FFmpegNormalize(
|
292
|
+
extension="wav",
|
293
|
+
keep_loudness_range_target=True,
|
294
|
+
target_level=SETTINGS.normalize.ffmpeg.target_level,
|
295
|
+
)
|
296
|
+
for wav_file in self._wav_filenames:
|
297
|
+
normalizer.add_media_file(str(wav_file), str(wav_file))
|
298
|
+
log.info("NORMALIZER: starting ffmpeg normalization...")
|
299
|
+
normalizer.run_normalization()
|
300
|
+
log.info("NORMALIZER: ffmpeg normalization completed successfully")
|
305
301
|
|
306
302
|
def _rename_wav(self) -> None:
|
307
303
|
"""Rename the wav files to a filename-sane representation of the track title."""
|
@@ -431,3 +427,54 @@ class Base:
|
|
431
427
|
with pathlib.Path(manifest_filename).open("w", encoding="utf-8") as manifest_file:
|
432
428
|
yaml.dump(manifest, manifest_file)
|
433
429
|
print(f"Wrote {manifest_filename}")
|
430
|
+
|
431
|
+
@staticmethod
|
432
|
+
def _which_normalizer() -> str:
|
433
|
+
"""Determine which normalizer to use based on settings and availability.
|
434
|
+
|
435
|
+
Returns:
|
436
|
+
str: The name of the normalizer to use ("wavegain", "ffmpeg" or "none")
|
437
|
+
"""
|
438
|
+
normalizer = SETTINGS.normalize.normalizer
|
439
|
+
if normalizer == "none":
|
440
|
+
return "none"
|
441
|
+
|
442
|
+
wavegain_found = shutil.which("wavegain")
|
443
|
+
if normalizer in ("auto", "wavegain") and wavegain_found:
|
444
|
+
return "wavegain"
|
445
|
+
|
446
|
+
ffmpeg_found = shutil.which("ffmpeg")
|
447
|
+
if normalizer in ("auto", "ffmpeg") and ffmpeg_found:
|
448
|
+
return "ffmpeg"
|
449
|
+
|
450
|
+
if wavegain_found:
|
451
|
+
log.warning("ffmpeg not found, using wavegain for normalization")
|
452
|
+
return "wavegain"
|
453
|
+
if ffmpeg_found:
|
454
|
+
log.warning("wavegain not found, using ffmpeg for normalization")
|
455
|
+
return "ffmpeg"
|
456
|
+
log.warning("wavegain not found, ffmpeg not found, using no normalization")
|
457
|
+
return "none"
|
458
|
+
|
459
|
+
@staticmethod
|
460
|
+
def _find_audio_files(directories: list[str | pathlib.Path]) -> Iterable[audiofile.AudioFile]:
|
461
|
+
"""Yield audiofile objects found in the given directories."""
|
462
|
+
paths: list[pathlib.Path] = []
|
463
|
+
# Grab all the paths first because thing may change as files are renamed.
|
464
|
+
for directory in directories:
|
465
|
+
path = pathlib.Path(directory)
|
466
|
+
for ext in audiofile.AudioFile.extensions():
|
467
|
+
paths.extend(path.rglob(f"*{ext}"))
|
468
|
+
paths = sorted(set(paths))
|
469
|
+
# Using yield rather than returning a list saves us from simultaneously storing
|
470
|
+
# potentially thousands of AudioFile objects in memory at the same time.
|
471
|
+
for path in paths:
|
472
|
+
try:
|
473
|
+
yield audiofile.AudioFile.open(path)
|
474
|
+
except FileNotFoundError:
|
475
|
+
continue
|
476
|
+
|
477
|
+
@staticmethod
|
478
|
+
def _read_manifest(manifest_path: pathlib.Path) -> dict[Any, Any]:
|
479
|
+
with manifest_path.open(encoding="utf-8") as manifest_file:
|
480
|
+
return dict(yaml.safe_load(manifest_file))
|
audiolibrarian/cli.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Audiolibrarian command line interface."""
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
5
5
|
#
|
6
6
|
# This file is part of audiolibrarian.
|
7
7
|
#
|
@@ -40,7 +40,6 @@ class CommandLineInterface:
|
|
40
40
|
"lame",
|
41
41
|
"mpg123",
|
42
42
|
"sndfile-convert",
|
43
|
-
"wavegain",
|
44
43
|
}
|
45
44
|
|
46
45
|
def __init__(self, *, parse_args: bool = True) -> None:
|
audiolibrarian/commands.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Command line commands."""
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
5
5
|
#
|
6
6
|
# This file is part of audiolibrarian.
|
7
7
|
#
|
@@ -51,8 +51,8 @@ class Convert(_Command, base.Base):
|
|
51
51
|
parser = argparse.ArgumentParser()
|
52
52
|
parser.add_argument("--artist", "-a", help="provide artist (ignore tags)")
|
53
53
|
parser.add_argument("--album", "-m", help="provide album (ignore tags)")
|
54
|
-
parser.add_argument("--mb-artist-id", help="
|
55
|
-
parser.add_argument("--mb-release-id", help="
|
54
|
+
parser.add_argument("--mb-artist-id", help="MusicBrainz artist ID")
|
55
|
+
parser.add_argument("--mb-release-id", help="MusicBrainz release ID")
|
56
56
|
parser.add_argument("--disc", "-d", help="format: x/y: disc x of y for multi-disc release")
|
57
57
|
parser.add_argument("filename", nargs="+", help="directory name or audio file name")
|
58
58
|
|
@@ -79,14 +79,14 @@ class Genre(_Command):
|
|
79
79
|
parser = argparse.ArgumentParser(
|
80
80
|
description=(
|
81
81
|
"Process all audio files in the given directory(ies), allowing the user to *update* "
|
82
|
-
"the genre in
|
83
|
-
"
|
82
|
+
"the genre in MusicBrainz or *tag* audio files with the user-defined genre in "
|
83
|
+
"MusicBrainz."
|
84
84
|
)
|
85
85
|
)
|
86
86
|
parser.add_argument("directory", nargs="+", help="root of directory tree to process")
|
87
87
|
parser_action = parser.add_mutually_exclusive_group()
|
88
88
|
parser_action.add_argument("--tag", action="store_true", help="update tags")
|
89
|
-
parser_action.add_argument("--update", action="store_true", help="update
|
89
|
+
parser_action.add_argument("--update", action="store_true", help="update MusicBrainz")
|
90
90
|
|
91
91
|
def __init__(self, args: argparse.Namespace) -> None:
|
92
92
|
"""Initialize a Genre command handler."""
|
@@ -106,8 +106,8 @@ class Manifest(_Command, base.Base):
|
|
106
106
|
parser.add_argument("--artist", "-a", help="provide artist (ignore tags)")
|
107
107
|
parser.add_argument("--album", "-m", help="provide album (ignore tags)")
|
108
108
|
parser.add_argument("--cd", "-c", action="store_true", help="original source was a CD")
|
109
|
-
parser.add_argument("--mb-artist-id", help="
|
110
|
-
parser.add_argument("--mb-release-id", help="
|
109
|
+
parser.add_argument("--mb-artist-id", help="MusicBrainz artist ID")
|
110
|
+
parser.add_argument("--mb-release-id", help="MusicBrainz release ID")
|
111
111
|
parser.add_argument("--disc", "-d", help="format: x/y: disc x of y for multi-disc release")
|
112
112
|
parser.add_argument("filename", nargs="+", help="directory name or audio file name")
|
113
113
|
|
@@ -229,8 +229,8 @@ class Rip(_Command, base.Base):
|
|
229
229
|
parser = argparse.ArgumentParser()
|
230
230
|
parser.add_argument("--artist", "-a", help="provide artist")
|
231
231
|
parser.add_argument("--album", "-m", help="provide album")
|
232
|
-
parser.add_argument("--mb-artist-id", help="
|
233
|
-
parser.add_argument("--mb-release-id", help="
|
232
|
+
parser.add_argument("--mb-artist-id", help="MusicBrainz artist ID")
|
233
|
+
parser.add_argument("--mb-release-id", help="MusicBrainz release ID")
|
234
234
|
parser.add_argument("--disc", "-d", help="x/y: disc x of y; multi-disc release")
|
235
235
|
|
236
236
|
def __init__(self, args: argparse.Namespace) -> None:
|
audiolibrarian/genremanager.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Genre Manager."""
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
5
5
|
#
|
6
6
|
# This file is part of audiolibrarian.
|
7
7
|
#
|
@@ -139,11 +139,11 @@ class GenreManager:
|
|
139
139
|
def _get_genres_by_artist(
|
140
140
|
self,
|
141
141
|
) -> tuple[dict[str, str], dict[str, dict[str, Any]]]:
|
142
|
-
"""Return two dicts mapping
|
142
|
+
"""Return two dicts mapping MusicBrainz-artist-ID to user and community.
|
143
143
|
|
144
144
|
Returns:
|
145
|
-
user: a single genre, set in
|
146
|
-
community: a list genre records (dicts) set in
|
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
147
|
with "name" and "count" fields
|
148
148
|
"""
|
149
149
|
user: dict[str, str] = {}
|
audiolibrarian/musicbrainz.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
"""Access the MusicBrainz service."""
|
2
2
|
|
3
3
|
#
|
4
|
-
# Copyright (c)
|
4
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
5
5
|
#
|
6
6
|
# This file is part of audiolibrarian.
|
7
7
|
#
|
@@ -417,7 +417,7 @@ class Searcher:
|
|
417
417
|
log.info("RELEASE_GROUPS: {release_group_ids}")
|
418
418
|
release_id = self._prompt_release_id(release_group_ids)
|
419
419
|
else:
|
420
|
-
release_id = self._prompt_uuid("
|
420
|
+
release_id = self._prompt_uuid("MusicBrainz Release ID: ")
|
421
421
|
|
422
422
|
return MusicBrainzRelease(release_id).get_release()
|
423
423
|
|
audiolibrarian/output.py
CHANGED
audiolibrarian/records.py
CHANGED
audiolibrarian/settings.py
CHANGED
@@ -21,23 +21,34 @@ configuration and cache locations.
|
|
21
21
|
Sensitive fields (like passwords) are handled using pydantic.SecretStr for security.
|
22
22
|
"""
|
23
23
|
|
24
|
+
#
|
25
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
26
|
+
#
|
27
|
+
# This file is part of audiolibrarian.
|
28
|
+
#
|
29
|
+
# Audiolibrarian is free software: you can redistribute it and/or modify it under the terms of the
|
30
|
+
# GNU General Public License as published by the Free Software Foundation, either version 3 of the
|
31
|
+
# License, or (at your option) any later version.
|
32
|
+
#
|
33
|
+
# Audiolibrarian is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
|
34
|
+
# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See
|
35
|
+
# the GNU General Public License for more details.
|
36
|
+
#
|
37
|
+
# You should have received a copy of the GNU General Public License along with audiolibrarian.
|
38
|
+
# If not, see <https://www.gnu.org/licenses/>.
|
39
|
+
#
|
24
40
|
import logging
|
25
41
|
import pathlib
|
26
42
|
from typing import Literal
|
27
43
|
|
28
44
|
import pydantic
|
45
|
+
import pydantic_settings
|
29
46
|
import xdg_base_dirs
|
30
|
-
from pydantic_settings import (
|
31
|
-
BaseSettings,
|
32
|
-
PydanticBaseSettingsSource,
|
33
|
-
SettingsConfigDict,
|
34
|
-
YamlConfigSettingsSource,
|
35
|
-
)
|
36
47
|
|
37
48
|
logger = logging.getLogger(__name__)
|
38
49
|
|
39
50
|
|
40
|
-
class MusicBrainzSettings(BaseSettings):
|
51
|
+
class MusicBrainzSettings(pydantic_settings.BaseSettings):
|
41
52
|
"""Configuration settings for MusicBrainz."""
|
42
53
|
|
43
54
|
password: pydantic.SecretStr = pydantic.SecretStr("")
|
@@ -45,17 +56,37 @@ class MusicBrainzSettings(BaseSettings):
|
|
45
56
|
rate_limit: pydantic.PositiveFloat = 1.5 # Seconds between requests.
|
46
57
|
|
47
58
|
|
48
|
-
class
|
59
|
+
class NormalizeFFmpegSettings(pydantic_settings.BaseSettings):
|
60
|
+
"""Configuration settings for ffmpeg normalization."""
|
61
|
+
|
62
|
+
target_level: float = -13
|
63
|
+
|
64
|
+
|
65
|
+
class NormalizeWavegainSettings(pydantic_settings.BaseSettings):
|
66
|
+
"""Configuration settings for wavegain normalization."""
|
67
|
+
|
68
|
+
gain: int = 5 # dB
|
69
|
+
preset: Literal["album", "radio"] = "radio"
|
70
|
+
|
71
|
+
|
72
|
+
class NormalizeSettings(pydantic_settings.BaseSettings):
|
73
|
+
"""Configuration settings for audio normalization."""
|
74
|
+
|
75
|
+
normalizer: Literal["auto", "wavegain", "ffmpeg", "none"] = "auto"
|
76
|
+
ffmpeg: NormalizeFFmpegSettings = NormalizeFFmpegSettings()
|
77
|
+
wavegain: NormalizeWavegainSettings = NormalizeWavegainSettings()
|
78
|
+
|
79
|
+
|
80
|
+
class Settings(pydantic_settings.BaseSettings):
|
49
81
|
"""Configuration settings for AudioLibrarian."""
|
50
82
|
|
51
83
|
discid_device: str | None = None # Use default device.
|
52
84
|
library_dir: pathlib.Path = pathlib.Path("library").resolve()
|
53
85
|
musicbrainz: MusicBrainzSettings = MusicBrainzSettings()
|
54
|
-
|
55
|
-
normalize_preset: Literal["album", "radio"] = "radio"
|
86
|
+
normalize: NormalizeSettings = NormalizeSettings()
|
56
87
|
work_dir: pathlib.Path = xdg_base_dirs.xdg_cache_home() / "audiolibrarian"
|
57
88
|
|
58
|
-
model_config = SettingsConfigDict(
|
89
|
+
model_config = pydantic_settings.SettingsConfigDict(
|
59
90
|
env_nested_delimiter="__",
|
60
91
|
env_prefix="AUDIOLIBRARIAN__",
|
61
92
|
yaml_file=str(xdg_base_dirs.xdg_config_home() / "audiolibrarian" / "config.yaml"),
|
@@ -65,14 +96,18 @@ class Settings(BaseSettings):
|
|
65
96
|
@classmethod
|
66
97
|
def settings_customise_sources(
|
67
98
|
cls,
|
68
|
-
settings_cls: type[BaseSettings],
|
69
|
-
init_settings: PydanticBaseSettingsSource,
|
70
|
-
env_settings: PydanticBaseSettingsSource,
|
71
|
-
dotenv_settings: PydanticBaseSettingsSource,
|
72
|
-
file_secret_settings: PydanticBaseSettingsSource,
|
73
|
-
) -> tuple[PydanticBaseSettingsSource, ...]:
|
99
|
+
settings_cls: type[pydantic_settings.BaseSettings],
|
100
|
+
init_settings: pydantic_settings.PydanticBaseSettingsSource,
|
101
|
+
env_settings: pydantic_settings.PydanticBaseSettingsSource,
|
102
|
+
dotenv_settings: pydantic_settings.PydanticBaseSettingsSource,
|
103
|
+
file_secret_settings: pydantic_settings.PydanticBaseSettingsSource,
|
104
|
+
) -> tuple[pydantic_settings.PydanticBaseSettingsSource, ...]:
|
74
105
|
del dotenv_settings, file_secret_settings # Unused.
|
75
|
-
return
|
106
|
+
return (
|
107
|
+
env_settings,
|
108
|
+
pydantic_settings.YamlConfigSettingsSource(settings_cls),
|
109
|
+
init_settings,
|
110
|
+
)
|
76
111
|
|
77
112
|
|
78
113
|
SETTINGS = Settings()
|
audiolibrarian/sh.py
CHANGED
audiolibrarian/text.py
CHANGED
@@ -0,0 +1,74 @@
|
|
1
|
+
Metadata-Version: 2.4
|
2
|
+
Name: audiolibrarian
|
3
|
+
Version: 0.17.0
|
4
|
+
Summary: Manage my audio library.
|
5
|
+
Project-URL: Repository, https://github.com/toadstule/audiolibrarian
|
6
|
+
Author-email: Steve Jibson <steve@jibson.com>
|
7
|
+
License-File: COPYING
|
8
|
+
License-File: LICENSE
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
10
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
11
|
+
Classifier: Programming Language :: Python
|
12
|
+
Requires-Python: <3.14,>=3.12
|
13
|
+
Requires-Dist: ansicolors
|
14
|
+
Requires-Dist: discid
|
15
|
+
Requires-Dist: ffmpeg-normalize
|
16
|
+
Requires-Dist: filelock
|
17
|
+
Requires-Dist: fuzzywuzzy
|
18
|
+
Requires-Dist: musicbrainzngs
|
19
|
+
Requires-Dist: mutagen
|
20
|
+
Requires-Dist: pydantic-settings
|
21
|
+
Requires-Dist: python-levenshtein
|
22
|
+
Requires-Dist: pyyaml
|
23
|
+
Requires-Dist: requests
|
24
|
+
Requires-Dist: xdg-base-dirs
|
25
|
+
Description-Content-Type: text/markdown
|
26
|
+
|
27
|
+
# audiolibrarian
|
28
|
+
|
29
|
+
[](https://pypi.org/project/audiolibrarian/)
|
30
|
+
[](https://www.gnu.org/licenses/gpl-3.0)
|
31
|
+
[](https://audiolibrarian.readthedocs.io/)
|
32
|
+
|
33
|
+
`audiolibrarian` is a command-line tool for ripping audio from CDs (or taking
|
34
|
+
high-quality audio from local files), tagging them with comprehensive metadata from MusicBrainz,
|
35
|
+
converting them to multiple formats, and organizing them in a clean directory structure.
|
36
|
+
|
37
|
+
## Features
|
38
|
+
|
39
|
+
- **CD Ripping**: Extract audio from CDs with accurate metadata lookup
|
40
|
+
- **Audio Conversion**: Convert between multiple audio formats (FLAC, M4A, MP3)
|
41
|
+
- **Metadata Management**: Automatically fetch and apply rich metadata from MusicBrainz
|
42
|
+
- **File Organization**: Intelligently organize music files into a clean directory structure
|
43
|
+
- **Batch Processing**: Handle multiple files and directories efficiently
|
44
|
+
|
45
|
+
## Basic Usage
|
46
|
+
|
47
|
+
```bash
|
48
|
+
# Rip audio from a CD
|
49
|
+
audiolibrarian rip
|
50
|
+
|
51
|
+
# Convert audio files
|
52
|
+
audiolibrarian convert /path/to/audio/files
|
53
|
+
|
54
|
+
# Get help
|
55
|
+
audiolibrarian --help
|
56
|
+
```
|
57
|
+
|
58
|
+
## Documentation
|
59
|
+
|
60
|
+
For complete documentation, including installation instructions, configuration, and advanced usage, visit:
|
61
|
+
|
62
|
+
[https://audiolibrarian.readthedocs.io/](https://audiolibrarian.readthedocs.io/)
|
63
|
+
|
64
|
+
## License
|
65
|
+
|
66
|
+
This project is licensed under the GPL-3.0 License - see the [LICENSE](LICENSE) file for details.
|
67
|
+
|
68
|
+
## Contributing
|
69
|
+
|
70
|
+
Contributions are welcome! Please see our [contributing guide](CONTRIBUTING.md) for details.
|
71
|
+
|
72
|
+
## Support
|
73
|
+
|
74
|
+
For support, please [open an issue](https://github.com/toadstule/audiolibrarian/issues) on GitHub.
|
@@ -0,0 +1,28 @@
|
|
1
|
+
audiolibrarian/__init__.py,sha256=nk7MHHw3booS4scifS56NhceSDNsLnU7vcljwNcLcZ0,792
|
2
|
+
audiolibrarian/audiosource.py,sha256=Xs8KBvb0Jd-JRHv3veyBA9lL2siC70-gO2RdZqy2ntk,8550
|
3
|
+
audiolibrarian/base.py,sha256=yDN2PELNboUr_eD-Ix8xi-JHhq8XbDuaj4AnvergSIE,21263
|
4
|
+
audiolibrarian/cli.py,sha256=vtAJND7U5yoBCbm20HRCvIFp2HYzXl3jfHNfsNUQVlE,4196
|
5
|
+
audiolibrarian/commands.py,sha256=rM44NQUKtZsBI3C8EY0u0vRrricfj3felM00x16fBeU,11171
|
6
|
+
audiolibrarian/genremanager.py,sha256=ag4LGcXPHjuI-Y07DTmGjVcaUKyKazyWHGBak9YzVAg,7362
|
7
|
+
audiolibrarian/musicbrainz.py,sha256=hoZKDueqgfMo-Da_ZH3_bQcRXqRvaz9KCx9MARMI5h8,19317
|
8
|
+
audiolibrarian/output.py,sha256=MQGsKrOfkRASGkVG4CEi2kgj0smEm8TfXqAR-lwULwk,1707
|
9
|
+
audiolibrarian/records.py,sha256=87DXbpwzl8Fsh71Nzi5XmJDAaEGvRjH6vwJ75cIM9PQ,7934
|
10
|
+
audiolibrarian/settings.py,sha256=GSuT3zOa6jyrejx14_dLwJaTA6eCTNYFq6EQ9UQOzhI,4042
|
11
|
+
audiolibrarian/sh.py,sha256=7V-FrSSiq56cNL1IyYBEcSUXtMbwFs6Ob1EhU_i_c84,1917
|
12
|
+
audiolibrarian/text.py,sha256=VrxrQm6pZtuYbK9MlukSC3MdqDBSlsnTgkhBTwj9MpY,4031
|
13
|
+
audiolibrarian/audiofile/__init__.py,sha256=PTBAVV5gfSc0UVvWMHyeL1d3xwhgJ8J00L-KVhuCGhY,898
|
14
|
+
audiolibrarian/audiofile/audiofile.py,sha256=UCVXu2ga7stnTzeeLzrx-JTFl5ad82qy45HmB3g2a8c,4357
|
15
|
+
audiolibrarian/audiofile/tags.py,sha256=Di8TjvOYZYYgedMNRZ8tTGFxZ1KN4oAK7z17TzTSeZg,1790
|
16
|
+
audiolibrarian/audiofile/formats/__init__.py,sha256=DexI0KdI6hnbDhvK9xuEFvYyyQA2SOFT_xfadc42Nsk,759
|
17
|
+
audiolibrarian/audiofile/formats/flac.py,sha256=UNXfZbnL0cjzmlS3nuZ26QczrfD-twgc7S6PbtoOkqA,9713
|
18
|
+
audiolibrarian/audiofile/formats/m4a.py,sha256=Ki-pHnObeEMXegIAHu7ymtB_PjMiWLDU5HHXaWxaPPo,10557
|
19
|
+
audiolibrarian/audiofile/formats/mp3.py,sha256=SQOcPVAE-coDz4tK-gopWSWJE02nQBTbxrlmRkyK7QU,12886
|
20
|
+
picard_src/README.md,sha256=mtJ7RNLlC7Oz9M038WU-3ciPa7jPdXlFLYdJBL8iRQo,411
|
21
|
+
picard_src/__init__.py,sha256=acu0-oac_qfEgiB0rw0RvuL---O15-rOckWboVMxWtM,198
|
22
|
+
picard_src/textencoding.py,sha256=0MRHFwhqEwauQbjTTz6gpgzo6YH1VDPfdJqQoCWHtjM,26234
|
23
|
+
audiolibrarian-0.17.0.dist-info/METADATA,sha256=CvHJpVXiHY01H6rQ828Nu_a4Yzr-KXrXWXh5BgOl-X0,2578
|
24
|
+
audiolibrarian-0.17.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
+
audiolibrarian-0.17.0.dist-info/entry_points.txt,sha256=reubnr_SGbTTDXji8j7z8aTmIL0AEQKVSLcnmFG3YYY,59
|
26
|
+
audiolibrarian-0.17.0.dist-info/licenses/COPYING,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
|
27
|
+
audiolibrarian-0.17.0.dist-info/licenses/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
|
28
|
+
audiolibrarian-0.17.0.dist-info/RECORD,,
|
@@ -1,333 +0,0 @@
|
|
1
|
-
Metadata-Version: 2.4
|
2
|
-
Name: audiolibrarian
|
3
|
-
Version: 0.16.4
|
4
|
-
Summary: Manage my audio library.
|
5
|
-
Project-URL: Repository, https://github.com/toadstule/audiolibrarian
|
6
|
-
Author-email: Steve Jibson <steve@jibson.com>
|
7
|
-
License-File: COPYING
|
8
|
-
License-File: LICENSE
|
9
|
-
Classifier: Development Status :: 4 - Beta
|
10
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
11
|
-
Classifier: Programming Language :: Python
|
12
|
-
Requires-Python: ==3.13.*
|
13
|
-
Requires-Dist: ansicolors
|
14
|
-
Requires-Dist: discid
|
15
|
-
Requires-Dist: filelock
|
16
|
-
Requires-Dist: fuzzywuzzy
|
17
|
-
Requires-Dist: musicbrainzngs
|
18
|
-
Requires-Dist: mutagen
|
19
|
-
Requires-Dist: pydantic-settings
|
20
|
-
Requires-Dist: python-levenshtein
|
21
|
-
Requires-Dist: pyyaml
|
22
|
-
Requires-Dist: requests
|
23
|
-
Requires-Dist: xdg-base-dirs
|
24
|
-
Description-Content-Type: text/markdown
|
25
|
-
|
26
|
-
# audiolibrarian #
|
27
|
-
|
28
|
-
## Overview ##
|
29
|
-
|
30
|
-
`audiolibrarian` is a command-line tool for ripping audio from CDs (or taking
|
31
|
-
high-quality audio from local files), tagging them with comprehensive metadata from MusicBrainz,
|
32
|
-
converting them to multiple formats, and organizing them in a clean directory structure.
|
33
|
-
|
34
|
-
### Key Features ###
|
35
|
-
|
36
|
-
- **CD Ripping**: Extract audio from CDs with accurate metadata lookup
|
37
|
-
- **Audio Conversion**: Convert between multiple audio formats (FLAC, M4A, MP3)
|
38
|
-
- **Metadata Management**: Automatically fetch and apply rich metadata from MusicBrainz
|
39
|
-
- **File Organization**: Intelligently organize music files into a clean directory structure
|
40
|
-
- **Batch Processing**: Handle multiple files and directories efficiently
|
41
|
-
- **Genre Management**: Work with MusicBrainz genres and tags
|
42
|
-
- **Flexible Configuration**: Customize behavior through config files and environment variables
|
43
|
-
|
44
|
-
### Why audiolibrarian? ###
|
45
|
-
|
46
|
-
- **Consistent Quality**: Maintains audio quality through the conversion process
|
47
|
-
- **Accurate Metadata**: Leverages MusicBrainz for comprehensive music information
|
48
|
-
- **Automated Workflow**: Reduces manual work in organizing and tagging music
|
49
|
-
- **Open Source**: Free to use and modify under the GPL-3.0 license
|
50
|
-
|
51
|
-
Whether you're digitizing a CD collection, organizing existing music files, or managing a large
|
52
|
-
digital library, `audiolibrarian` provides the tools you need to keep your music collection
|
53
|
-
well-organized and properly tagged.
|
54
|
-
|
55
|
-
## Installation ##
|
56
|
-
|
57
|
-
> **NOTE:** This library has only been tested on Linux. It may not work on other operating
|
58
|
-
> systems.
|
59
|
-
|
60
|
-
### External Requirements ###
|
61
|
-
|
62
|
-
`audiolibrarian` uses a few command-line tools to run:
|
63
|
-
|
64
|
-
- [cd-paranoia](https://www.gnu.org/software/libcdio/)
|
65
|
-
- [util-linux](https://github.com/util-linux/util-linux)
|
66
|
-
- [faad2](https://github.com/knik0/faad2)
|
67
|
-
- [fdkaac](https://github.com/nu774/fdkaac)
|
68
|
-
- [flac](https://github.com/xiph/flac)
|
69
|
-
- [lame](https://lame.sourceforge.io/)
|
70
|
-
- [mpg123](https://www.mpg123.de/)
|
71
|
-
- [libsndfile](https://github.com/libsndfile/libsndfile)
|
72
|
-
- [wavegain](https://github.com/MestreLion/wavegain)
|
73
|
-
|
74
|
-
It also requires the [libdiscid](https://musicbrainz.org/doc/libdiscid) library.
|
75
|
-
|
76
|
-
### Install from PyPI ###
|
77
|
-
|
78
|
-
`audiolibrarian` is available on PyPI:
|
79
|
-
|
80
|
-
```bash
|
81
|
-
pip install audiolibrarian
|
82
|
-
```
|
83
|
-
|
84
|
-
## Configuration ##
|
85
|
-
|
86
|
-
`audiolibrarian` uses a flexible configuration system that supports multiple configuration sources,
|
87
|
-
listed in order of precedence:
|
88
|
-
|
89
|
-
1. **Environment Variables** (highest precedence)
|
90
|
-
- Prefix: `AUDIOLIBRARIAN__`
|
91
|
-
- Nested fields: Use `__` as delimiter (e.g., `AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME`)
|
92
|
-
- Example:
|
93
|
-
|
94
|
-
```bash
|
95
|
-
# Override library directory (library_dir)
|
96
|
-
export AUDIOLIBRARIAN__LIBRARY_DIR="/mnt/music/library"
|
97
|
-
|
98
|
-
# Set MusicBrainz credentials (musicbrainz.username and musicbrainz.password)
|
99
|
-
export AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME="your_username"
|
100
|
-
export AUDIOLIBRARIAN__MUSICBRAINZ__PASSWORD="your_password"
|
101
|
-
```
|
102
|
-
|
103
|
-
2. **YAML Configuration File** (medium precedence)
|
104
|
-
- Default location: `~/.config/audiolibrarian/config.yaml`
|
105
|
-
- Example:
|
106
|
-
|
107
|
-
```yaml
|
108
|
-
# Base directory for your music library
|
109
|
-
library_dir: "~/music/library"
|
110
|
-
|
111
|
-
# Cache and working directory
|
112
|
-
work_dir: "~/.cache/audiolibrarian"
|
113
|
-
|
114
|
-
# CD/DVD device path (use null for default device)
|
115
|
-
discid_device: null
|
116
|
-
|
117
|
-
# Audio normalization settings
|
118
|
-
normalize_gain: 5 # dB gain for normalization
|
119
|
-
normalize_preset: "radio" # "album" or "radio"
|
120
|
-
|
121
|
-
# MusicBrainz API settings (optional)
|
122
|
-
musicbrainz:
|
123
|
-
username: "your_username" # For personal genre preferences
|
124
|
-
password: "your_password" # Will be stored securely
|
125
|
-
rate_limit: 1.5 # Seconds between API requests
|
126
|
-
```
|
127
|
-
|
128
|
-
3. **Default Values** (lowest precedence)
|
129
|
-
- Built-in defaults from the application
|
130
|
-
|
131
|
-
### Available Settings ###
|
132
|
-
|
133
|
-
| Setting | Default | Description |
|
134
|
-
|--------------------------|---------------------------|-------------------------------------------|
|
135
|
-
| `library_dir` | `./library` | Directory for storing audio files |
|
136
|
-
| `work_dir` | `~/.cache/audiolibrarian` | Directory for temporary files |
|
137
|
-
| `discid_device` | `null` | CD device path (null for default device) |
|
138
|
-
| `normalize_gain` | `5` | Normalization gain in dB |
|
139
|
-
| `normalize_preset` | `"radio"` | Normalization preset ("album" or "radio") |
|
140
|
-
| `musicbrainz.username` | (not set) | MusicBrainz username |
|
141
|
-
| `musicbrainz.password` | (not set) | MusicBrainz password |
|
142
|
-
| `musicbrainz.rate_limit` | `1.5` | Seconds between requests |
|
143
|
-
|
144
|
-
> **Notes**:
|
145
|
-
>
|
146
|
-
> - The `musicbrainz` username and password are optional but recommended for accessing personal genre
|
147
|
-
> preferences on [MusicBrainz](https://musicbrainz.org/).
|
148
|
-
> - The `work_dir` default is actually `$XDG_CACHE_HOME/audiolibrarian`, which defaults to
|
149
|
-
> `~/.cache/audiolibrarian` on Linux and macOS.
|
150
|
-
|
151
|
-
## Usage ##
|
152
|
-
|
153
|
-
### Basic Commands ###
|
154
|
-
|
155
|
-
```bash
|
156
|
-
# Rip audio from a CD
|
157
|
-
audiolibrarian rip
|
158
|
-
|
159
|
-
# Convert audio files
|
160
|
-
audiolibrarian convert /path/to/audio/files
|
161
|
-
|
162
|
-
# Create or update manifest files
|
163
|
-
audiolibrarian manifest /path/to/audio/files
|
164
|
-
|
165
|
-
# Reconvert files from existing source
|
166
|
-
audiolibrarian reconvert /path/to/source/directories
|
167
|
-
|
168
|
-
# Rename files based on tags
|
169
|
-
audiolibrarian rename /path/to/audio/directories
|
170
|
-
|
171
|
-
# Manage MusicBrainz genres
|
172
|
-
audiolibrarian genre /path/to/audio/directories --tag # Update tags with MB genres
|
173
|
-
|
174
|
-
# Show help for all commands
|
175
|
-
audiolibrarian --help
|
176
|
-
```
|
177
|
-
|
178
|
-
### Directory Structure ###
|
179
|
-
|
180
|
-
`audiolibrarian` organizes files in the following structure:
|
181
|
-
|
182
|
-
**Processed audio files** (organized by format):
|
183
|
-
|
184
|
-
```text
|
185
|
-
library/
|
186
|
-
├── flac/
|
187
|
-
│ └── Artist/
|
188
|
-
│ └── YYYY__Album/
|
189
|
-
│ ├── 01__Track_Title.flac
|
190
|
-
│ └── 02__Another_Track.flac
|
191
|
-
├── m4a/
|
192
|
-
│ └── Artist/
|
193
|
-
│ └── YYYY__Album/
|
194
|
-
│ ├── 01__Track_Title.m4a
|
195
|
-
│ └── 02__Another_Track.m4a
|
196
|
-
├── mp3/
|
197
|
-
│ └── Artist/
|
198
|
-
│ └── YYYY__Album/
|
199
|
-
│ ├── 01__Track_Title.mp3
|
200
|
-
│ └── 02__Another_Track.mp3
|
201
|
-
├── source/
|
202
|
-
│ └── Artist/
|
203
|
-
│ └── YYYY__Album/
|
204
|
-
│ ├── 01__Track_Title.flac
|
205
|
-
│ ├── 02__Another_Track.flac
|
206
|
-
│ └── Manifest.yaml
|
207
|
-
```
|
208
|
-
|
209
|
-
### Advanced Usage ###
|
210
|
-
|
211
|
-
#### Ripping CDs ####
|
212
|
-
|
213
|
-
```bash
|
214
|
-
# Basic CD rip
|
215
|
-
audiolibrarian rip
|
216
|
-
|
217
|
-
# Specify artist and album
|
218
|
-
audiolibrarian rip --artist "Artist Name" --album "Album Name"
|
219
|
-
|
220
|
-
# Specify MusicBrainz release ID (for better metadata)
|
221
|
-
audiolibrarian rip --mb-release-id "12345678-1234-1234-1234-123456789012"
|
222
|
-
|
223
|
-
# Specify disc number for multi-disc sets
|
224
|
-
audiolibrarian rip --disc "1/2" # First disc of two
|
225
|
-
```
|
226
|
-
|
227
|
-
#### Converting Audio Files ####
|
228
|
-
|
229
|
-
```bash
|
230
|
-
# Convert with specific artist and album
|
231
|
-
audiolibrarian convert --artist "Artist Name" --album "Album Name" /path/to/audio/files
|
232
|
-
|
233
|
-
# Convert with MusicBrainz release ID
|
234
|
-
audiolibrarian convert --mb-release-id "12345678-1234-1234-1234-123456789012" /path/to/audio/files
|
235
|
-
|
236
|
-
# Convert multi-disc release
|
237
|
-
audiolibrarian convert --disc "1/2" /path/to/disc1/files
|
238
|
-
```
|
239
|
-
|
240
|
-
#### Working with Manifests ####
|
241
|
-
|
242
|
-
```bash
|
243
|
-
# Create manifest for existing files
|
244
|
-
audiolibrarian manifest /path/to/audio/files
|
245
|
-
|
246
|
-
# Specify CD as source
|
247
|
-
audiolibrarian manifest --cd /path/to/audio/files
|
248
|
-
|
249
|
-
# Specify MusicBrainz artist and release IDs
|
250
|
-
audiolibrarian manifest \
|
251
|
-
--mb-artist-id "12345678-1234-1234-1234-123456789012" \
|
252
|
-
--mb-release-id "87654321-4321-4321-4321-210987654321" \
|
253
|
-
/path/to/audio/files
|
254
|
-
```
|
255
|
-
|
256
|
-
#### Reconverting Files ####
|
257
|
-
|
258
|
-
```bash
|
259
|
-
# Reconvert all files in directory
|
260
|
-
audiolibrarian reconvert /path/to/source/directories
|
261
|
-
|
262
|
-
# Reconvert with dry run (no changes)
|
263
|
-
audiolibrarian reconvert --dry-run /path/to/source/directories
|
264
|
-
```
|
265
|
-
|
266
|
-
#### Renaming Files ####
|
267
|
-
|
268
|
-
```bash
|
269
|
-
# Rename files based on tags
|
270
|
-
audiolibrarian rename /path/to/audio/directories
|
271
|
-
|
272
|
-
# Preview renames without making changes
|
273
|
-
audiolibrarian rename --dry-run /path/to/audio/directories
|
274
|
-
```
|
275
|
-
|
276
|
-
#### Using Different Normalization Presets ####
|
277
|
-
|
278
|
-
```bash
|
279
|
-
# Use radio normalization preset (default)
|
280
|
-
export AUDIOLIBRARIAN__NORMALIZE_PRESET="radio"
|
281
|
-
|
282
|
-
# Use album normalization preset
|
283
|
-
export AUDIOLIBRARIAN__NORMALIZE_PRESET="album"
|
284
|
-
```
|
285
|
-
|
286
|
-
#### Combining Configuration Sources ####
|
287
|
-
|
288
|
-
Configuration sources are combined with the following precedence (highest to lowest):
|
289
|
-
1. Environment variables
|
290
|
-
2. YAML configuration file
|
291
|
-
3. Default values
|
292
|
-
|
293
|
-
For example, with this `config.yaml`:
|
294
|
-
|
295
|
-
```yaml
|
296
|
-
# config.yaml
|
297
|
-
library_dir: /media/music/library
|
298
|
-
normalize_gain: 5.0
|
299
|
-
```
|
300
|
-
|
301
|
-
And this environment variable:
|
302
|
-
|
303
|
-
```bash
|
304
|
-
export AUDIOLIBRARIAN__NORMALIZE_GAIN="8.0"
|
305
|
-
```
|
306
|
-
|
307
|
-
The effective value of `normalize_gain` will be `8.0` (from the environment variable), while
|
308
|
-
`library_dir` will be set to `/media/music/library` from the YAML file.
|
309
|
-
|
310
|
-
### Troubleshooting ###
|
311
|
-
|
312
|
-
#### Increasing Verbosity ####
|
313
|
-
|
314
|
-
```bash
|
315
|
-
# Show more detailed output
|
316
|
-
audiolibrarian --log-level INFO cd
|
317
|
-
|
318
|
-
# Show debug information
|
319
|
-
audiolibrarian --log-level DEBUG cd
|
320
|
-
```
|
321
|
-
|
322
|
-
#### MusicBrainz Issues ####
|
323
|
-
|
324
|
-
If you encounter MusicBrainz-related errors:
|
325
|
-
|
326
|
-
1. Verify your credentials are correct
|
327
|
-
2. Check your Internet connection
|
328
|
-
3. Use the debug log level to get more information
|
329
|
-
4. Increase the rate limit if you're hitting rate limits
|
330
|
-
|
331
|
-
```bash
|
332
|
-
export AUDIOLIBRARIAN__MUSICBRAINZ__RATE_LIMIT="2.0"
|
333
|
-
```
|
@@ -1,28 +0,0 @@
|
|
1
|
-
audiolibrarian/__init__.py,sha256=VVNnoovlK4Pl3YDm2ETbU5q14mKK3UH6PvNQIVxNq7Y,787
|
2
|
-
audiolibrarian/audiosource.py,sha256=EtoSQb6R2zfHUwxiR9ZqGwjxrX2mTB5gPpmKangps4s,8573
|
3
|
-
audiolibrarian/base.py,sha256=54s_NacDvyGpvc_uOJO5apmQin4HKUGfPrlw0XFiJoI,19428
|
4
|
-
audiolibrarian/cli.py,sha256=azX49RO2t4q8qyUJ9WTOlDqUO13p17mq0wNmEdl6QGw,4211
|
5
|
-
audiolibrarian/commands.py,sha256=8R9aagOaaLE798lGYPWknW-NiIHkxEbQbiM43RZrPTU,11166
|
6
|
-
audiolibrarian/genremanager.py,sha256=ZciuJb3GoaM31h33_khowNg8iv4MVGkjJM-beijSF3k,7357
|
7
|
-
audiolibrarian/musicbrainz.py,sha256=DAodaLVySqq6nSbGCLfUktXSAm95OfPhkOXyyexMj6w,19312
|
8
|
-
audiolibrarian/output.py,sha256=JHuttwEMEzUxty7P8e2jaL6702q52fTWAdZEgb85bxQ,1702
|
9
|
-
audiolibrarian/records.py,sha256=BovHwNVd0nTTwQPYkhOAOIVASWz9l47j6cGdXAx5VjM,7929
|
10
|
-
audiolibrarian/settings.py,sha256=qKQ9JYfKr2k_JceWfYPiMlpBKylujCQI_2pmxWpsGFo,2550
|
11
|
-
audiolibrarian/sh.py,sha256=tVTuKXlMH738jTopjnMWqidTaH02_EyHau3q9kQ6mYE,1912
|
12
|
-
audiolibrarian/text.py,sha256=05q_E22w9hMsy01_A5XlU2pqQIMlM5RF4DXzYuLCcYo,4026
|
13
|
-
audiolibrarian/audiofile/__init__.py,sha256=BRjf4iu_cogB3A5oIVE4BjIgUGCH0dAzPG4A_48rOUM,894
|
14
|
-
audiolibrarian/audiofile/audiofile.py,sha256=IhTIzFzYeKfJmsgOG7bN62KWD06ehOGRoVv3eCf-_vw,4352
|
15
|
-
audiolibrarian/audiofile/tags.py,sha256=42oyqHZ6jRokmQskD2p53NMgKxJR7xkidXImOhUzszY,1785
|
16
|
-
audiolibrarian/audiofile/formats/__init__.py,sha256=ZV5urTtQQ4ZX_K2s9qFsSzDunvdQaQ43K7CE-BPPtEo,25
|
17
|
-
audiolibrarian/audiofile/formats/flac.py,sha256=3MfzdgnAqTVSbAraAVde2BSHwdAyi-fKkc55OvuRNSc,9708
|
18
|
-
audiolibrarian/audiofile/formats/m4a.py,sha256=S0aFZxMzgwpzkwYE2D_K_US0Dnh0objK21ph1S_8rJk,10552
|
19
|
-
audiolibrarian/audiofile/formats/mp3.py,sha256=ipuanK7CIYSk9LqW0AvAgKAUZeU9ecwK0uZf2AhAGXo,12881
|
20
|
-
picard_src/README.md,sha256=mtJ7RNLlC7Oz9M038WU-3ciPa7jPdXlFLYdJBL8iRQo,411
|
21
|
-
picard_src/__init__.py,sha256=acu0-oac_qfEgiB0rw0RvuL---O15-rOckWboVMxWtM,198
|
22
|
-
picard_src/textencoding.py,sha256=0MRHFwhqEwauQbjTTz6gpgzo6YH1VDPfdJqQoCWHtjM,26234
|
23
|
-
audiolibrarian-0.16.4.dist-info/METADATA,sha256=_odZKLK0zulvGlhQs-bwdns_cUleRO49K1tpcT2qrdk,10476
|
24
|
-
audiolibrarian-0.16.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
25
|
-
audiolibrarian-0.16.4.dist-info/entry_points.txt,sha256=reubnr_SGbTTDXji8j7z8aTmIL0AEQKVSLcnmFG3YYY,59
|
26
|
-
audiolibrarian-0.16.4.dist-info/licenses/COPYING,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
|
27
|
-
audiolibrarian-0.16.4.dist-info/licenses/LICENSE,sha256=WJ7YI-moTFb-uVrFjnzzhGJrnL9P2iqQe8NuED3hutI,35141
|
28
|
-
audiolibrarian-0.16.4.dist-info/RECORD,,
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|