11# auto_tag/audio_recognize.py
22"""
33Recognise audio files with Shazam, optionally rename or copy them,
4- and update metadata (tags, cover art) using `eyed3` and `mutagen` .
4+ and update metadata (tags, cover art).
55
66OGG files are converted to WAV via soundfile/libsndfile (no ffmpeg),
7- but if conversion or recognition on WAV fails, we fall back to using
8- the original OGG for recognition—so tests with DummyShazam still work.
7+ but if conversion or recognition on WAV fails, we fall back to the
8+ original OGG for recognition—so tests with DummyShazam still work.
99"""
1010
1111from __future__ import annotations
@@ -41,20 +41,27 @@ async def find_and_recognize_audio_files(
4141 plex_structure : bool = False ,
4242 copy_to : str | None = None ,
4343) -> None :
44+ """
45+ Walk folder_path, recognise each file, then move or copy/tag it.
46+ copy_to, if given, is the base dir to copy files into (instead of moving).
47+ """
4448 exts = {e .lower ().lstrip ("." ) for e in extensions }
4549 audio_files : list [str ] = []
50+
4651 for root , _ , files in os .walk (folder_path ):
4752 if "test" in os .path .basename (root ).lower ():
4853 continue
4954 for fn in files :
5055 if os .path .splitext (fn )[1 ].lower ().lstrip ("." ) in exts :
5156 audio_files .append (os .path .join (root , fn ))
57+
5258 if not audio_files :
5359 print (f"No files with extensions { exts } found in { folder_path } ." )
5460 return
5561
5662 shazam = Shazam ()
5763 ok = 0
64+
5865 for path in tqdm (audio_files , desc = "Recognising and renaming" ):
5966 res = await recognize_and_rename_file (
6067 file_path = path ,
@@ -87,10 +94,16 @@ async def recognize_and_rename_file(
8794 plex_structure : bool ,
8895 copy_to : str | None = None ,
8996) -> dict :
97+ """
98+ Recognise file_path with Shazam, then move or copy & tag it.
99+ - If copy_to is set, the file is **copied** to that directory (with or
100+ without Plex subfolders) and the original remains untouched.
101+ - Otherwise it is **moved** (renamed) in place or under the output_dir.
102+ """
90103 ext = os .path .splitext (file_path )[1 ].lower ()
91104 tmp_wav : str | None = None
92105
93- # Prepare for recognition: try WAV conversion for OGG
106+ # 1) For OGG, try to convert to WAV first
94107 input_path = file_path
95108 if ext == ".ogg" :
96109 fd , tmp_wav = tempfile .mkstemp (suffix = ".wav" )
@@ -102,16 +115,15 @@ async def recognize_and_rename_file(
102115 except Exception as exc :
103116 if trace :
104117 print (f"[{ os .path .basename (file_path )} ] OGG→WAV failed: { exc } " )
105- input_path = file_path # fallback to original
118+ input_path = file_path # fallback
106119
107- # Attempt recognition (first on input_path, then if OGG and failed, on
108- # original)
120+ # 2) Recognise with retries
109121 out = None
110122 for attempt in range (1 , nbr_retry + 1 ):
111123 try :
112- res = await shazam .recognize (input_path )
113- if res :
114- out = res
124+ candidate = await shazam .recognize (input_path )
125+ if candidate :
126+ out = candidate
115127 break
116128 except Exception as exc :
117129 if trace :
@@ -121,24 +133,24 @@ async def recognize_and_rename_file(
121133 if attempt < nbr_retry :
122134 await asyncio .sleep (delay )
123135
124- # If OGG and first recognition on WAV failed, try on original OGG
136+ # Fallback for OGG if WAV recognition failed
125137 if ext == ".ogg" and out is None and input_path != file_path :
126138 for attempt in range (1 , nbr_retry + 1 ):
127139 try :
128- res = await shazam .recognize (file_path )
129- if res :
130- out = res
140+ candidate = await shazam .recognize (file_path )
141+ if candidate :
142+ out = candidate
131143 break
132144 except Exception as exc :
133145 if trace :
134146 print (
135- f"[{ os .path .basename (file_path )} ] OGG original attempt "
136- f"{ attempt } : { exc } "
147+ f"[{ os .path .basename (file_path )} ] OGG fallback "
148+ f" attempt { attempt } : { exc } "
137149 )
138150 if attempt < nbr_retry :
139151 await asyncio .sleep (delay )
140152
141- # Clean up temp WAV
153+ # cleanup
142154 if tmp_wav and os .path .exists (tmp_wav ):
143155 os .remove (tmp_wav )
144156
@@ -147,7 +159,7 @@ async def recognize_and_rename_file(
147159 print (f"Shazam failed: { file_path } " )
148160 return {"file_path" : file_path , "error" : "Recognition failed" }
149161
150- # Extract metadata
162+ # 3) Extract metadata
151163 track = out ["track" ]
152164 title = track .get ("title" , "Unknown Title" )
153165 artist = track .get ("subtitle" , "Unknown Artist" )
@@ -158,27 +170,27 @@ async def recognize_and_rename_file(
158170 s_artist = sanitize (artist , trace )
159171 s_album = sanitize (album , trace )
160172
161- # Build new filename
173+ # 4) Build filename
162174 if plex_structure :
163175 new_name = f"{ s_title } { ext } "
164176 else :
165177 new_name = f"{ s_title } - { s_artist } - { s_album } { ext } "
166178
167- # Determine target directory
168- base_dir = copy_to or output_dir or os .path .dirname (file_path )
179+ # 5) Determine the root folder for the new file
180+ root_dir = copy_to or output_dir or os .path .dirname (file_path )
169181 if plex_structure :
170- base_dir = os .path .join (base_dir , s_artist , s_album )
171- os .makedirs (base_dir , exist_ok = True )
182+ root_dir = os .path .join (root_dir , s_artist , s_album )
183+ os .makedirs (root_dir , exist_ok = True )
172184
173- # Ensure unique filename
174- new_path = os .path .join (base_dir , new_name )
185+ # 6) Ensure uniqueness
186+ new_path = os .path .join (root_dir , new_name )
175187 count = 1
176188 while os .path .exists (new_path ) and new_path != file_path :
177- stem , ext2 = os .path .splitext (new_path )
178- new_path = f"{ stem } ({ count } ){ ext2 } "
189+ stem , e2 = os .path .splitext (new_path )
190+ new_path = f"{ stem } ({ count } ){ e2 } "
179191 count += 1
180192
181- # Move or copy & tag
193+ # 7) Move or copy & tag
182194 if modify :
183195 try :
184196 if copy_to :
@@ -243,6 +255,7 @@ def update_ogg_tags(
243255 cover_url : str ,
244256 trace : bool ,
245257) -> None :
258+ # Try Vorbis, then Opus, then generic
246259 try :
247260 audio = OggVorbis (file_path )
248261 except Exception :
@@ -253,9 +266,10 @@ def update_ogg_tags(
253266 if audio is None :
254267 raise RuntimeError ("Unsupported OGG type for tagging" )
255268
256- audio ["TITLE" ] = title
257- audio ["ARTIST" ] = artist
258- audio ["ALBUM" ] = album
269+ # Mutagen expects tag values as lists
270+ audio ["TITLE" ] = [title ]
271+ audio ["ARTIST" ] = [artist ]
272+ audio ["ALBUM" ] = [album ]
259273
260274 if cover_url :
261275 try :
@@ -265,9 +279,8 @@ def update_ogg_tags(
265279 pic .type = 3
266280 pic .mime = "image/jpeg"
267281 pic .width = pic .height = pic .depth = pic .colors = 0
268- audio ["METADATA_BLOCK_PICTURE" ] = [
269- base64 .b64encode (pic .write ()).decode ()
270- ]
282+ b64 = base64 .b64encode (pic .write ()).decode ("ascii" )
283+ audio ["METADATA_BLOCK_PICTURE" ] = [b64 ]
271284 except Exception as exc :
272285 if trace :
273286 print ("Cover art error:" , exc )
0 commit comments