8
8
from datetime import datetime
9
9
10
10
import jsonpickle
11
+ import tomli
11
12
from easydict import EasyDict
12
13
14
+ from ndtoolbox .config import config
13
15
from ndtoolbox .db import NavidromeDb , NavidromeDbConnection
14
16
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
16
18
from ndtoolbox .utils import PrintUtil as PU
17
19
from ndtoolbox .utils import StringUtil as SU
18
20
@@ -48,13 +50,12 @@ class DuplicateProcessor:
48
50
stop (float): The timestamp when processing stopped. This is set at the end of an action or an error occurs.
49
51
"""
50
52
51
- config : ToolboxConfig
52
53
db : NavidromeDb
53
54
data : DuplicateData
54
55
stats : Stats
55
56
errors : list
56
57
57
- def __init__ (self , config : ToolboxConfig ):
58
+ def __init__ (self ):
58
59
"""
59
60
Initialize the DuplicateProcessor with a database and an input file containing duplicate media files.
60
61
@@ -67,10 +68,11 @@ def __init__(self, config: ToolboxConfig):
67
68
base_path_navidrome (str): The actual location in the Navidrome music library.
68
69
dry_run (bool): If True, no actual file operations will be performed.
69
70
"""
70
- self .config = config
71
+ # self.config = config
71
72
self .data = DuplicateData ()
72
73
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 )
74
76
self .stats = Stats (self )
75
77
76
78
def merge_and_store_annotations (self ):
@@ -120,7 +122,7 @@ def eval_deletable_duplicates(self):
120
122
keepable = self ._get_keepable_media (dups )
121
123
PU .log (f"<- Found keepable: { keepable .path } " , 0 )
122
124
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" )
124
126
with open (file_path , "w" , encoding = "utf-8" ) as file :
125
127
file .write (jsonpickle .encode (self .data .media , indent = 4 ))
126
128
@@ -165,18 +167,18 @@ def load_navidrome_database(self):
165
167
PU .ln ()
166
168
167
169
# 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 ) } " )
169
171
# 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 :
171
173
dups_input = json .load (file )
172
174
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 ) } '" )
174
176
PU .info ("Please generate the duplicates info using Beets `duplicatez` plugin first." )
175
177
sys .exit (1 )
176
178
177
179
# 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." )
180
182
PU .note ("Do you want to continue anyway and overwrite existing data?" )
181
183
CLI .ask_continue ()
182
184
@@ -186,14 +188,14 @@ def load_navidrome_database(self):
186
188
187
189
# Persist data.
188
190
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 :
190
192
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 ) } '" )
192
194
self .stats .stop ()
193
195
self .stats .print_stats ()
194
196
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 :
197
199
json .dump (self .errors , f , indent = 4 )
198
200
else :
199
201
PU .success ("No errors found." )
@@ -204,20 +206,20 @@ def _load_navidrome_data_file(self):
204
206
Load data from a previously saved JSON file.
205
207
"""
206
208
# 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." )
209
211
PU .note ("Have you checked the errors and decided to continue anyway?" )
210
212
CLI .ask_continue ()
211
213
212
214
PU .bold ("Loading duplicate records from JSON file" )
213
215
PU .ln ()
214
216
# 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 :
216
218
data = jsonpickle .decode (file .read ())
217
219
self .data .media = data ["dups_media_files" ]
218
220
self .stats = data ["stats" ]
219
221
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 ) } '" )
221
223
222
224
def _split_duplicates_by_album_folder (
223
225
self , dups_media_files : dict [str , list [MediaFile ]]
@@ -293,18 +295,22 @@ def _replace_base_path(self, dups_input: dict[str, list[MediaFile]]):
293
295
dups_input (dict[str, list[str]]): A dictionary where the keys are duplicate identifiers and the values
294
296
are lists of file paths.
295
297
"""
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 ) :
297
299
PU .warning ("Skipping base path update, since no paths are set" )
298
300
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 ) :
300
302
PU .warning ("Skipping base path update as target equals source" )
301
303
return
302
304
303
305
for paths in dups_input .values ():
304
306
item : str
305
307
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
+ )
308
314
309
315
def _query_media_data (self , dups_input : dict [str , list [str ]]):
310
316
"""
@@ -532,8 +538,8 @@ def is_keepable(self, this: MediaFile, that: MediaFile) -> MediaFile:
532
538
# Skip, if none is a suffixed path
533
539
534
540
# 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 )
537
543
PU .log (f"Compare if file extension is keepable: { left } || { right } " , 1 )
538
544
if left != right :
539
545
if left :
@@ -652,9 +658,32 @@ def is_keepable(self, this: MediaFile, that: MediaFile) -> MediaFile:
652
658
this .delete_reason = f"No reason, since no condition matched | { SU .gray (that .path )} "
653
659
return that
654
660
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 ("\n Initializing 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
+
655
684
656
685
if __name__ == "__main__" :
657
- config = ToolboxConfig ()
686
+ print_info ()
658
687
659
688
# Read the action argument from the command line
660
689
action = ""
@@ -666,12 +695,14 @@ def is_keepable(self, this: MediaFile, that: MediaFile) -> MediaFile:
666
695
PU .error ("No action specified. Please use the format 'python app.py action=<action>'." )
667
696
sys .exit (1 )
668
697
669
- processor = DuplicateProcessor (config )
670
- # client = BeetsClient(config)
671
- # print("BEETS CLIENT: " + client._query1())
698
+ processor = DuplicateProcessor ()
672
699
673
700
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 )
675
706
elif action == "load-duplicates" :
676
707
processor .load_navidrome_database ()
677
708
elif action == "merge-annotations" :
0 commit comments