207 lines · 8.8 KB
Raw Download
1
#!/usr/bin/env python3
2
"""
3
find_and_handle_duplicates_04.py
4
5
Added hierarchical TV‑show aware scanning:
6
- Scan only *season* folders inside each show.
7
- Compare episode identifiers only within that season folder.
8
"""
9
10
import os
11
import re
12
from pathlib import Path
13
from collections import defaultdict
14
15
16
def normalize_basename(name):
17
    """Normalize a base name by lowercasing and removing non‑alphanumeric characters."""
18
    return re.sub(r"[^a-z0-9]", "", name.lower())
19
20
21
def extract_episode_id(name):
22
    """
23
    Extract the SxxExx episode identifier from a filename, if present.
24
    Returns the identifier (e.g. 's01e01') or None if not found.
25
    Handles formats: S01E01, s01e01, S01E01E02, etc.
26
    """
27
    match = re.search(r"s\d{1,2}e\d{1,2}", name, re.IGNORECASE)
28
    if match:
29
        return match.group(0).lower()
30
    return None
31
32
33
def get_file_size_mb(filepath):
34
    """Get file size in MB."""
35
    size_bytes = os.path.getsize(filepath)
36
    return size_bytes / (1024 * 1024)
37
38
39
def find_and_handle_duplicates(folder_path):
40
    """Find duplicate episode files within each season folder and let user choose which to keep."""
41
42
    # Check if folder exists
43
    if not os.path.exists(folder_path):
44
        print(f"Error: Folder '{folder_path}' does not exist.")
45
        return
46
47
    # ------------------------------------------------------------------
48
    # 1️⃣ Walk the TV‑shows tree (show → season)
49
    for show_root, _dirs, _files in os.walk(folder_path):
50
        # We only care about *direct* children of `folder_path` that look like shows
51
        if Path(show_root).resolve() == Path(folder_path).resolve():
52
            continue  # skip the root itself
53
54
        # A show folder should contain sub‑folders named "Season XX" or similar.
55
        for season_name in os.listdir(show_root):
56
            season_path = os.path.join(show_root, season_name)
57
58
            if not os.path.isdir(season_path):
59
                continue  # ignore non‑folder items (e.g. stray files)
60
61
            # ──────────────────────  NEW  ──────────────────────
62
            # Walk *inside* the season folder – this is where we look for duplicates.
63
            files_by_key = defaultdict(list)
64
            key_type = {}  # key -> 'episode' or 'movie'
65
66
            for root, _dirs, files in os.walk(season_path):
67
                for item in files:
68
                    if item.lower().endswith(".srt"):
69
                        continue
70
                    filepath = os.path.join(root, item)
71
                    if os.path.isfile(filepath):  # sanity check – should always be true
72
                        basename = Path(item).stem
73
                        episode_id = extract_episode_id(basename)
74
75
                        if episode_id:
76
                            key = episode_id
77
                            key_type[key] = "episode"
78
                        else:
79
                            key = normalize_basename(basename)
80
                            key_type[key] = "movie"
81
82
                        files_by_key[key].append(filepath)
83
            # ──────────────────────  END NEW  ──────────────────────
84
85
            # Find and process duplicates inside this season
86
            duplicates_found = False
87
            total_deleted_mb = 0.0
88
            total_kept_mb = 0.0
89
90
            for key, filepaths in files_by_key.items():
91
                if len(filepaths) > 1:
92
                    duplicates_found = True
93
                    print(f"\n{'='*60}")
94
95
                    if key_type[key] == "episode":
96
                        print(
97
                            f"Found {len(filepaths)} files for episode: '{key.upper()}' "
98
                            f"in season folder: '{season_path}'"
99
                        )
100
                    else:
101
                        print(
102
                            f"Found {len(filepaths)} files with base name: '{key}' (normalized) "
103
                            f"in season folder: '{season_path}'"
104
                        )
105
                    print("=" * 60)
106
107
                    # Create list of (filepath, size) tuples and sort by size
108
                    file_info = [(fp, get_file_size_mb(fp)) for fp in filepaths]
109
                    file_info.sort(key=lambda x: x[1])  # Sort smallest to largest
110
111
                    # Display files with their sizes
112
                    for idx, (filepath, size_mb) in enumerate(file_info, 1):
113
                        filename = os.path.basename(filepath)
114
                        print(f"{idx}. {filename}")
115
                        print(f"   Size: {size_mb:.2f} MB")
116
117
                    # For TV episodes, offer a one-key shortcut to keep the smallest file
118
                    if key_type[key] == "episode":
119
                        print(
120
                            f"\n[Tip] File #1 is the smallest. Press Enter to keep it and delete the rest."
121
                        )
122
123
                    # Ask user which to keep
124
                    while True:
125
                        prompt = f"\nWhich file do you want to KEEP? (1-{len(file_info)}, or 's' to skip): "
126
                        choice = input(prompt).strip().lower()
127
128
                        # Empty Enter = keep smallest (episode shortcut)
129
                        if choice == "" and key_type[key] == "episode":
130
                            choice = "1"
131
132
                        if choice == "s":
133
                            print("Skipping this group.")
134
                            for filepath, size_mb in file_info:
135
                                total_kept_mb += size_mb
136
                            break
137
138
                        try:
139
                            choice_idx = int(choice) - 1
140
                            if 0 <= choice_idx < len(file_info):
141
                                kept_file = file_info[choice_idx][0]
142
                                kept_size = file_info[choice_idx][1]
143
                                total_kept_mb += kept_size
144
                                print(f"\nKeeping: {os.path.basename(kept_file)}")
145
146
                                for idx, (filepath, size_mb) in enumerate(file_info):
147
                                    if idx != choice_idx:
148
                                        try:
149
                                            os.remove(filepath)
150
                                            total_deleted_mb += size_mb
151
                                            print(
152
                                                f"Deleted: {os.path.basename(filepath)}"
153
                                            )
154
                                        except Exception as e:
155
                                            print(
156
                                                f"Error deleting {os.path.basename(filepath)}: {e}"
157
                                            )
158
                                break
159
                            else:
160
                                print(
161
                                    f"Please enter a number between 1 and {len(file_info)}."
162
                                )
163
                        except ValueError:
164
                            print(
165
                                "Invalid input. Please enter a number or 's' to skip."
166
                            )
167
168
            if not duplicates_found:
169
                print(
170
                    "\nNo duplicate filenames found in this season folder (ignoring extensions)."
171
                )
172
173
            # Print summary stats for this season
174
            total_original_mb = total_deleted_mb + total_kept_mb
175
            pct_saved = (
176
                (total_deleted_mb / total_original_mb * 100)
177
                if total_original_mb > 0
178
                else 0
179
            )
180
            total_space_saved_mb = total_deleted_mb - total_kept_mb
181
182
            print(f"\n{'='*60}")
183
            print("SUMMARY")
184
            print(f"Total deleted:        {total_deleted_mb:>10.2f} MB")
185
            print(f"Total kept/skipped:   {total_kept_mb:>10.2f} MB")
186
            print(
187
                f"Total space saved:    {total_space_saved_mb:>10.2f} MB  ({pct_saved:.1f}%)"
188
            )
189
        # ────────────────────── END PER‑SEASON LOOP ──────────────────────
190
    # ------------------------------------------------------------------
191
192
193
def main():
194
    folder_path = input("Enter the folder path to scan: ").strip()
195
196
    # Remove quotes if present (helpful for copy‑pasted paths)
197
    folder_path = folder_path.strip('"').strip("'")
198
199
    print(f"\nScanning folder: {folder_path}\n")
200
    find_and_handle_duplicates(folder_path)
201
    print("\n" + "=" * 60)
202
    print("Done!")
203
204
205
if __name__ == "__main__":
206
    main()
207