audiolibrarian 0.16.4__py3-none-any.whl → 0.17.0__py3-none-any.whl

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