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.
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/PKG-INFO +2 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/api/audiolibrarian.md +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/development/contributing.md +0 -5
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/index.md +1 -0
- audiolibrarian-0.17.0/docs/user-guide/changelog.md +17 -0
- audiolibrarian-0.17.0/docs/user-guide/configuration.md +95 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/user-guide/getting-started.md +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/user-guide/installation.md +4 -16
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/mkdocs.yml +6 -3
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/pyproject.toml +4 -3
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/scripts/update_links.sh +11 -12
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/__init__.py +2 -2
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/__init__.py +1 -2
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/audiofile.py +1 -1
- audiolibrarian-0.17.0/src/audiolibrarian/audiofile/formats/__init__.py +17 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/formats/flac.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/formats/m4a.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/formats/mp3.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiofile/tags.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/audiosource.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/base.py +86 -39
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/cli.py +1 -2
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/commands.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/genremanager.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/musicbrainz.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/output.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/records.py +1 -1
- audiolibrarian-0.17.0/src/audiolibrarian/settings.py +114 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/sh.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/audiolibrarian/text.py +1 -1
- audiolibrarian-0.17.0/tools/__init__.py +17 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/tools/diff.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/tools/show_tags.py +1 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/uv.lock +52 -3
- audiolibrarian-0.16.5/docs/user-guide/configuration.md +0 -70
- audiolibrarian-0.16.5/src/audiolibrarian/audiofile/formats/__init__.py +0 -1
- audiolibrarian-0.16.5/src/audiolibrarian/settings.py +0 -79
- audiolibrarian-0.16.5/tools/__init__.py +0 -1
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.github/workflows/main.yml +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.gitignore +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.python-version +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/.readthedocs.yaml +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/CONTRIBUTING.md +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/COPYING +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/LICENSE +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/Makefile +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/README.md +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/docs/user-guide/advanced.md +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/git_hooks/README.md +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/git_hooks/pre-push +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/picard_src/README.md +0 -0
- {audiolibrarian-0.16.5 → audiolibrarian-0.17.0}/src/picard_src/__init__.py +0 -0
- {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.
|
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)
|
@@ -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 © 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
|
-
|
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 © 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
|
11
|
-
"mkdocs
|
12
|
-
"mkdocstrings[python]
|
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
|
-
#
|
3
|
+
# Copyright (c) 2000-2025 Stephen Jibson
|
4
4
|
#
|
5
|
-
#
|
5
|
+
# This file is part of audiolibrarian.
|
6
6
|
#
|
7
|
-
#
|
8
|
-
#
|
9
|
-
#
|
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
|
-
#
|
12
|
-
#
|
13
|
-
#
|
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
|
-
#
|
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)
|
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"
|
@@ -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
|
+
#
|
@@ -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))
|
@@ -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:
|
@@ -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"]
|
@@ -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
|
+
#
|
@@ -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"
|
91
|
-
{ name = "mkdocs-material"
|
92
|
-
{ name = "mkdocstrings", extras = ["python"]
|
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
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|