Skip to content

Commit 50b44b0

Browse files
committed
feat: add yaml configuration
1 parent faf062f commit 50b44b0

File tree

6 files changed

+158
-132
lines changed

6 files changed

+158
-132
lines changed

config/config.yaml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
# Heartbeets Configuration
3+
4+
timezone: Europe/Vienna
5+
log-level: INFO
6+
dry-run: True
7+
pref-extensions: [mp3, flac]
8+
remove-extensions: [m4a, wma]
9+
data: ./data
10+
music: ./music
11+
12+
beets:
13+
config: ./config/beets/docker.config.yaml
14+
database: ./data/beets/library.db
15+
base-path: ./music
16+
17+
navidrome:
18+
database: ./navidrome/navidrome.db
19+
base-path: ./music/library

src/ndtoolbox/app.py

Lines changed: 61 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
from datetime import datetime
99

1010
import jsonpickle
11+
import tomli
1112
from easydict import EasyDict
1213

14+
from ndtoolbox.config import config
1315
from ndtoolbox.db import NavidromeDb, NavidromeDbConnection
1416
from ndtoolbox.model import Annotation, Folder, MediaFile
15-
from ndtoolbox.utils import CLI, FileTools, FileUtil, Stats, ToolboxConfig
17+
from ndtoolbox.utils import CLI, FileTools, FileUtil, PrintUtil, Stats
1618
from ndtoolbox.utils import PrintUtil as PU
1719
from ndtoolbox.utils import StringUtil as SU
1820

@@ -48,13 +50,12 @@ class DuplicateProcessor:
4850
stop (float): The timestamp when processing stopped. This is set at the end of an action or an error occurs.
4951
"""
5052

51-
config: ToolboxConfig
5253
db: NavidromeDb
5354
data: DuplicateData
5455
stats: Stats
5556
errors: list
5657

57-
def __init__(self, config: ToolboxConfig):
58+
def __init__(self):
5859
"""
5960
Initialize the DuplicateProcessor with a database and an input file containing duplicate media files.
6061
@@ -67,10 +68,11 @@ def __init__(self, config: ToolboxConfig):
6768
base_path_navidrome (str): The actual location in the Navidrome music library.
6869
dry_run (bool): If True, no actual file operations will be performed.
6970
"""
70-
self.config = config
71+
# self.config = config
7172
self.data = DuplicateData()
7273
self.errors = []
73-
self.db = NavidromeDb(config.navidrome_db_path, config, self)
74+
navidrome_db = config["navidrome"]["database"].get(str)
75+
self.db = NavidromeDb(navidrome_db, self)
7476
self.stats = Stats(self)
7577

7678
def merge_and_store_annotations(self):
@@ -120,7 +122,7 @@ def eval_deletable_duplicates(self):
120122
keepable = self._get_keepable_media(dups)
121123
PU.log(f"<- Found keepable: {keepable.path}", 0)
122124

123-
file_path = os.path.join(self.config.data_folder, "duplicates-with-keepers.json")
125+
file_path = os.path.join(config["data"].get(str), "duplicates-with-keepers.json")
124126
with open(file_path, "w", encoding="utf-8") as file:
125127
file.write(jsonpickle.encode(self.data.media, indent=4))
126128

@@ -165,18 +167,18 @@ def load_navidrome_database(self):
165167
PU.ln()
166168

167169
# Read raw duplicates info generated by the Beets `duplicatez` plugin.
168-
PU.info(f"Reading duplicates from Beets JSON file: {self.config.FILE_BEETS_INPUT_JSON}")
170+
PU.info(f"Reading duplicates from Beets JSON file: {config["FILE_BEETS_INPUT_JSON"].get(str)}")
169171
# Read the input JSON file containing duplicate media files references from Beets.
170-
with open(self.config.FILE_BEETS_INPUT_JSON, "r", encoding="utf-8") as file:
172+
with open(config["FILE_BEETS_INPUT_JSON"].get(str), "r", encoding="utf-8") as file:
171173
dups_input = json.load(file)
172174
if not dups_input:
173-
PU.error(f"No duplicates found in input file '{self.config.FILE_BEETS_INPUT_JSON}'")
175+
PU.error(f"No duplicates found in input file '{config["FILE_BEETS_INPUT_JSON"].get(str)}'")
174176
PU.info("Please generate the duplicates info using Beets `duplicatez` plugin first.")
175177
sys.exit(1)
176178

177179
# Check for existing data file.
178-
if os.path.isfile(self.config.FILE_TOOLBOX_DATA_JSON):
179-
PU.note(f"Data file '{self.config.FILE_TOOLBOX_DATA_JSON}' existing already.")
180+
if os.path.isfile(config["FILE_TOOLBOX_DATA_JSON"].get(str)):
181+
PU.note(f"Data file '{config["FILE_TOOLBOX_DATA_JSON"].get(str)}' existing already.")
180182
PU.note("Do you want to continue anyway and overwrite existing data?")
181183
CLI.ask_continue()
182184

@@ -186,14 +188,14 @@ def load_navidrome_database(self):
186188

187189
# Persist data.
188190
data = {"dups_media_files": self.data.media, "stats": self.stats, "cache": self.data}
189-
with open(self.config.FILE_TOOLBOX_DATA_JSON, "w", encoding="utf-8") as file:
191+
with open(config["FILE_TOOLBOX_DATA_JSON"].get(str), "w", encoding="utf-8") as file:
190192
file.write(jsonpickle.encode(data, indent=4, keys=True))
191-
PU.success(f"Stored Navidrome data to '{self.config.FILE_TOOLBOX_DATA_JSON}'")
193+
PU.success(f"Stored Navidrome data to '{config["FILE_TOOLBOX_DATA_JSON"].get(str)}'")
192194
self.stats.stop()
193195
self.stats.print_stats()
194196
if self._has_errors():
195-
PU.error(f"Please review {len(self.errors)} errors in {self.config.ERROR_REPORT_JSON} ... ")
196-
with open(self.config.ERROR_REPORT_JSON, "w") as f:
197+
PU.error(f"Please review {len(self.errors)} errors in {config["ERROR_REPORT_JSON"].get(str)}...")
198+
with open(config["ERROR_REPORT_JSON"].get(str), "w") as f:
197199
json.dump(self.errors, f, indent=4)
198200
else:
199201
PU.success("No errors found.")
@@ -204,20 +206,20 @@ def _load_navidrome_data_file(self):
204206
Load data from a previously saved JSON file.
205207
"""
206208
# Check for existing errors file.
207-
if os.path.isfile(self.config.ERROR_REPORT_JSON):
208-
PU.error(f"Errors file '{self.config.ERROR_REPORT_JSON}' found.")
209+
if os.path.isfile(config["ERROR_REPORT_JSON"].get(str)):
210+
PU.error(f"Errors file '{config["ERROR_REPORT_JSON"].get(str)}' found.")
209211
PU.note("Have you checked the errors and decided to continue anyway?")
210212
CLI.ask_continue()
211213

212214
PU.bold("Loading duplicate records from JSON file")
213215
PU.ln()
214216
# Load data from JSON file.
215-
with open(self.config.FILE_TOOLBOX_DATA_JSON, "r", encoding="utf-8") as file:
217+
with open(config["FILE_TOOLBOX_DATA_JSON"].get(str), "r", encoding="utf-8") as file:
216218
data = jsonpickle.decode(file.read())
217219
self.data.media = data["dups_media_files"]
218220
self.stats = data["stats"]
219221
self.data = data["cache"]
220-
PU.success(f"Loaded duplicate records from '{self.config.FILE_TOOLBOX_DATA_JSON}'")
222+
PU.success(f"Loaded duplicate records from '{config["FILE_TOOLBOX_DATA_JSON"].get(str)}'")
221223

222224
def _split_duplicates_by_album_folder(
223225
self, dups_media_files: dict[str, list[MediaFile]]
@@ -293,18 +295,22 @@ def _replace_base_path(self, dups_input: dict[str, list[MediaFile]]):
293295
dups_input (dict[str, list[str]]): A dictionary where the keys are duplicate identifiers and the values
294296
are lists of file paths.
295297
"""
296-
if not self.config.base_path_beets or not self.config.base_path_navidrome:
298+
if not config["beets"]["base-path"].get(str) or not config["navidrome"]["base-path"].get(str):
297299
PU.warning("Skipping base path update, since no paths are set")
298300
return
299-
if self.config.base_path_beets == self.config.base_path_navidrome:
301+
if config["beets"]["base-path"].get(str) == config["navidrome"]["base-path"].get(str):
300302
PU.warning("Skipping base path update as target equals source")
301303
return
302304

303305
for paths in dups_input.values():
304306
item: str
305307
for i, item in enumerate(paths):
306-
paths[i] = item.replace(self.config.base_path_beets, self.config.base_path_navidrome, 1)
307-
PU.info(f"Updated all base paths from '{self.config.base_path_beets}' to '{self.config.base_path_navidrome}'.")
308+
paths[i] = item.replace(
309+
config["beets"]["base-path"].get(str), config["navidrome"]["base-path"].get(str), 1
310+
)
311+
PU.info(
312+
f"Updated all base paths from '{config["beets"]["base-path"].get(str)}' to '{config["navidrome"]["base-path"].get(str)}'."
313+
)
308314

309315
def _query_media_data(self, dups_input: dict[str, list[str]]):
310316
"""
@@ -532,8 +538,8 @@ def is_keepable(self, this: MediaFile, that: MediaFile) -> MediaFile:
532538
# Skip, if none is a suffixed path
533539

534540
# Having a preferred file extension is keepable
535-
left = this.path.split(".")[-1].lower() in ToolboxConfig.pref_extensions
536-
right = that.path.split(".")[-1].lower() in ToolboxConfig.pref_extensions
541+
left = this.path.split(".")[-1].lower() in config["pref-extensions"].get(list)
542+
right = that.path.split(".")[-1].lower() in config["pref-extensions"].get(list)
537543
PU.log(f"Compare if file extension is keepable: {left} || {right}", 1)
538544
if left != right:
539545
if left:
@@ -652,9 +658,32 @@ def is_keepable(self, this: MediaFile, that: MediaFile) -> MediaFile:
652658
this.delete_reason = f"No reason, since no condition matched | {SU.gray(that.path)}"
653659
return that
654660

661+
# self.init_logger()
662+
# self.print_info()
663+
664+
665+
def print_info():
666+
"""Prints the current configuration details."""
667+
# Print app version
668+
with open("pyproject.toml", mode="rb") as file:
669+
data = tomli.load(file)
670+
version = data["tool"]["poetry"]["version"]
671+
PrintUtil.ln()
672+
PrintUtil.bold(f" Heartbeets v{version}")
673+
PrintUtil.ln()
674+
675+
# Print config details
676+
PrintUtil.bold("\nInitializing configuration")
677+
PrintUtil.ln()
678+
PrintUtil.info(f"Dry-run: {config["dry-run"].get(bool)}")
679+
PrintUtil.info(f"Navidrome database path: {config["navidrome"]["database"].get(str)}")
680+
PrintUtil.info(f"Output folder: {config["data"].get(str)}")
681+
PrintUtil.info(f"Beets library root: {config["beets"]["base-path"].get(str)}")
682+
PrintUtil.info(f"Navidrome library root: {config["navidrome"]["base-path"].get(str)}")
683+
655684

656685
if __name__ == "__main__":
657-
config = ToolboxConfig()
686+
print_info()
658687

659688
# Read the action argument from the command line
660689
action = ""
@@ -666,12 +695,14 @@ def is_keepable(self, this: MediaFile, that: MediaFile) -> MediaFile:
666695
PU.error("No action specified. Please use the format 'python app.py action=<action>'.")
667696
sys.exit(1)
668697

669-
processor = DuplicateProcessor(config)
670-
# client = BeetsClient(config)
671-
# print("BEETS CLIENT: " + client._query1())
698+
processor = DuplicateProcessor()
672699

673700
if action == "remove-unsupported":
674-
FileTools.move_by_extension(config.music_dir, config.data_dir, config.remove_extensions, ToolboxConfig.dry_run)
701+
music = config["music"].get(str)
702+
data = config["data"].get(str)
703+
remove_ext = config["remove-extensions"].get(list)
704+
dry_run = config["music"].get(str)
705+
FileTools.move_by_extension(music, data, remove_ext, dry_run)
675706
elif action == "load-duplicates":
676707
processor.load_navidrome_database()
677708
elif action == "merge-annotations":

src/ndtoolbox/config.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
"""App configuration."""
2+
3+
import logging
4+
import os
5+
6+
import colorlog
7+
import confuse
8+
9+
10+
class Config(confuse.Configuration):
11+
"""
12+
Configuration for application based on Confuse.
13+
"""
14+
15+
logger: logging.Logger = None
16+
17+
def init(self):
18+
"""Init configuration."""
19+
dir_data = self["data"].get(str)
20+
file_duplicates_json = os.path.join(dir_data, "beets/beets-duplicates.json")
21+
file_data_json = os.path.join(dir_data, "nd-toolbox-data.json")
22+
file_error_json = os.path.join(dir_data, "nd-toolbox-error.json")
23+
file_log = os.path.join(dir_data, "nd-toolbox.log")
24+
self.set_args(
25+
{
26+
"FILE_BEETS_INPUT_JSON": file_duplicates_json,
27+
"FILE_TOOLBOX_DATA_JSON": file_data_json,
28+
"ERROR_REPORT_JSON": file_error_json,
29+
"file-log": file_log,
30+
}
31+
)
32+
33+
def init_logger(self):
34+
"""Setup logger."""
35+
log_level = self["log-level"].get(str)
36+
file_log = self["file-log"].get(str)
37+
if log_level not in logging._nameToLevel:
38+
raise ValueError(f"Invalid log-level: {log_level}")
39+
self.logger = colorlog.getLogger("ndtoolbox")
40+
41+
colorlog.basicConfig(
42+
filename=file_log,
43+
filemode="w",
44+
encoding="utf-8",
45+
level=log_level,
46+
format="%(log_color)s %(msecs)d %(name)s %(levelname)s %(message)s",
47+
)
48+
self.logger.info(f"Initialized logger with level: {log_level} and log file: {file_log}")
49+
50+
51+
config = Config("Heartbeets")
52+
config.set_file("config/config.yaml")
53+
config.init()
54+
config.init_logger()

src/ndtoolbox/db.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,10 @@
66
import unicodedata
77
from typing import Generator
88

9+
from ndtoolbox.config import config
910
from ndtoolbox.model import Album, Annotation, Artist, Folder, MediaFile
1011
from ndtoolbox.utils import DateUtil as DU
11-
from ndtoolbox.utils import FileUtil, ToolboxConfig
12+
from ndtoolbox.utils import FileUtil
1213
from ndtoolbox.utils import PrintUtil as PU
1314

1415

@@ -57,20 +58,18 @@ class NavidromeDb:
5758
"""
5859

5960
db_path: str
60-
config: ToolboxConfig
6161
app: object
6262
user_id: str
6363
conn: NavidromeDbConnection
6464

65-
def __init__(self, db_path: str, config: ToolboxConfig, app):
65+
def __init__(self, db_path: str, app):
6666
"""
6767
Initialize the database connection and set the user ID.
6868
6969
Args:
7070
db_path (str): Path to the database file.
7171
"""
7272
NavidromeDbConnection.db_path = db_path
73-
self.config = config
7473
self.app = app
7574
self.user_id = self.init_user()
7675

@@ -166,7 +165,9 @@ def get_media_batch(self, file_paths: list[str], conn: NavidromeDbConnection) ->
166165
# and `á` (`\u00e1`) are not threaded as the same.
167166
nd_path = unicodedata.normalize("NFC", beets_path)
168167
# Restore the Beets prefix
169-
beets_path = beets_path.replace(self.config.base_path_navidrome, self.config.base_path_beets, 1)
168+
beets_path = beets_path.replace(
169+
config["navidrome"]["base-path"].get(str), config["beets"]["base-path"].get(str), 1
170+
)
170171
path_mapping[nd_path] = beets_path
171172

172173
cursor = conn.cursor()

src/ndtoolbox/model.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22
Model classes representing the Navidrome database.
33
"""
44

5-
import os
65
from datetime import datetime
76
from enum import Enum
87
from typing import Optional
98

109
from ndtoolbox.beets import BeetsClient
10+
from ndtoolbox.config import config
1111
from ndtoolbox.utils import DateUtil as DU
12-
from ndtoolbox.utils import FileUtil, ToolboxConfig
12+
from ndtoolbox.utils import FileUtil
1313
from ndtoolbox.utils import PrintUtil as PU
1414

1515

@@ -249,11 +249,11 @@ def __init__(self, media: MediaFile):
249249

250250
# Set folder type
251251
self.type = Folder.Type.ALBUM
252-
if self.beets_path == ToolboxConfig.base_path_beets:
252+
if self.beets_path == config["beets"]["base-path"].get(str):
253253
self.type = Folder.Type.ROOT
254-
elif FileUtil.is_artist_folder(ToolboxConfig.base_path_beets, self.beets_path):
254+
elif FileUtil.is_artist_folder(config["beets"]["base-path"].get(str), self.beets_path):
255255
self.type = Folder.Type.ARTIST
256-
elif FileUtil.is_album_folder(ToolboxConfig.base_path_beets, self.beets_path):
256+
elif FileUtil.is_album_folder(config["base-path"].get(str), self.beets_path):
257257
self.type = Folder.Type.ALBUM
258258
else:
259259
self.type = Folder.Type.UNKNOWN

0 commit comments

Comments
 (0)