audiolibrarian 0.16.5__tar.gz → 0.18.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 (56) hide show
  1. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/.gitignore +1 -2
  2. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/PKG-INFO +2 -1
  3. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/docs/api/audiolibrarian.md +2 -2
  4. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/docs/development/contributing.md +0 -5
  5. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/docs/index.md +1 -0
  6. audiolibrarian-0.18.0/docs/user-guide/changelog.md +28 -0
  7. audiolibrarian-0.18.0/docs/user-guide/configuration.md +97 -0
  8. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/docs/user-guide/getting-started.md +1 -1
  9. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/docs/user-guide/installation.md +4 -16
  10. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/mkdocs.yml +6 -3
  11. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/pyproject.toml +7 -4
  12. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/scripts/update_links.sh +11 -12
  13. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/__init__.py +2 -2
  14. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiofile/__init__.py +1 -2
  15. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiofile/audiofile.py +1 -1
  16. audiolibrarian-0.18.0/src/audiolibrarian/audiofile/formats/__init__.py +17 -0
  17. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiofile/formats/flac.py +1 -1
  18. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiofile/formats/m4a.py +1 -1
  19. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiofile/formats/mp3.py +1 -1
  20. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiofile/tags.py +1 -1
  21. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/audiosource.py +4 -5
  22. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/base.py +45 -47
  23. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/commands.py +59 -18
  24. audiolibrarian-0.18.0/src/audiolibrarian/config.py +152 -0
  25. audiolibrarian-0.18.0/src/audiolibrarian/entrypoints/__init__.py +17 -0
  26. {audiolibrarian-0.16.5/src/audiolibrarian → audiolibrarian-0.18.0/src/audiolibrarian/entrypoints}/cli.py +3 -4
  27. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/genremanager.py +6 -6
  28. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/musicbrainz.py +28 -14
  29. audiolibrarian-0.18.0/src/audiolibrarian/normalizer.py +155 -0
  30. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/output.py +1 -1
  31. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/records.py +1 -1
  32. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/sh.py +1 -1
  33. audiolibrarian-0.18.0/src/audiolibrarian/templates/config.toml +36 -0
  34. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/audiolibrarian/text.py +1 -1
  35. audiolibrarian-0.18.0/tools/__init__.py +17 -0
  36. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/tools/diff.py +1 -1
  37. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/tools/show_tags.py +1 -1
  38. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/uv.lock +100 -37
  39. audiolibrarian-0.16.5/docs/user-guide/configuration.md +0 -70
  40. audiolibrarian-0.16.5/src/audiolibrarian/audiofile/formats/__init__.py +0 -1
  41. audiolibrarian-0.16.5/src/audiolibrarian/settings.py +0 -79
  42. audiolibrarian-0.16.5/tools/__init__.py +0 -1
  43. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/.github/workflows/main.yml +0 -0
  44. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/.python-version +0 -0
  45. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/.readthedocs.yaml +0 -0
  46. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/CONTRIBUTING.md +0 -0
  47. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/COPYING +0 -0
  48. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/LICENSE +0 -0
  49. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/Makefile +0 -0
  50. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/README.md +0 -0
  51. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/docs/user-guide/advanced.md +0 -0
  52. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/git_hooks/README.md +0 -0
  53. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/git_hooks/pre-push +0 -0
  54. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/picard_src/README.md +0 -0
  55. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/picard_src/__init__.py +0 -0
  56. {audiolibrarian-0.16.5 → audiolibrarian-0.18.0}/src/picard_src/textencoding.py +0 -0
@@ -6,6 +6,7 @@ MANIFEST
6
6
  __pycache__/
7
7
  .mypy_cache/
8
8
  .pytype/
9
+ .scratch/
9
10
  .venv/
10
11
  audiolibrarian.egg-info/
11
12
  dist/
@@ -15,8 +16,6 @@ new_library/
15
16
  workdir/
16
17
  venv/
17
18
 
18
- NOTES.md
19
-
20
19
  # MkDocs build output
21
20
  site/
22
21
  .cache/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: audiolibrarian
3
- Version: 0.16.5
3
+ Version: 0.18.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
@@ -31,7 +31,7 @@
31
31
  from audiolibrarian import AudioLibrarian
32
32
 
33
33
  # Initialize with custom config
34
- al = AudioLibrarian(config_path="custom_config.yaml")
34
+ al = AudioLibrarian(config_path="custom_config.toml")
35
35
 
36
36
  # Scan a directory
37
37
  al.scan("/path/to/music")
@@ -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,28 @@
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.18.0] - 2025-06-27
9
+
10
+ ### Added
11
+
12
+ - New `config` command to view and manage configuration
13
+ - Support for tilde (`~`) in configuration file paths
14
+
15
+ ### Changed
16
+
17
+ - Switched from YAML to TOML for configuration files
18
+
19
+ ## [0.17.0] - 2025-06-22
20
+
21
+ ### Added
22
+
23
+ - Support for `ffmpeg-normalize` for normalization (optional)
24
+
25
+ ### Changed
26
+
27
+ - Configuration options for normalization selection
28
+ - Configuration options for `ffmpeg-normalize`
@@ -0,0 +1,97 @@
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. TOML Configuration File (medium precedence)
24
+
25
+ - **Default location**: `~/.config/audiolibrarian/config.toml`
26
+ - **Example**:
27
+
28
+ ```toml
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 empty string for default device)
36
+ discid_device = ""
37
+
38
+ # Audio normalization settings
39
+ [normalize]
40
+ normalizer = "auto" # "auto", "wavegain", "ffmpeg", or "none"
41
+
42
+ [normalize.ffmpeg]
43
+ target_level = -13 # Target LUFS level for ffmpeg normalization
44
+
45
+ [normalize.wavegain]
46
+ gain = 5 # dB gain for wavegain normalization (0-10)
47
+ preset = "radio" # "album" or "radio"
48
+
49
+ # MusicBrainz API settings (optional)
50
+ [musicbrainz]
51
+ username = "your_username" # For personal genre preferences
52
+ password = "your_password" # Will be stored securely
53
+ rate_limit = 1.5 # Seconds between API requests
54
+ ```
55
+
56
+ ### 3. Default Values (lowest precedence)
57
+
58
+ - Built-in defaults from the application
59
+
60
+ ## Available Settings
61
+
62
+ | Setting | Default | Description |
63
+ |---------------------------------|------------------|-------------------------------------------|
64
+ | `library_dir` | `./library` | Directory for storing audio files |
65
+ | `work_dir` | (see below)[^wd] | Directory for temporary files |
66
+ | `discid_device` | `` | CD device path (null for default device) |
67
+ | `normalize.normalizer` | `"auto"` | "auto", "wavegain", "ffmpeg", or "none" |
68
+ | `normalize.ffmpeg.target_level` | `-13` | Target LUFS level (ffmpeg) |
69
+ | `normalize.wavegain.gain` | `5` | Normalization gain in dB (0-10, wavegain) |
70
+ | `normalize.wavegain.preset` | `"radio"` | "album" or "radio" (wavegain) |
71
+ | `musicbrainz.username` | (not set) | MusicBrainz username[^mb] |
72
+ | `musicbrainz.password` | (not set) | MusicBrainz password[^mb] |
73
+ | `musicbrainz.rate_limit` | `1.5` | Seconds between requests |
74
+
75
+ [^wd]: The `work_dir` default is `$XDG_CACHE_HOME/audiolibrarian`, which defaults
76
+ to `~/.cache/audiolibrarian` on Linux and macOS.
77
+
78
+ [^mb]: The `musicbrainz` username and password are optional but recommended for accessing personal genre
79
+ preferences on [MusicBrainz](https://musicbrainz.org/).
80
+
81
+ ## Audio Normalization
82
+
83
+ Audio normalization ensures consistent volume levels across tracks. The following options are available:
84
+
85
+ - `auto` (default): Automatically selects the best available normalizer (prefers wavegain)
86
+ - `wavegain`: Uses the `wavegain` command-line tool (recommended for better album normalization)
87
+ - `ffmpeg`: Uses the `ffmpeg-normalize` Python package
88
+ - `none`: Skips normalization entirely
89
+
90
+ ### Wavegain Settings
91
+
92
+ - `gain`: Adjusts the target volume level (in dB, typically 0-10)
93
+ - `preset`: "album" (loudness normalization) or "radio" (peak normalization)
94
+
95
+ ### FFmpeg Settings
96
+
97
+ - `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,12 +7,13 @@ 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",
16
+ "pytest-mock",
16
17
  "ruff",
17
18
  "types-pyyaml",
18
19
  "types-requests",
@@ -30,6 +31,7 @@ description = "Manage my audio library."
30
31
  dependencies = [
31
32
  "ansicolors",
32
33
  "discid",
34
+ "ffmpeg-normalize",
33
35
  "filelock",
34
36
  "fuzzywuzzy",
35
37
  "musicbrainzngs",
@@ -48,7 +50,7 @@ readme = "README.md"
48
50
  requires-python = ">=3.12,<3.14"
49
51
 
50
52
  [project.scripts]
51
- audiolibrarian = "audiolibrarian:cli.main"
53
+ audiolibrarian = "audiolibrarian.entrypoints.cli:main"
52
54
 
53
55
  [project.urls]
54
56
  Repository = "https://github.com/toadstule/audiolibrarian"
@@ -85,6 +87,7 @@ ignore_errors = true # This is not our code.
85
87
  [tool.pymarkdown]
86
88
  plugins.md013.line_length = 100
87
89
  plugins.md013.code_block_line_length = 100
90
+ plugins.md024.enabled = false
88
91
  #plugins.md007.enabled = true
89
92
  #plugins.md007.code_block_line_length = 160
90
93
 
@@ -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.18.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
  #
@@ -27,8 +27,7 @@ from collections.abc import Callable # noqa: TC003
27
27
 
28
28
  import discid
29
29
 
30
- from audiolibrarian import audiofile, records, sh, text
31
- from audiolibrarian.settings import SETTINGS
30
+ from audiolibrarian import audiofile, config, records, sh, text
32
31
 
33
32
  log = logging.getLogger(__name__)
34
33
 
@@ -93,10 +92,10 @@ class AudioSource(abc.ABC):
93
92
  class CDAudioSource(AudioSource):
94
93
  """AudioSource from a compact disc."""
95
94
 
96
- def __init__(self) -> None:
95
+ def __init__(self, settings: config.Settings) -> None:
97
96
  """Initialize a CDAudioSource."""
98
97
  super().__init__()
99
- self._cd = discid.read(SETTINGS.discid_device, features=["mcn"])
98
+ self._cd = discid.read(settings.discid_device or None, features=["mcn"])
100
99
 
101
100
  def get_search_data(self) -> dict[str, str]:
102
101
  """Return a dictionary of search data useful for doing a MusicBrainz search."""
@@ -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
  #
@@ -23,18 +23,25 @@ import argparse
23
23
  import logging
24
24
  import pathlib
25
25
  import shutil
26
- import subprocess
27
26
  import sys
28
27
  import warnings
29
28
  from collections.abc import Iterable
30
- from typing import Any
29
+ from typing import Any, Final
31
30
 
32
31
  import colors
33
32
  import filelock
34
33
  import yaml
35
34
 
36
- from audiolibrarian import audiofile, audiosource, musicbrainz, records, sh, text
37
- from audiolibrarian.settings import SETTINGS
35
+ from audiolibrarian import (
36
+ audiofile,
37
+ audiosource,
38
+ config,
39
+ musicbrainz,
40
+ normalizer,
41
+ records,
42
+ sh,
43
+ text,
44
+ )
38
45
 
39
46
  log = logging.getLogger(__name__)
40
47
 
@@ -46,9 +53,11 @@ class Base:
46
53
  """
47
54
 
48
55
  command: str | None = None
56
+ _manifest_file: Final[str] = "Manifest.yaml"
49
57
 
50
- def __init__(self, args: argparse.Namespace) -> None:
58
+ def __init__(self, args: argparse.Namespace, settings: config.Settings) -> None:
51
59
  """Initialize the base."""
60
+ self._settings = settings
52
61
  # Pull in stuff from args.
53
62
  search_keys = ("album", "artist", "mb_artist_id", "mb_release_id")
54
63
  self._provided_search_data = {k: v for k, v in vars(args).items() if k in search_keys}
@@ -58,17 +67,18 @@ class Base:
58
67
  self._disc_number, self._disc_count = 1, 1
59
68
 
60
69
  # Directories.
61
- self._library_dir = SETTINGS.library_dir
62
- self._work_dir = SETTINGS.work_dir
70
+ self._library_dir = self._settings.library_dir
71
+ self._work_dir = self._settings.work_dir
63
72
  self._flac_dir = self._work_dir / "flac"
64
73
  self._m4a_dir = self._work_dir / "m4a"
65
74
  self._mp3_dir = self._work_dir / "mp3"
66
75
  self._source_dir = self._work_dir / "source"
67
76
  self._wav_dir = self._work_dir / "wav"
68
77
 
69
- self._manifest_file = "Manifest.yaml"
70
78
  self._lock = filelock.FileLock(str(self._work_dir) + ".lock")
71
79
 
80
+ self._normalizer = normalizer.Normalizer.factory(self._settings.normalize)
81
+
72
82
  # Initialize stuff that will be defined later.
73
83
  self._audio_source: audiosource.AudioSource | None = None
74
84
  self._release: records.Release | None = None
@@ -126,24 +136,6 @@ class Base:
126
136
  self._make_mp3()
127
137
  self._move_files(move_source=make_source)
128
138
 
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
139
  def _find_manifests(self, directories: list[str | pathlib.Path]) -> list[pathlib.Path]:
148
140
  """Return a sorted, unique list of manifest files anywhere in the given directories."""
149
141
  manifests = set()
@@ -158,7 +150,7 @@ class Base:
158
150
  search_data: dict[str, str] = (
159
151
  self._audio_source.get_search_data() if self._audio_source is not None else {}
160
152
  )
161
- searcher = musicbrainz.Searcher(**search_data) # type: ignore[arg-type]
153
+ searcher = musicbrainz.Searcher(settings=self._settings.musicbrainz, **search_data) # type: ignore[arg-type]
162
154
  searcher.disc_number = str(self._disc_number)
163
155
  # Override with user-provided info.
164
156
  if value := self._provided_search_data.get("artist"):
@@ -283,25 +275,8 @@ class Base:
283
275
  path.rename(source_dir / path.name)
284
276
 
285
277
  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()
300
-
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))
278
+ """Normalize the wav files using the selected normalizer."""
279
+ self._normalizer.normalize(set(self._wav_filenames))
305
280
 
306
281
  def _rename_wav(self) -> None:
307
282
  """Rename the wav files to a filename-sane representation of the track title."""
@@ -431,3 +406,26 @@ class Base:
431
406
  with pathlib.Path(manifest_filename).open("w", encoding="utf-8") as manifest_file:
432
407
  yaml.dump(manifest, manifest_file)
433
408
  print(f"Wrote {manifest_filename}")
409
+
410
+ @staticmethod
411
+ def _find_audio_files(directories: list[str | pathlib.Path]) -> Iterable[audiofile.AudioFile]:
412
+ """Yield audiofile objects found in the given directories."""
413
+ paths: list[pathlib.Path] = []
414
+ # Grab all the paths first because thing may change as files are renamed.
415
+ for directory in directories:
416
+ path = pathlib.Path(directory)
417
+ for ext in audiofile.AudioFile.extensions():
418
+ paths.extend(path.rglob(f"*{ext}"))
419
+ paths = sorted(set(paths))
420
+ # Using yield rather than returning a list saves us from simultaneously storing
421
+ # potentially thousands of AudioFile objects in memory at the same time.
422
+ for path in paths:
423
+ try:
424
+ yield audiofile.AudioFile.open(path)
425
+ except FileNotFoundError:
426
+ continue
427
+
428
+ @staticmethod
429
+ def _read_manifest(manifest_path: pathlib.Path) -> dict[Any, Any]:
430
+ with manifest_path.open(encoding="utf-8") as manifest_file:
431
+ return dict(yaml.safe_load(manifest_file))