audiolibrarian 0.16.5__tar.gz → 0.17.0__tar.gz

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.
Files changed (53) hide show
  1. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/PKG-INFO +2 -1
  2. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/api/audiolibrarian.md +1 -1
  3. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/development/contributing.md +0 -5
  4. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/index.md +1 -0
  5. audiolibrarian-0.17.0/docs/user-guide/changelog.md +17 -0
  6. audiolibrarian-0.17.0/docs/user-guide/configuration.md +95 -0
  7. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/user-guide/getting-started.md +1 -1
  8. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/user-guide/installation.md +4 -16
  9. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/mkdocs.yml +6 -3
  10. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/pyproject.toml +4 -3
  11. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/scripts/update_links.sh +11 -12
  12. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/__init__.py +2 -2
  13. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/__init__.py +1 -2
  14. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/audiofile.py +1 -1
  15. audiolibrarian-0.17.0/src/audiolibrarian/audiofile/formats/__init__.py +17 -0
  16. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/formats/flac.py +1 -1
  17. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/formats/m4a.py +1 -1
  18. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/formats/mp3.py +1 -1
  19. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/tags.py +1 -1
  20. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiosource.py +1 -1
  21. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/base.py +86 -39
  22. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/cli.py +1 -2
  23. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/commands.py +1 -1
  24. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/genremanager.py +1 -1
  25. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/musicbrainz.py +1 -1
  26. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/output.py +1 -1
  27. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/records.py +1 -1
  28. audiolibrarian-0.17.0/src/audiolibrarian/settings.py +114 -0
  29. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/sh.py +1 -1
  30. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/text.py +1 -1
  31. audiolibrarian-0.17.0/tools/__init__.py +17 -0
  32. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/tools/diff.py +1 -1
  33. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/tools/show_tags.py +1 -1
  34. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/uv.lock +52 -3
  35. audiolibrarian-0.16.5/docs/user-guide/configuration.md +0 -70
  36. audiolibrarian-0.16.5/src/audiolibrarian/audiofile/formats/__init__.py +0 -1
  37. audiolibrarian-0.16.5/src/audiolibrarian/settings.py +0 -79
  38. audiolibrarian-0.16.5/tools/__init__.py +0 -1
  39. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.github/workflows/main.yml +0 -0
  40. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.gitignore +0 -0
  41. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.python-version +0 -0
  42. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.readthedocs.yaml +0 -0
  43. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/CONTRIBUTING.md +0 -0
  44. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/COPYING +0 -0
  45. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/LICENSE +0 -0
  46. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/Makefile +0 -0
  47. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/README.md +0 -0
  48. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/user-guide/advanced.md +0 -0
  49. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/git_hooks/README.md +0 -0
  50. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/git_hooks/pre-push +0 -0
  51. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/picard_src/README.md +0 -0
  52. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/picard_src/__init__.py +0 -0
  53. {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/picard_src/textencoding.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audiolibrarian
3
- Version: 0.16.5
3
+ Version: 0.17.0
4
4
  Summary: Manage my audio library.
5
5
  Project-URL: Repository, https://github.com/toadstule/audiolibrarian
6
6
  Author-email: Steve Jibson <steve@jibson.com>
@@ -12,6 +12,7 @@ Classifier: Programming Language :: Python
12
12
  Requires-Python: <3.14,>=3.12
13
13
  Requires-Dist: ansicolors
14
14
  Requires-Dist: discid
15
+ Requires-Dist: ffmpeg-normalize
15
16
  Requires-Dist: filelock
16
17
  Requires-Dist: fuzzywuzzy
17
18
  Requires-Dist: musicbrainzngs
@@ -53,4 +53,4 @@ print(f"Found track: {track_info.title} from {track_info.album}")
53
53
 
54
54
  ## Contributing
55
55
 
56
- If you want to contribute to the development of audiolibrarian, please read our [contributing guidelines](development/contributing.md).
56
+ If you want to contribute to the development of audiolibrarian, please read our [contributing guidelines](../development/contributing.md).
@@ -99,11 +99,6 @@ We welcome feature requests! Please open an issue and describe:
99
99
  - Why this feature would be useful
100
100
  - Any suggestions for implementation
101
101
 
102
- ## Code of Conduct
103
-
104
- This project adheres to the Contributor Covenant [code of conduct](CODE_OF_CONDUCT.md).
105
- By participating, you are expected to uphold this code.
106
-
107
102
  ## Available Make Commands
108
103
 
109
104
  For a complete list of available commands, run:
@@ -31,5 +31,6 @@ converting them to multiple formats, and organizing them in a clean directory st
31
31
 
32
32
  - Found a bug? [Open an issue](https://github.com/toadstule/audiolibrarian/issues).
33
33
  - Want to contribute? Read our [contributing guide](development/contributing.md).
34
+ - Check out the [changelog](user-guide/changelog.md) to see what's new.
34
35
 
35
36
  [MusicBrainz]: https://musicbrainz.org
@@ -0,0 +1,17 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [0.17.0] - 2025-06-22
9
+
10
+ ### Added
11
+
12
+ - Support for `ffmpeg-normalize` for normalization (optional)
13
+
14
+ ### Changed
15
+
16
+ - Configuration options for normalization selection
17
+ - Configuration options for `ffmpeg-normalize`
@@ -0,0 +1,95 @@
1
+ # Configuration
2
+
3
+ `audiolibrarian` uses a flexible configuration system that supports multiple configuration sources,
4
+ listed in order of precedence:
5
+
6
+ ## Configuration Sources
7
+
8
+ ### 1. Environment Variables (highest precedence)
9
+
10
+ - **Prefix**: `AUDIOLIBRARIAN__`
11
+ - **Nested fields**: Use `__` as delimiter (e.g., `AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME`)
12
+ - **Example**:
13
+
14
+ ```bash
15
+ # Override library directory (library_dir)
16
+ export AUDIOLIBRARIAN__LIBRARY_DIR="/mnt/music/library"
17
+
18
+ # Set MusicBrainz credentials (musicbrainz.username and musicbrainz.password)
19
+ export AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME="your_username"
20
+ export AUDIOLIBRARIAN__MUSICBRAINZ__PASSWORD="your_password"
21
+ ```
22
+
23
+ ### 2. YAML Configuration File (medium precedence)
24
+
25
+ - **Default location**: `~/.config/audiolibrarian/config.yaml`
26
+ - **Example**:
27
+
28
+ ```yaml
29
+ # Base directory for your music library
30
+ library_dir: "~/music/library"
31
+
32
+ # Cache and working directory
33
+ work_dir: "~/.cache/audiolibrarian"
34
+
35
+ # CD/DVD device path (use null for default device)
36
+ discid_device: null
37
+
38
+ # Audio normalization settings
39
+ normalize:
40
+ normalizer: "auto" # "auto", "wavegain", "ffmpeg", or "none"
41
+ ffmpeg:
42
+ target_level: -13 # Target LUFS level for ffmpeg normalization
43
+ wavegain:
44
+ gain: 5 # dB gain for wavegain normalization (0-10)
45
+ preset: "radio" # "album" or "radio"
46
+
47
+ # MusicBrainz API settings (optional)
48
+ musicbrainz:
49
+ username: "your_username" # For personal genre preferences
50
+ password: "your_password" # Will be stored securely
51
+ rate_limit: 1.5 # Seconds between API requests
52
+ ```
53
+
54
+ ### 3. Default Values (lowest precedence)
55
+
56
+ - Built-in defaults from the application
57
+
58
+ ## Available Settings
59
+
60
+ | Setting | Default | Description |
61
+ |---------------------------------|------------------|-------------------------------------------|
62
+ | `library_dir` | `./library` | Directory for storing audio files |
63
+ | `work_dir` | (see below)[^wd] | Directory for temporary files |
64
+ | `discid_device` | `null` | CD device path (null for default device) |
65
+ | `normalize.normalizer` | `"auto"` | "auto", "wavegain", "ffmpeg", or "none" |
66
+ | `normalize.ffmpeg.target_level` | `-13` | Target LUFS level (ffmpeg) |
67
+ | `normalize.wavegain.gain` | `5` | Normalization gain in dB (0-10, wavegain) |
68
+ | `normalize.wavegain.preset` | `"radio"` | "album" or "radio" (wavegain) |
69
+ | `musicbrainz.username` | (not set) | MusicBrainz username[^mb] |
70
+ | `musicbrainz.password` | (not set) | MusicBrainz password[^mb] |
71
+ | `musicbrainz.rate_limit` | `1.5` | Seconds between requests |
72
+
73
+ [^wd]: The `work_dir` default is `$XDG_CACHE_HOME/audiolibrarian`, which defaults
74
+ to `~/.cache/audiolibrarian` on Linux and macOS.
75
+
76
+ [^mb]: The `musicbrainz` username and password are optional but recommended for accessing personal genre
77
+ preferences on [MusicBrainz](https://musicbrainz.org/).
78
+
79
+ ## Audio Normalization
80
+
81
+ Audio normalization ensures consistent volume levels across tracks. The following options are available:
82
+
83
+ - `auto` (default): Automatically selects the best available normalizer (prefers wavegain)
84
+ - `wavegain`: Uses the `wavegain` command-line tool (recommended for better album normalization)
85
+ - `ffmpeg`: Uses the `ffmpeg-normalize` Python package
86
+ - `none`: Skips normalization entirely
87
+
88
+ ### Wavegain Settings
89
+
90
+ - `gain`: Adjusts the target volume level (in dB, typically 0-10)
91
+ - `preset`: "album" (loudness normalization) or "radio" (peak normalization)
92
+
93
+ ### FFmpeg Settings
94
+
95
+ - `target_level`: Target LUFS level (typically between -16 and -12)
@@ -62,7 +62,7 @@ This will:
62
62
  **Sample output:**
63
63
 
64
64
  ```text
65
- ─➤ audiolibrarian rip
65
+ $ audiolibrarian rip
66
66
 
67
67
  Gathering search information...
68
68
  Finding MusicBrainz release information...
@@ -15,11 +15,12 @@
15
15
  - [util-linux](https://github.com/util-linux/util-linux)
16
16
  - [faad2](https://github.com/knik0/faad2)
17
17
  - [fdkaac](https://github.com/nu774/fdkaac)
18
+ - [ffmpeg](https://ffmpeg.org/) - optional
18
19
  - [flac](https://github.com/xiph/flac)
19
20
  - [lame](https://lame.sourceforge.io/)
20
21
  - [mpg123](https://www.mpg123.de/)
21
22
  - [libsndfile](https://github.com/libsndfile/libsndfile)
22
- - [wavegain](https://github.com/MestreLion/wavegain)
23
+ - [wavegain](https://github.com/MestreLion/wavegain) - optional
23
24
 
24
25
  It also requires the [libdiscid](https://musicbrainz.org/doc/libdiscid) library.
25
26
 
@@ -68,6 +69,7 @@ pip uninstall audiolibrarian
68
69
  sudo pacman -S \
69
70
  faad2 \
70
71
  fdkaac \
72
+ ffmpeg \
71
73
  flac \
72
74
  lame \
73
75
  libcdio-paranoia \
@@ -92,6 +94,7 @@ sudo apt install -y \
92
94
  eject \
93
95
  faad \
94
96
  fdkaac \
97
+ ffmpeg \
95
98
  flac \
96
99
  lame \
97
100
  libdiscid0 \
@@ -99,18 +102,3 @@ sudo apt install -y \
99
102
  mpg123 \
100
103
  python3-pip
101
104
  ```
102
-
103
- This will get you everything you need except for `wavegain`. You build and install
104
- `wavegain` from source as follows:
105
-
106
- ```bash
107
- sudo apt install -y gcc wget unzip
108
- wget "https://www.rarewares.org/files/others/wavegain-1.3.1srcs.zip"
109
- unzip wavegain-1.3.1srcs.zip
110
- cd WaveGain-1.3.1
111
- gcc -fcommon *.c -o wavegain -DHAVE_CONFIG_H -lm
112
- # You'll get some warning here, but they can be ignored.
113
- sudo cp wavegain /usr/loca/bin/wavegain
114
- cd ..
115
- rm -rf WaveGain-1.3.1 wavegain-1.3.1srcs.zip
116
- ```
@@ -1,6 +1,7 @@
1
1
  site_name: audiolibrarian
2
2
  site_url: https://audiolibrarian.readthedocs.io/
3
3
  repo_url: https://github.com/toadstule/audiolibrarian
4
+ copyright: Copyright &copy; 2025 Steve Jibson
4
5
 
5
6
  theme:
6
7
  name: readthedocs
@@ -22,14 +23,17 @@ theme:
22
23
  - toc.follow
23
24
  - toc.sticky
24
25
 
25
- #markdown_extensions:
26
+ markdown_extensions:
26
27
  # - footnotes
27
28
  # - toc:
28
29
  # permalink: true
29
- # - pymdownx.highlight:
30
+ # - pymdownx.highlight
30
31
  # anchor_linenums: true
31
32
  # line_spans: __span
32
33
  # - pymdownx.inlinehilite
34
+ # - pymdownx.inlinehilite
35
+ # - pymdownx.superfences
36
+
33
37
  # - pymdownx.snippets
34
38
  # - attr_list
35
39
  # - md_in_html
@@ -72,7 +76,6 @@ nav:
72
76
  - Contributing: development/contributing.md
73
77
 
74
78
  # Extra configuration
75
- extra_copyright: Copyright &copy; 2025 Steve Jibson
76
79
  extra:
77
80
  # Add links to show in the footer
78
81
  links:
@@ -7,9 +7,9 @@ dev = [
7
7
  "coverage",
8
8
  "hatchling",
9
9
  "mypy",
10
- "mkdocs-material>=9.0.0",
11
- "mkdocs>=1.5.0",
12
- "mkdocstrings[python]>=0.23.0",
10
+ "mkdocs-material",
11
+ "mkdocs",
12
+ "mkdocstrings[python]",
13
13
  "pymarkdownlnt",
14
14
  "pytest",
15
15
  "pytest-env",
@@ -30,6 +30,7 @@ description = "Manage my audio library."
30
30
  dependencies = [
31
31
  "ansicolors",
32
32
  "discid",
33
+ "ffmpeg-normalize",
33
34
  "filelock",
34
35
  "fuzzywuzzy",
35
36
  "musicbrainzngs",
@@ -1,20 +1,19 @@
1
1
  #!/bin/bash
2
2
  #
3
- # Copyright (c) 2020 Stephen Jibson
3
+ # Copyright (c) 2000-2025 Stephen Jibson
4
4
  #
5
- # This file is part of audiolibrarian.
5
+ # This file is part of audiolibrarian.
6
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.
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
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/>.
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.
17
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/>.
18
17
  #
19
18
 
20
19
  # This file should be located in the /bin directory of the library (i.e. "/media/music/bin")
@@ -36,7 +35,7 @@ function make_links {
36
35
  fi
37
36
  fi
38
37
  for source in "$d"/*; do
39
- # echo "$source"
38
+ # echo "$source"
40
39
  ln -sf ../"$source" "$base"/
41
40
  done
42
41
  done
@@ -1,7 +1,7 @@
1
1
  """The audiolibrarian package."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
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.16.5"
19
+ __version__ = "0.17.0"
@@ -1,7 +1,6 @@
1
1
  """Audio file library."""
2
-
3
2
  #
4
- # Copyright (c) 2020 Stephen Jibson
3
+ # Copyright (c) 2000-2025 Stephen Jibson
5
4
  #
6
5
  # This file is part of audiolibrarian.
7
6
  #
@@ -1,7 +1,7 @@
1
1
  """Audio file library."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -0,0 +1,17 @@
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
+ #
@@ -1,7 +1,7 @@
1
1
  """AudioFile support for flac files."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """AudioFile support for m4a files."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """AudioFile support for mp3 files."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """Manage tags."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """AudioSource."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -4,7 +4,7 @@ Useful stuff: https://help.mp3tag.de/main_tags.html
4
4
  """
5
5
 
6
6
  #
7
- # Copyright (c) 2020 Stephen Jibson
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 wavegain."""
287
- print("Normalizing wav files...")
288
- command = [
289
- "wavegain",
290
- f"--{SETTINGS.normalize_preset}",
291
- f"--gain={SETTINGS.normalize_gain}",
292
- "--apply",
293
- ]
294
- command.extend(str(f) for f in self._wav_filenames)
295
- result = subprocess.run(command, capture_output=True, check=False) # noqa: S603
296
- for line in str(result.stderr).split(r"\n"):
297
- line_trunc = line[:137] + "..." if len(line) > 140 else line # noqa: PLR2004
298
- log.info("WAVEGAIN: %s", line_trunc)
299
- result.check_returncode()
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
- @staticmethod
302
- def _read_manifest(manifest_path: pathlib.Path) -> dict[Any, Any]:
303
- with manifest_path.open(encoding="utf-8") as manifest_file:
304
- return dict(yaml.safe_load(manifest_file))
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))
@@ -1,7 +1,7 @@
1
1
  """Audiolibrarian command line interface."""
2
2
 
3
3
  #
4
- # Copyright (c) 2021 Stephen Jibson
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:
@@ -1,7 +1,7 @@
1
1
  """Command line commands."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """Genre Manager."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """Access the MusicBrainz service."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """Screen output utilities."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -4,7 +4,7 @@ Useful field reference: https://github.com/metabrainz/picard/blob/master/picard/
4
4
  """
5
5
 
6
6
  #
7
- # Copyright (c) 2020 Stephen Jibson
7
+ # Copyright (c) 2000-2025 Stephen Jibson
8
8
  #
9
9
  # This file is part of audiolibrarian.
10
10
  #
@@ -0,0 +1,114 @@
1
+ """Configuration module for AudioLibrarian.
2
+
3
+ This module provides configuration management using pydantic-settings, supporting multiple
4
+ sources of configuration:
5
+
6
+ 1. Environment Variables (highest precedence):
7
+ - Prefix: "AUDIOLIBRARIAN__"
8
+ - Nested fields: Use "__" as delimiter (e.g., AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME)
9
+
10
+ 2. YAML Configuration File:
11
+ - Location: $XDG_CONFIG_HOME/audiolibrarian/config.yaml
12
+ - Supports nested structure for MusicBrainz settings
13
+
14
+ 3. Default Values:
15
+ - Defined in Settings class
16
+ - Lowest precedence
17
+
18
+ The configuration is immutable (frozen=True) and follows XDG base directory standards for
19
+ configuration and cache locations.
20
+
21
+ Sensitive fields (like passwords) are handled using pydantic.SecretStr for security.
22
+ """
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
+ #
40
+ import logging
41
+ import pathlib
42
+ from typing import Literal
43
+
44
+ import pydantic
45
+ import pydantic_settings
46
+ import xdg_base_dirs
47
+
48
+ logger = logging.getLogger(__name__)
49
+
50
+
51
+ class MusicBrainzSettings(pydantic_settings.BaseSettings):
52
+ """Configuration settings for MusicBrainz."""
53
+
54
+ password: pydantic.SecretStr = pydantic.SecretStr("")
55
+ username: str = ""
56
+ rate_limit: pydantic.PositiveFloat = 1.5 # Seconds between requests.
57
+
58
+
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):
81
+ """Configuration settings for AudioLibrarian."""
82
+
83
+ discid_device: str | None = None # Use default device.
84
+ library_dir: pathlib.Path = pathlib.Path("library").resolve()
85
+ musicbrainz: MusicBrainzSettings = MusicBrainzSettings()
86
+ normalize: NormalizeSettings = NormalizeSettings()
87
+ work_dir: pathlib.Path = xdg_base_dirs.xdg_cache_home() / "audiolibrarian"
88
+
89
+ model_config = pydantic_settings.SettingsConfigDict(
90
+ env_nested_delimiter="__",
91
+ env_prefix="AUDIOLIBRARIAN__",
92
+ yaml_file=str(xdg_base_dirs.xdg_config_home() / "audiolibrarian" / "config.yaml"),
93
+ frozen=True, # Make settings immutable.
94
+ )
95
+
96
+ @classmethod
97
+ def settings_customise_sources(
98
+ cls,
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, ...]:
105
+ del dotenv_settings, file_secret_settings # Unused.
106
+ return (
107
+ env_settings,
108
+ pydantic_settings.YamlConfigSettingsSource(settings_cls),
109
+ init_settings,
110
+ )
111
+
112
+
113
+ SETTINGS = Settings()
114
+ __all__ = ["SETTINGS"]
@@ -1,7 +1,7 @@
1
1
  """Command execution helpers."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """Text utilities."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -0,0 +1,17 @@
1
+ """Miscellaneous tools."""
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
+ #
@@ -1,7 +1,7 @@
1
1
  """Show differences in tags between two files."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -1,7 +1,7 @@
1
1
  """Show tags."""
2
2
 
3
3
  #
4
- # Copyright (c) 2020 Stephen Jibson
4
+ # Copyright (c) 2000-2025 Stephen Jibson
5
5
  #
6
6
  # This file is part of audiolibrarian.
7
7
  #
@@ -40,6 +40,7 @@ source = { editable = "." }
40
40
  dependencies = [
41
41
  { name = "ansicolors" },
42
42
  { name = "discid" },
43
+ { name = "ffmpeg-normalize" },
43
44
  { name = "filelock" },
44
45
  { name = "fuzzywuzzy" },
45
46
  { name = "musicbrainzngs" },
@@ -72,6 +73,7 @@ dev = [
72
73
  requires-dist = [
73
74
  { name = "ansicolors" },
74
75
  { name = "discid" },
76
+ { name = "ffmpeg-normalize" },
75
77
  { name = "filelock" },
76
78
  { name = "fuzzywuzzy" },
77
79
  { name = "musicbrainzngs" },
@@ -87,9 +89,9 @@ requires-dist = [
87
89
  dev = [
88
90
  { name = "coverage" },
89
91
  { name = "hatchling" },
90
- { name = "mkdocs", specifier = ">=1.5.0" },
91
- { name = "mkdocs-material", specifier = ">=9.0.0" },
92
- { name = "mkdocstrings", extras = ["python"], specifier = ">=0.23.0" },
92
+ { name = "mkdocs" },
93
+ { name = "mkdocs-material" },
94
+ { name = "mkdocstrings", extras = ["python"] },
93
95
  { name = "mypy" },
94
96
  { name = "pymarkdownlnt" },
95
97
  { name = "pytest" },
@@ -187,6 +189,18 @@ wheels = [
187
189
  { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
188
190
  ]
189
191
 
192
+ [[package]]
193
+ name = "colorlog"
194
+ version = "6.9.0"
195
+ source = { registry = "https://pypi.jibson.com/simple" }
196
+ dependencies = [
197
+ { name = "colorama", marker = "sys_platform == 'win32'" },
198
+ ]
199
+ sdist = { url = "https://files.pythonhosted.org/packages/d3/7a/359f4d5df2353f26172b3cc39ea32daa39af8de522205f512f458923e677/colorlog-6.9.0.tar.gz", hash = "sha256:bfba54a1b93b94f54e1f4fe48395725a3d92fd2a4af702f6bd70946bdc0c6ac2", size = 16624, upload-time = "2024-10-29T18:34:51.011Z" }
200
+ wheels = [
201
+ { url = "https://files.pythonhosted.org/packages/e3/51/9b208e85196941db2f0654ad0357ca6388ab3ed67efdbfc799f35d1f83aa/colorlog-6.9.0-py3-none-any.whl", hash = "sha256:5906e71acd67cb07a71e779c47c4bcb45fb8c2993eebe9e5adcd6a6f1b283eff", size = 11424, upload-time = "2024-10-29T18:34:49.815Z" },
202
+ ]
203
+
190
204
  [[package]]
191
205
  name = "columnar"
192
206
  version = "1.4.1"
@@ -248,6 +262,29 @@ version = "1.2.0"
248
262
  source = { registry = "https://pypi.jibson.com/simple" }
249
263
  sdist = { url = "https://files.pythonhosted.org/packages/d5/fa/c8856ae3eb53393445d84589afbd49ded85527563a7c0457f4e967d5b7af/discid-1.2.0.tar.gz", hash = "sha256:cd9630bc53f5566df92819641040226e290b2047573f2c74a6e92b8eed9e86b9", size = 30777, upload-time = "2019-02-23T11:16:55.652Z" }
250
264
 
265
+ [[package]]
266
+ name = "ffmpeg-normalize"
267
+ version = "1.32.5"
268
+ source = { registry = "https://pypi.jibson.com/simple" }
269
+ dependencies = [
270
+ { name = "colorama", marker = "sys_platform == 'win32'" },
271
+ { name = "colorlog" },
272
+ { name = "ffmpeg-progress-yield" },
273
+ { name = "mutagen" },
274
+ { name = "tqdm" },
275
+ ]
276
+ wheels = [
277
+ { url = "https://files.pythonhosted.org/packages/b1/11/1b1adca14e40084198632d8eab31fdb91cb26bc74b0b76ac7366b8eeab8d/ffmpeg_normalize-1.32.5-py3-none-any.whl", hash = "sha256:c22ab5421726a1736134992efd6b52da570d9f808d2ba9500a21b7ef20de4d6c", size = 36240, upload-time = "2025-06-22T18:22:52.39Z" },
278
+ ]
279
+
280
+ [[package]]
281
+ name = "ffmpeg-progress-yield"
282
+ version = "1.0.1"
283
+ source = { registry = "https://pypi.jibson.com/simple" }
284
+ wheels = [
285
+ { url = "https://files.pythonhosted.org/packages/88/b1/1f88ee6006f212e36e2d1867d20bdaffd0f5a065c17d34c7083a3b03b4f3/ffmpeg_progress_yield-1.0.1-py3-none-any.whl", hash = "sha256:3c24844110accc84d48bde8c7c4d5a8c163cc652f1cf0e2f62c803565ae42dae", size = 13704, upload-time = "2025-06-22T18:20:13.827Z" },
286
+ ]
287
+
251
288
  [[package]]
252
289
  name = "filelock"
253
290
  version = "3.18.0"
@@ -980,6 +1017,18 @@ wheels = [
980
1017
  { url = "https://files.pythonhosted.org/packages/03/98/eb27cc78ad3af8e302c9d8ff4977f5026676e130d28dd7578132a457170c/toolz-1.0.0-py3-none-any.whl", hash = "sha256:292c8f1c4e7516bf9086f8850935c799a874039c8bcf959d47b600e4c44a6236", size = 56383, upload-time = "2024-10-04T16:17:01.533Z" },
981
1018
  ]
982
1019
 
1020
+ [[package]]
1021
+ name = "tqdm"
1022
+ version = "4.67.1"
1023
+ source = { registry = "https://pypi.jibson.com/simple" }
1024
+ dependencies = [
1025
+ { name = "colorama", marker = "sys_platform == 'win32'" },
1026
+ ]
1027
+ sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" }
1028
+ wheels = [
1029
+ { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" },
1030
+ ]
1031
+
983
1032
  [[package]]
984
1033
  name = "trove-classifiers"
985
1034
  version = "2025.5.9.12"
@@ -1,70 +0,0 @@
1
- # Configuration
2
-
3
- `audiolibrarian` uses a flexible configuration system that supports multiple configuration sources,
4
- listed in order of precedence:
5
-
6
- ## Configuration Sources
7
-
8
- ### 1. Environment Variables (highest precedence)
9
-
10
- - **Prefix**: `AUDIOLIBRARIAN__`
11
- - **Nested fields**: Use `__` as delimiter (e.g., `AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME`)
12
- - **Example**:
13
-
14
- ```bash
15
- # Override library directory (library_dir)
16
- export AUDIOLIBRARIAN__LIBRARY_DIR="/mnt/music/library"
17
-
18
- # Set MusicBrainz credentials (musicbrainz.username and musicbrainz.password)
19
- export AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME="your_username"
20
- export AUDIOLIBRARIAN__MUSICBRAINZ__PASSWORD="your_password"
21
- ```
22
-
23
- ### 2. YAML Configuration File (medium precedence)
24
-
25
- - **Default location**: `~/.config/audiolibrarian/config.yaml`
26
- - **Example**:
27
-
28
- ```yaml
29
- # Base directory for your music library
30
- library_dir: "~/music/library"
31
-
32
- # Cache and working directory
33
- work_dir: "~/.cache/audiolibrarian"
34
-
35
- # CD/DVD device path (use null for default device)
36
- discid_device: null
37
-
38
- # Audio normalization settings
39
- normalize_gain: 5 # dB gain for normalization
40
- normalize_preset: "radio" # "album" or "radio"
41
-
42
- # MusicBrainz API settings (optional)
43
- musicbrainz:
44
- username: "your_username" # For personal genre preferences
45
- password: "your_password" # Will be stored securely
46
- rate_limit: 1.5 # Seconds between API requests
47
- ```
48
-
49
- ### 3. Default Values (lowest precedence)
50
-
51
- - Built-in defaults from the application
52
-
53
- ## Available Settings
54
-
55
- | Setting | Default | Description |
56
- |--------------------------|---------------------------|-------------------------------------------|
57
- | `library_dir` | `./library` | Directory for storing audio files |
58
- | `work_dir` | `~/.cache/audiolibrarian` | Directory for temporary files[^wd] |
59
- | `discid_device` | `null` | CD device path (null for default device) |
60
- | `normalize_gain` | `5` | Normalization gain in dB |
61
- | `normalize_preset` | `"radio"` | Normalization preset ("album" or "radio") |
62
- | `musicbrainz.username` | (not set) | MusicBrainz username[^mb] |
63
- | `musicbrainz.password` | (not set) | MusicBrainz password[^mb] |
64
- | `musicbrainz.rate_limit` | `1.5` | Seconds between requests |
65
-
66
- [^wd]: The `work_dir` default is actually `$XDG_CACHE_HOME/audiolibrarian`, which defaults
67
- to `~/.cache/audiolibrarian` on Linux and macOS.
68
-
69
- [^mb]: The `musicbrainz` username and password are optional but recommended for accessing personal genre
70
- preferences on [MusicBrainz](https://musicbrainz.org/).
@@ -1 +0,0 @@
1
- """AudioFile formats."""
@@ -1,79 +0,0 @@
1
- """Configuration module for AudioLibrarian.
2
-
3
- This module provides configuration management using pydantic-settings, supporting multiple
4
- sources of configuration:
5
-
6
- 1. Environment Variables (highest precedence):
7
- - Prefix: "AUDIOLIBRARIAN__"
8
- - Nested fields: Use "__" as delimiter (e.g., AUDIOLIBRARIAN__MUSICBRAINZ__USERNAME)
9
-
10
- 2. YAML Configuration File:
11
- - Location: $XDG_CONFIG_HOME/audiolibrarian/config.yaml
12
- - Supports nested structure for MusicBrainz settings
13
-
14
- 3. Default Values:
15
- - Defined in Settings class
16
- - Lowest precedence
17
-
18
- The configuration is immutable (frozen=True) and follows XDG base directory standards for
19
- configuration and cache locations.
20
-
21
- Sensitive fields (like passwords) are handled using pydantic.SecretStr for security.
22
- """
23
-
24
- import logging
25
- import pathlib
26
- from typing import Literal
27
-
28
- import pydantic
29
- import xdg_base_dirs
30
- from pydantic_settings import (
31
- BaseSettings,
32
- PydanticBaseSettingsSource,
33
- SettingsConfigDict,
34
- YamlConfigSettingsSource,
35
- )
36
-
37
- logger = logging.getLogger(__name__)
38
-
39
-
40
- class MusicBrainzSettings(BaseSettings):
41
- """Configuration settings for MusicBrainz."""
42
-
43
- password: pydantic.SecretStr = pydantic.SecretStr("")
44
- username: str = ""
45
- rate_limit: pydantic.PositiveFloat = 1.5 # Seconds between requests.
46
-
47
-
48
- class Settings(BaseSettings):
49
- """Configuration settings for AudioLibrarian."""
50
-
51
- discid_device: str | None = None # Use default device.
52
- library_dir: pathlib.Path = pathlib.Path("library").resolve()
53
- musicbrainz: MusicBrainzSettings = MusicBrainzSettings()
54
- normalize_gain: int = 5 # dB
55
- normalize_preset: Literal["album", "radio"] = "radio"
56
- work_dir: pathlib.Path = xdg_base_dirs.xdg_cache_home() / "audiolibrarian"
57
-
58
- model_config = SettingsConfigDict(
59
- env_nested_delimiter="__",
60
- env_prefix="AUDIOLIBRARIAN__",
61
- yaml_file=str(xdg_base_dirs.xdg_config_home() / "audiolibrarian" / "config.yaml"),
62
- frozen=True, # Make settings immutable.
63
- )
64
-
65
- @classmethod
66
- def settings_customise_sources(
67
- 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, ...]:
74
- del dotenv_settings, file_secret_settings # Unused.
75
- return env_settings, YamlConfigSettingsSource(settings_cls), init_settings
76
-
77
-
78
- SETTINGS = Settings()
79
- __all__ = ["SETTINGS"]
@@ -1 +0,0 @@
1
- """Miscellaneous tools."""
File without changes
File without changes