@@ -740,6 +740,21 @@ def get_file_extension(media_type):
740740 else :
741741 return ".bin"
742742
743+ def get_memory_output_dir (output_root , date_obj , create = True ):
744+ """Return the Year/Month/Day subfolder (under output_root) for a memory's local date.
745+
746+ e.g. output_root/2024/01 - January/2024-01-15
747+ """
748+ day_dir = (
749+ Path (output_root )
750+ / date_obj .strftime ("%Y" )
751+ / date_obj .strftime ("%m - %B" )
752+ / date_obj .strftime ("%Y-%m-%d" )
753+ )
754+ if create :
755+ day_dir .mkdir (parents = True , exist_ok = True )
756+ return day_dir
757+
743758# ==================== GUI Application ====================
744759
745760# --- Wrappers to refactored modules (override long original implementations) ---
@@ -1435,7 +1450,7 @@ def cleanup_temp_files(self, output_path):
14351450
14361451 cleaned_count = 0
14371452 for pattern in temp_patterns :
1438- for temp_file in output_path .glob (pattern ):
1453+ for temp_file in output_path .rglob (pattern ):
14391454 try :
14401455 temp_file .unlink ()
14411456 cleaned_count += 1
@@ -1478,46 +1493,51 @@ def should_skip_download(self, item, output_path, idx, date_obj, date_obj_local,
14781493 # Generate both local and UTC formatted dates for backward compatibility
14791494 date_formatted_local = date_obj_local .strftime ("%Y%m%d_%H%M%S" )
14801495 date_formatted_utc = date_obj .strftime ("%Y%m%d_%H%M%S" )
1481-
1482- # We'll check patterns for both local (preferred) and UTC (legacy) timestamps
1483- date_patterns = [date_formatted_local ]
1496+
1497+ # We'll check patterns for both local (preferred) and UTC (legacy) timestamps,
1498+ # each paired with the date used to derive its Year/Month/Day folder
1499+ date_patterns = [(date_formatted_local , date_obj_local )]
14841500 if date_formatted_utc != date_formatted_local : # Only check UTC if different
1485- date_patterns .append (date_formatted_utc )
1486-
1501+ date_patterns .append (( date_formatted_utc , date_obj ) )
1502+
14871503 # Check 1: Normal download filename (YYYYMMDD_HHMMSS_idx.ext)
1488- # Check both local and UTC patterns for backward compatibility
1489- for date_formatted in date_patterns :
1490- normal_filename = f"{ date_formatted } _{ idx } { extension } "
1491- normal_path = output_path / normal_filename
1492- if normal_path .exists ():
1493- if validate_downloaded_file (str (normal_path )):
1494- return True , str (normal_path ), "normal download"
1495- else :
1496- logging .warning (f"Found invalid existing file, will re-download: { normal_path } " )
1497- return False , None , "invalid file"
1498-
1499- # Check 2: Merged overlay filename (YYYYMMDD_HHMMSS.ext) - no idx suffix
1500- # This pattern is created when ZIP files contain -main/-overlay pairs
1501- merged_base = f"{ date_formatted } { extension } "
1502- merged_path = output_path / merged_base
1503- if merged_path .exists ():
1504- if validate_downloaded_file (str (merged_path )):
1505- return True , str (merged_path ), "merged overlay"
1506- else :
1507- logging .warning (f"Found invalid merged file, will re-download: { merged_path } " )
1508- return False , None , "invalid merged"
1509-
1510- # Check 3: Collision-resolved merged files (YYYYMMDD_HHMMSS_1.ext, _2.ext, ...)
1511- # When multiple overlays have the same timestamp, counter suffixes are added
1512- for count in range (1 , 11 ): # Reasonable upper bound
1513- collision_name = f"{ date_formatted } _{ count } { extension } "
1514- collision_path = output_path / collision_name
1515- if collision_path .exists ():
1516- if validate_downloaded_file (str (collision_path )):
1517- return True , str (collision_path ), f"collision-resolved merge (_{ count } )"
1504+ # Check both local and UTC patterns for backward compatibility, in both the
1505+ # current Year/Month/Day folder and the flat root (files from before this
1506+ # folder structure was added)
1507+ for date_formatted , date_for_dir in date_patterns :
1508+ nested_dir = get_memory_output_dir (output_path , date_for_dir , create = False )
1509+ for search_dir in (nested_dir , output_path ):
1510+ normal_filename = f"{ date_formatted } _{ idx } { extension } "
1511+ normal_path = search_dir / normal_filename
1512+ if normal_path .exists ():
1513+ if validate_downloaded_file (str (normal_path )):
1514+ return True , str (normal_path ), "normal download"
15181515 else :
1519- logging .warning (f"Found invalid collision file, will re-download: { collision_path } " )
1520- return False , None , "invalid collision"
1516+ logging .warning (f"Found invalid existing file, will re-download: { normal_path } " )
1517+ return False , None , "invalid file"
1518+
1519+ # Check 2: Merged overlay filename (YYYYMMDD_HHMMSS.ext) - no idx suffix
1520+ # This pattern is created when ZIP files contain -main/-overlay pairs
1521+ merged_base = f"{ date_formatted } { extension } "
1522+ merged_path = search_dir / merged_base
1523+ if merged_path .exists ():
1524+ if validate_downloaded_file (str (merged_path )):
1525+ return True , str (merged_path ), "merged overlay"
1526+ else :
1527+ logging .warning (f"Found invalid merged file, will re-download: { merged_path } " )
1528+ return False , None , "invalid merged"
1529+
1530+ # Check 3: Collision-resolved merged files (YYYYMMDD_HHMMSS_1.ext, _2.ext, ...)
1531+ # When multiple overlays have the same timestamp, counter suffixes are added
1532+ for count in range (1 , 11 ): # Reasonable upper bound
1533+ collision_name = f"{ date_formatted } _{ count } { extension } "
1534+ collision_path = search_dir / collision_name
1535+ if collision_path .exists ():
1536+ if validate_downloaded_file (str (collision_path )):
1537+ return True , str (collision_path ), f"collision-resolved merge (_{ count } )"
1538+ else :
1539+ logging .warning (f"Found invalid collision file, will re-download: { collision_path } " )
1540+ return False , None , "invalid collision"
15211541
15221542 # Check 4: Failed conversions directory (use local timezone pattern)
15231543 normal_filename_local = f"{ date_formatted_local } _{ idx } { extension } "
@@ -1621,11 +1641,12 @@ def progress_callback(message):
16211641 date_obj_local = date_obj
16221642 tz_offset = "+00:00"
16231643
1624- # Generate filename
1644+ # Generate filename, sorted into a Year/Month/Day folder
16251645 date_formatted = date_obj_local .strftime ("%Y%m%d_%H%M%S" )
16261646 extension = get_file_extension (media_type )
16271647 filename = f"{ date_formatted } _{ idx } { extension } "
1628- file_path = output_path / filename
1648+ media_output_dir = get_memory_output_dir (output_path , date_obj_local )
1649+ file_path = media_output_dir / filename
16291650
16301651 log_local (f" File: { filename } " )
16311652 log_local (f" Type: { media_type } " )
@@ -2529,6 +2550,7 @@ def process_local_files_thread(self, json_file, memories_root, output_dir):
25292550 )
25302551 date_str_preview = local_dt_skip .strftime ("%Y%m%d_%H%M%S" )
25312552 except Exception :
2553+ local_dt_skip = utc_mtime
25322554 date_str_preview = utc_mtime .strftime ("%Y%m%d_%H%M%S" )
25332555
25342556 # Build the full set of outputs this file would produce
@@ -2541,7 +2563,13 @@ def process_local_files_thread(self, json_file, memories_root, output_dir):
25412563 if has_overlay_skip and overlay_mode_skip == "both" :
25422564 expected .append (f"{ date_str_preview } _{ global_idx } _original{ ext } " )
25432565
2544- if all ((output_path / n ).exists () for n in expected ):
2566+ # Check the Year/Month/Day folder (current behavior) as well as
2567+ # the flat root (files placed before this folder structure existed)
2568+ media_output_dir_skip = get_memory_output_dir (output_path , local_dt_skip , create = False )
2569+ if all (
2570+ (media_output_dir_skip / n ).exists () or (output_path / n ).exists ()
2571+ for n in expected
2572+ ):
25452573 self .log (
25462574 f"[{ global_idx } /{ grand_total } ] ↷ Skipped "
25472575 f"(already exists: { expected [0 ]} )" ,
@@ -2996,9 +3024,12 @@ def log_local(msg):
29963024 has_overlay = overlay_path and os .path .exists (overlay_path )
29973025 primary_output = None
29983026
3027+ # Sort into a Year/Month/Day folder under output_path
3028+ media_output_dir = get_memory_output_dir (output_path , local_dt )
3029+
29993030 if has_overlay and overlay_mode in ("merge" , "both" ):
30003031 out_name = f"{ date_str } _{ idx } { ext } "
3001- out_file = str (output_path / out_name )
3032+ out_file = str (media_output_dir / out_name )
30023033
30033034 if is_video :
30043035 ok , result = merge_video_overlay (file_path , overlay_path , out_file )
@@ -3012,21 +3043,21 @@ def log_local(msg):
30123043 primary_output = out_file
30133044 else :
30143045 log_local (f" ⚠ Merge failed ({ result } ), copying original instead" )
3015- out_file = str (output_path / out_name )
3046+ out_file = str (media_output_dir / out_name )
30163047 _copy_file_with_metadata (file_path , out_file , is_video , local_dt ,
30173048 latitude , longitude , tz_off_str , log_local )
30183049 primary_output = out_file
30193050
30203051 if overlay_mode == "both" :
30213052 orig_name = f"{ date_str } _{ idx } _original{ ext } "
3022- orig_out = str (output_path / orig_name )
3053+ orig_out = str (media_output_dir / orig_name )
30233054 _copy_file_with_metadata (file_path , orig_out , is_video , local_dt ,
30243055 latitude , longitude , tz_off_str , log_local )
30253056 log_local (f" ✓ Saved original → { orig_name } " )
30263057
30273058 else :
30283059 out_name = f"{ date_str } _{ idx } { ext } "
3029- out_file = str (output_path / out_name )
3060+ out_file = str (media_output_dir / out_name )
30303061 _copy_file_with_metadata (file_path , out_file , is_video , local_dt ,
30313062 latitude , longitude , tz_off_str , log_local )
30323063 log_local (f" ✓ → { out_name } " )
0 commit comments