Skip to content

Commit a7ad3ba

Browse files
authored
fix: work w/various dir structs + unicode error + overwritten log files (#18)
* fix: work w/various dir structs + unicode error + overwritten log files * build: update version number
1 parent 803ff60 commit a7ad3ba

File tree

2 files changed

+108
-12
lines changed

2 files changed

+108
-12
lines changed

ipod_wrapped/backend/log_analysis.py

Lines changed: 107 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -229,13 +229,63 @@ def parse_track_info(self, path: str) -> tuple:
229229
if music_idx >= 0:
230230
adjusted_sections = path_sections[music_idx + 1:]
231231

232-
# need at least Artist/Album/Track
233-
if len(adjusted_sections) < 3:
232+
# supported folder structures
233+
# - /Music/Artist/Album/Track.ext
234+
# - /Music/Artist - Album/Track.ext
235+
# - /Music/Track.ext
236+
if len(adjusted_sections) < 1:
234237
raise ValueError(f"Not enough sections after Music: {path}")
235238

236-
artist = adjusted_sections[0].strip()
237-
album = self.fix_explicit_label(adjusted_sections[1].strip())
238-
track_filename = adjusted_sections[2]
239+
if len(adjusted_sections) == 1:
240+
# files directly in /Music/
241+
track_filename = adjusted_sections[0]
242+
# extract metadata from filename
243+
if ' - ' in track_filename:
244+
parts = track_filename.split(' - ')
245+
if len(parts) >= 3:
246+
# "Artist - Album - Song.ext" format
247+
artist = parts[0].strip()
248+
album = self.fix_explicit_label(parts[1].strip())
249+
elif len(parts) == 2:
250+
# "Artist - Song.ext" format
251+
artist = parts[0].strip()
252+
album = "Unknown Album"
253+
else:
254+
artist = "Unknown Artist"
255+
album = "Unknown Album"
256+
else:
257+
artist = "Unknown Artist"
258+
album = "Unknown Album"
259+
elif len(adjusted_sections) == 2:
260+
# files in /Music/Artist - Album/ or /Music/Various Artists/Album/
261+
folder_name = adjusted_sections[0].strip()
262+
track_filename = adjusted_sections[1]
263+
264+
# edge cases: folders like "Various Artists" or "Soundtracks" (from google lol)
265+
# cannot guarantee the below at all :sob:
266+
if folder_name in ["Various Artists", "Soundtracks", "Compilations"]:
267+
artist_from_file = None
268+
if ' - ' in track_filename:
269+
parts = track_filename.split(' - ', 1)
270+
if len(parts) >= 2:
271+
artist_from_file = parts[0].strip()
272+
273+
artist = artist_from_file if artist_from_file else folder_name
274+
album = folder_name
275+
# try to split by ' - ' to separate artist and album
276+
elif ' - ' in folder_name:
277+
parts = folder_name.split(' - ', 1)
278+
artist = parts[0].strip()
279+
album = self.fix_explicit_label(parts[1].strip())
280+
else:
281+
# no separator, use folder as both artist and album
282+
artist = folder_name
283+
album = folder_name
284+
else:
285+
# standard /Music/Artist/Album/Track.ext structure
286+
artist = adjusted_sections[0].strip()
287+
album = self.fix_explicit_label(adjusted_sections[1].strip())
288+
track_filename = adjusted_sections[2]
239289

240290
# check track extension
241291
if not self.is_valid_track_filename(track_filename):
@@ -248,7 +298,7 @@ def parse_track_info(self, path: str) -> tuple:
248298
song = self.extract_song_from_filename(track_filename)
249299

250300
# ensure song title is valid
251-
if not song or song.lower() in ['flac', 'mp3', 'ogg', 'wav', 'm4a']:
301+
if not song or song.lower() in SONG_EXTENSIONS:
252302
raise ValueError(f"Invalid song title: {song}")
253303

254304
return album, artist, song
@@ -366,7 +416,11 @@ def logs_to_df(self) -> pd.DataFrame:
366416
})
367417
except Exception as e:
368418
failed.append(log_entry)
369-
print(log_entry, e)
419+
# handle encoding errors on Windows (charmap codec)
420+
try:
421+
print(log_entry, e)
422+
except UnicodeEncodeError:
423+
print(log_entry.encode('ascii', 'replace').decode('ascii'), e)
370424

371425
df = pd.DataFrame(entries)
372426
if len(failed) > 0:
@@ -802,11 +856,49 @@ def run(self) -> dict:
802856

803857

804858
try:
805-
# copy log to local storage
806-
shutil.copy2(log_location, STORAGE_DIR)
859+
# merge iPod playback.log with local storage copy
860+
local_log_path = os.path.join(STORAGE_DIR, 'playback.log')
861+
862+
# read existing local log entries (if exists)
863+
existing_entries = set()
864+
if os.path.exists(local_log_path):
865+
with open(local_log_path, 'r', encoding='utf-8', errors='replace') as f:
866+
for line in f:
867+
if not line.startswith('#'):
868+
existing_entries.add(line.strip())
869+
870+
# read iPod log and collect new entries
871+
new_entries = []
872+
new_headers = []
873+
with open(log_location, 'r', encoding='utf-8', errors='replace') as f:
874+
for line in f:
875+
line = line.strip()
876+
if line.startswith('#'):
877+
# collect header comments from iPod log
878+
new_headers.append(line)
879+
elif line and line not in existing_entries:
880+
new_entries.append(line)
881+
882+
# append new entries to local log
883+
if new_entries:
884+
with open(local_log_path, 'a', encoding='utf-8') as f:
885+
# write headers if any
886+
for header in new_headers:
887+
f.write(header + '\n')
888+
# write new play entries
889+
for entry in new_entries:
890+
f.write(entry + '\n')
891+
print(f"Appended {len(new_entries)} new plays to local playback.log")
892+
else:
893+
# if no local log exists yet, just copy the iPod log
894+
if not os.path.exists(local_log_path):
895+
shutil.copy2(log_location, STORAGE_DIR)
896+
print("Created initial local playback.log from iPod")
897+
else:
898+
print("No new plays to append to local playback.log")
807899

808900
# read and analyse logs
809-
self.log_data = self.load_logs(log_location)
901+
self.log_data = self.load_logs(local_log_path)
810902
self.log_df = self.logs_to_df()
811903
print(f"Loaded {len(self.log_df)} log entries")
812904

@@ -831,7 +923,11 @@ def run(self) -> dict:
831923
# run stats
832924
self.stats = self.calc_all_stats()
833925
except Exception as e:
834-
print(f"ERROR: {e}")
926+
# handle encoding errors on Windows (charmap codec)
927+
try:
928+
print(f"ERROR: {e}")
929+
except UnicodeEncodeError:
930+
print(f"ERROR: {str(e).encode('ascii', 'replace').decode('ascii')}")
835931
return {"error": "Something went wrong. Please try again later"}
836932

837933
# cleanup

ipod_wrapped/frontend/widgets/menu_nav.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,7 @@ def _open_about_dialog(window: Gtk.ApplicationWindow, revealer: Gtk.Revealer) ->
127127
about_dialog.set_application_name("iPod Wrapped")
128128
about_dialog.set_developer_name("Mandy-cyber")
129129
about_dialog.set_issue_url("https://github.com/Mandy-cyber/Everything-iPod/issues")
130-
about_dialog.set_version("1.0.0")
130+
about_dialog.set_version("1.0.1")
131131
about_dialog.set_comments("An all-in-one tool to view your iPod library, listening history, and 'iPod Wrapped' statistics")
132132
about_dialog.set_website("https://github.com/Mandy-cyber/Everything-iPod")
133133
about_dialog.set_license_type(Gtk.License.GPL_3_0)

0 commit comments

Comments
 (0)