shownamer 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {shownamer-2.2.0/shownamer.egg-info → shownamer-2.3.0}/PKG-INFO +2 -2
- {shownamer-2.2.0 → shownamer-2.3.0}/pyproject.toml +2 -2
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer/__init__.py +1 -1
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer/api.py +25 -1
- shownamer-2.3.0/shownamer/core.py +433 -0
- {shownamer-2.2.0 → shownamer-2.3.0/shownamer.egg-info}/PKG-INFO +2 -2
- shownamer-2.2.0/shownamer/core.py +0 -180
- {shownamer-2.2.0 → shownamer-2.3.0}/LICENSE +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/MANIFEST.in +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/README.md +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/setup.cfg +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer/__main__.py +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer/titleEmbed.py +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer/utils.py +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer.egg-info/SOURCES.txt +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer.egg-info/dependency_links.txt +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer.egg-info/entry_points.txt +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer.egg-info/requires.txt +0 -0
- {shownamer-2.2.0 → shownamer-2.3.0}/shownamer.egg-info/top_level.txt +0 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shownamer
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: A powerful yet lightweight command-line tool that automatically renames your TV show and movie files.
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Amal Lalgi <theamallalgi+shownamer@google.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
@@ -4,9 +4,9 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "shownamer"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.3.0"
|
|
8
8
|
authors = [
|
|
9
|
-
{ name="
|
|
9
|
+
{ name="Amal Lalgi", email="theamallalgi+shownamer@google.com" },
|
|
10
10
|
]
|
|
11
11
|
description = "A powerful yet lightweight command-line tool that automatically renames your TV show and movie files."
|
|
12
12
|
readme = "README.md"
|
|
@@ -5,6 +5,7 @@ BASE_URL = "http://api.tvmaze.com"
|
|
|
5
5
|
OMDB_KEY_FILE = Path.home() / ".shownamer_omdb_key"
|
|
6
6
|
OMDB_URL = "http://www.omdbapi.com/"
|
|
7
7
|
|
|
8
|
+
|
|
8
9
|
def get_omdb_key():
|
|
9
10
|
if OMDB_KEY_FILE.exists():
|
|
10
11
|
return OMDB_KEY_FILE.read_text().strip()
|
|
@@ -14,6 +15,7 @@ def get_omdb_key():
|
|
|
14
15
|
OMDB_KEY_FILE.write_text(key)
|
|
15
16
|
return key
|
|
16
17
|
|
|
18
|
+
|
|
17
19
|
def fetch_omdb_metadata(title, year=None, api_key=None):
|
|
18
20
|
params = {"t": title, "apikey": api_key, "type": "movie"}
|
|
19
21
|
if year:
|
|
@@ -27,6 +29,7 @@ def fetch_omdb_metadata(title, year=None, api_key=None):
|
|
|
27
29
|
pass
|
|
28
30
|
return None
|
|
29
31
|
|
|
32
|
+
|
|
30
33
|
def search_media(name, media_type="shows"):
|
|
31
34
|
endpoint = f"search/{media_type}"
|
|
32
35
|
try:
|
|
@@ -39,14 +42,35 @@ def search_media(name, media_type="shows"):
|
|
|
39
42
|
print(f"[!] Error searching for {name}: {e}")
|
|
40
43
|
return None
|
|
41
44
|
|
|
45
|
+
|
|
42
46
|
def get_episode_by_number(show_id, season, episode):
|
|
43
47
|
try:
|
|
44
48
|
response = requests.get(
|
|
45
49
|
f"{BASE_URL}/shows/{show_id}/episodebynumber",
|
|
46
|
-
params={"season": season, "number": episode}
|
|
50
|
+
params={"season": season, "number": episode},
|
|
47
51
|
)
|
|
48
52
|
response.raise_for_status()
|
|
49
53
|
return response.json()
|
|
50
54
|
except requests.exceptions.RequestException as e:
|
|
51
55
|
print(f"[!] Error getting episode S{season:02}E{episode:02}: {e}")
|
|
52
56
|
return None
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def get_show_episodes(show_id):
|
|
60
|
+
try:
|
|
61
|
+
response = requests.get(f"{BASE_URL}/shows/{show_id}/episodes")
|
|
62
|
+
response.raise_for_status()
|
|
63
|
+
return response.json()
|
|
64
|
+
except requests.exceptions.RequestException as e:
|
|
65
|
+
print(f"[!] Error getting episode list for show {show_id}: {e}")
|
|
66
|
+
return []
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def get_show_cast(show_id):
|
|
70
|
+
try:
|
|
71
|
+
response = requests.get(f"{BASE_URL}/shows/{show_id}/cast")
|
|
72
|
+
response.raise_for_status()
|
|
73
|
+
return response.json()
|
|
74
|
+
except requests.exceptions.RequestException as e:
|
|
75
|
+
print(f"[!] Error getting cast for show {show_id}: {e}")
|
|
76
|
+
return []
|
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from . import utils, api
|
|
5
|
+
from . import titleEmbed
|
|
6
|
+
import textwrap
|
|
7
|
+
import re
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def process_directory(args):
|
|
11
|
+
if args.name:
|
|
12
|
+
list_detected_media(args.dir, args.ext, args.movie, args)
|
|
13
|
+
return
|
|
14
|
+
|
|
15
|
+
for filename in os.listdir(args.dir):
|
|
16
|
+
file_ext = os.path.splitext(filename)[1][1:]
|
|
17
|
+
if file_ext.lower() in [e.lower() for e in args.ext]:
|
|
18
|
+
process_file(filename, args)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_rating(media_info, source):
|
|
22
|
+
for rating in media_info.get("Ratings", []):
|
|
23
|
+
if rating.get("Source") == source:
|
|
24
|
+
return rating.get("Value", "N/A")
|
|
25
|
+
return "N/A"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def process_file(filename, args):
|
|
29
|
+
if args.format:
|
|
30
|
+
try:
|
|
31
|
+
utils.validate_format(args.format)
|
|
32
|
+
except ValueError as e:
|
|
33
|
+
print(f"[!] Error: {e}")
|
|
34
|
+
return
|
|
35
|
+
|
|
36
|
+
if args.verbose:
|
|
37
|
+
print(f"Processing: {filename}")
|
|
38
|
+
|
|
39
|
+
file_path = os.path.join(args.dir, filename)
|
|
40
|
+
file_info = utils.parse_filename(os.path.splitext(filename)[0], args.movie)
|
|
41
|
+
|
|
42
|
+
if not file_info:
|
|
43
|
+
if args.verbose:
|
|
44
|
+
print(f"[skip] Could not parse file information from '{filename}'")
|
|
45
|
+
return
|
|
46
|
+
|
|
47
|
+
rename_succeeded = False
|
|
48
|
+
new_name = ""
|
|
49
|
+
if args.movie:
|
|
50
|
+
new_name = rename_movie(file_info, args)
|
|
51
|
+
else:
|
|
52
|
+
if not file_info.get("is_movie"):
|
|
53
|
+
new_name = rename_show(file_info, args)
|
|
54
|
+
|
|
55
|
+
if new_name:
|
|
56
|
+
file_ext = os.path.splitext(filename)[1]
|
|
57
|
+
new_filename = new_name + file_ext
|
|
58
|
+
new_filepath = os.path.join(args.dir, new_filename)
|
|
59
|
+
|
|
60
|
+
if filename == new_filename:
|
|
61
|
+
print(f"[skip] '{filename}' already matches the target name.")
|
|
62
|
+
elif not args.dry_run:
|
|
63
|
+
if os.path.exists(new_filepath):
|
|
64
|
+
print(f"[skip] '{new_filename}' already exists.")
|
|
65
|
+
else:
|
|
66
|
+
try:
|
|
67
|
+
print(f"[rename] '{filename}' → '{new_filename}'")
|
|
68
|
+
shutil.move(file_path, new_filepath)
|
|
69
|
+
rename_succeeded = True
|
|
70
|
+
except OSError as e:
|
|
71
|
+
print(f" → [!] Error renaming file: {e}")
|
|
72
|
+
else:
|
|
73
|
+
print(f"[rename] '{filename}' → '{new_filename}'")
|
|
74
|
+
|
|
75
|
+
if args.title:
|
|
76
|
+
resolved_name = new_name if new_name else os.path.splitext(filename)[0]
|
|
77
|
+
|
|
78
|
+
titleStr = _buildTitleStr(resolved_name, args, file_info)
|
|
79
|
+
|
|
80
|
+
if args.dry_run:
|
|
81
|
+
print(f" → [title] Would embed: {titleStr}")
|
|
82
|
+
else:
|
|
83
|
+
target_path = Path(new_filepath) if rename_succeeded else Path(file_path)
|
|
84
|
+
|
|
85
|
+
success = titleEmbed.embedTitle(target_path, titleStr)
|
|
86
|
+
|
|
87
|
+
if args.verbose:
|
|
88
|
+
if success:
|
|
89
|
+
print(f" → [title] Embedded: {titleStr}")
|
|
90
|
+
else:
|
|
91
|
+
print(
|
|
92
|
+
f" → [title] Failed to embed metadata (ffmpeg/mutagen required)"
|
|
93
|
+
)
|
|
94
|
+
elif args.verbose:
|
|
95
|
+
print(f"[skip] No new name generated for '{filename}'")
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _buildTitleStr(resolvedName: str, args, fileInfo: dict) -> str:
|
|
99
|
+
if args.format:
|
|
100
|
+
return resolvedName
|
|
101
|
+
if args.movie:
|
|
102
|
+
name, _, year = resolvedName.rpartition(" (")
|
|
103
|
+
return titleEmbed.buildMovieTitle(name, year.rstrip(")"))
|
|
104
|
+
season = fileInfo.get("season", 1)
|
|
105
|
+
episode = fileInfo.get("episode", 1)
|
|
106
|
+
title = resolvedName.split(" - ", 1)[-1] if " - " in resolvedName else resolvedName
|
|
107
|
+
showName = fileInfo.get("name", resolvedName)
|
|
108
|
+
return titleEmbed.buildShowTitle(showName, season, episode, title)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def format_episode_ranges(episodes):
|
|
112
|
+
episodes = sorted(episodes)
|
|
113
|
+
|
|
114
|
+
if not episodes:
|
|
115
|
+
return ""
|
|
116
|
+
|
|
117
|
+
ranges = []
|
|
118
|
+
start = end = episodes[0]
|
|
119
|
+
|
|
120
|
+
for ep in episodes[1:]:
|
|
121
|
+
if ep == end + 1:
|
|
122
|
+
end = ep
|
|
123
|
+
else:
|
|
124
|
+
ranges.append((start, end))
|
|
125
|
+
start = end = ep
|
|
126
|
+
|
|
127
|
+
ranges.append((start, end))
|
|
128
|
+
|
|
129
|
+
parts = []
|
|
130
|
+
|
|
131
|
+
for start, end in ranges:
|
|
132
|
+
if start == end:
|
|
133
|
+
parts.append(str(start))
|
|
134
|
+
else:
|
|
135
|
+
parts.append(f"{start}-{end}")
|
|
136
|
+
|
|
137
|
+
return ", ".join(parts)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def format_ranges(numbers):
|
|
141
|
+
numbers = sorted(numbers)
|
|
142
|
+
|
|
143
|
+
if not numbers:
|
|
144
|
+
return "None"
|
|
145
|
+
|
|
146
|
+
ranges = []
|
|
147
|
+
start = end = numbers[0]
|
|
148
|
+
|
|
149
|
+
for n in numbers[1:]:
|
|
150
|
+
if n == end + 1:
|
|
151
|
+
end = n
|
|
152
|
+
else:
|
|
153
|
+
ranges.append((start, end))
|
|
154
|
+
start = end = n
|
|
155
|
+
|
|
156
|
+
ranges.append((start, end))
|
|
157
|
+
|
|
158
|
+
parts = []
|
|
159
|
+
|
|
160
|
+
for start, end in ranges:
|
|
161
|
+
if start == end:
|
|
162
|
+
parts.append(f"{start:02}")
|
|
163
|
+
else:
|
|
164
|
+
parts.append(f"{start:02}-{end:02}")
|
|
165
|
+
|
|
166
|
+
return ", ".join(parts)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def rename_show(file_info, args):
|
|
170
|
+
media_info = api.search_media(file_info["name"], "shows")
|
|
171
|
+
if not media_info:
|
|
172
|
+
if args.verbose:
|
|
173
|
+
print(f" → [API Error] Could not find show '{file_info['name']}'")
|
|
174
|
+
return None
|
|
175
|
+
|
|
176
|
+
show_id = media_info["id"]
|
|
177
|
+
episode_info = api.get_episode_by_number(
|
|
178
|
+
show_id, file_info["season"], file_info["episode"]
|
|
179
|
+
)
|
|
180
|
+
if not episode_info:
|
|
181
|
+
if args.verbose:
|
|
182
|
+
print(
|
|
183
|
+
f" → [API Error] Could not find episode S{file_info['season']:02}E{file_info['episode']:02} for '{media_info['name']}'"
|
|
184
|
+
)
|
|
185
|
+
return None
|
|
186
|
+
|
|
187
|
+
format_str = args.format or "{name} S{season:02}E{episode:02} - {title}"
|
|
188
|
+
|
|
189
|
+
try:
|
|
190
|
+
return format_str.format(
|
|
191
|
+
name=utils.clean_show_name(media_info["name"], args.char),
|
|
192
|
+
season=file_info["season"],
|
|
193
|
+
episode=file_info["episode"],
|
|
194
|
+
title=utils.clean_show_name(episode_info["name"], args.char),
|
|
195
|
+
year=media_info.get("premiered", "N/A").split("-")[0]
|
|
196
|
+
if media_info.get("premiered")
|
|
197
|
+
else "N/A",
|
|
198
|
+
)
|
|
199
|
+
except KeyError as e:
|
|
200
|
+
print(f"[!] Unknown placeholder in --format: {e}")
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def rename_movie(file_info, args):
|
|
205
|
+
api_key = args.api_key or api.get_omdb_key()
|
|
206
|
+
media_info = api.fetch_omdb_metadata(file_info["name"], file_info["year"], api_key)
|
|
207
|
+
|
|
208
|
+
if not media_info:
|
|
209
|
+
if args.verbose:
|
|
210
|
+
print(f" → [API Error] Could not find movie '{file_info['name']}'")
|
|
211
|
+
return None
|
|
212
|
+
|
|
213
|
+
format_str = args.format or "{name} ({year})"
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
return format_str.format(
|
|
217
|
+
name=utils.clean_show_name(media_info["Title"], args.char),
|
|
218
|
+
year=media_info.get("Year", "N/A"),
|
|
219
|
+
director=media_info.get("Director", "N/A").split(",")[0],
|
|
220
|
+
genre=media_info.get("Genre", "N/A").split(",")[0],
|
|
221
|
+
)
|
|
222
|
+
except KeyError as e:
|
|
223
|
+
print(f"[!] Unknown placeholder in --format: {e}")
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def list_detected_media(directory, extensions, is_movie=False, args=None):
|
|
228
|
+
if is_movie and args is None:
|
|
229
|
+
raise ValueError("args is required when is_movie=True")
|
|
230
|
+
|
|
231
|
+
media = {}
|
|
232
|
+
if is_movie:
|
|
233
|
+
api_key = args.api_key or api.get_omdb_key()
|
|
234
|
+
|
|
235
|
+
for filename in os.listdir(directory):
|
|
236
|
+
file_ext = os.path.splitext(filename)[1][1:]
|
|
237
|
+
if file_ext.lower() in [e.lower() for e in extensions]:
|
|
238
|
+
info = utils.parse_filename(os.path.splitext(filename)[0], is_movie)
|
|
239
|
+
if info:
|
|
240
|
+
name = info["name"]
|
|
241
|
+
if is_movie:
|
|
242
|
+
if name not in media:
|
|
243
|
+
media_info = api.fetch_omdb_metadata(
|
|
244
|
+
info["name"], info["year"], api_key
|
|
245
|
+
)
|
|
246
|
+
if media_info:
|
|
247
|
+
media[name] = {
|
|
248
|
+
"filename": filename,
|
|
249
|
+
"title": media_info.get("Title", "N/A"),
|
|
250
|
+
"year": media_info.get("Year", "N/A"),
|
|
251
|
+
"director": media_info.get("Director", "N/A"),
|
|
252
|
+
"genre": media_info.get("Genre", "N/A"),
|
|
253
|
+
"runtime": media_info.get("Runtime", "N/A"),
|
|
254
|
+
"rated": media_info.get("Rated", "N/A"),
|
|
255
|
+
"released": media_info.get("Released", "N/A"),
|
|
256
|
+
"writer": media_info.get("Writer", "N/A"),
|
|
257
|
+
"actors": media_info.get("Actors", "N/A"),
|
|
258
|
+
"plot": media_info.get("Plot", "N/A"),
|
|
259
|
+
"language": media_info.get("Language", "N/A"),
|
|
260
|
+
"country": media_info.get("Country", "N/A"),
|
|
261
|
+
"awards": media_info.get("Awards", "N/A"),
|
|
262
|
+
"imdb_rating": media_info.get("imdbRating", "N/A"),
|
|
263
|
+
"rotten_tomatoes": get_rating(
|
|
264
|
+
media_info, "Rotten Tomatoes"
|
|
265
|
+
),
|
|
266
|
+
"metacritic": get_rating(media_info, "Metacritic"),
|
|
267
|
+
"box_office": media_info.get("BoxOffice", "N/A"),
|
|
268
|
+
"production": media_info.get("Production", "N/A"),
|
|
269
|
+
}
|
|
270
|
+
else:
|
|
271
|
+
if name not in media:
|
|
272
|
+
show_info = api.search_media(name, "shows")
|
|
273
|
+
|
|
274
|
+
all_episodes = []
|
|
275
|
+
cast = []
|
|
276
|
+
|
|
277
|
+
if show_info:
|
|
278
|
+
all_episodes = api.get_show_episodes(show_info["id"])
|
|
279
|
+
cast = api.get_show_cast(show_info["id"])
|
|
280
|
+
|
|
281
|
+
media[name] = {
|
|
282
|
+
"show_info": show_info,
|
|
283
|
+
"all_episodes": all_episodes,
|
|
284
|
+
"cast": cast,
|
|
285
|
+
"seasons": {},
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
season = info["season"]
|
|
289
|
+
episode = info["episode"]
|
|
290
|
+
|
|
291
|
+
if season not in media[name]["seasons"]:
|
|
292
|
+
media[name]["seasons"][season] = set()
|
|
293
|
+
|
|
294
|
+
media[name]["seasons"][season].add(episode)
|
|
295
|
+
|
|
296
|
+
for name, data in media.items():
|
|
297
|
+
if is_movie:
|
|
298
|
+
print(f"Movie Name: {data['title']}")
|
|
299
|
+
print(f"Filename: {data['filename']}")
|
|
300
|
+
print(f"Year: {data['year']}")
|
|
301
|
+
print(f"Director(s): {data['director']}")
|
|
302
|
+
print(f"Genre(s): {data['genre']}")
|
|
303
|
+
print(f"Runtime: {data['runtime']}")
|
|
304
|
+
print(f"Rated: {data['rated']}")
|
|
305
|
+
print(f"Released: {data['released']}")
|
|
306
|
+
print(f"Writer(s): {data['writer']}")
|
|
307
|
+
print(f"Main Cast: {data['actors']}")
|
|
308
|
+
wrapped_plot = textwrap.fill(
|
|
309
|
+
data["plot"], width=80, subsequent_indent=" "
|
|
310
|
+
)
|
|
311
|
+
print(f"Plot: {wrapped_plot}")
|
|
312
|
+
# print(f"Plot: {data['plot']}")
|
|
313
|
+
print(f"Language(s): {data['language']}")
|
|
314
|
+
print(f"Country: {data['country']}")
|
|
315
|
+
print(f"Awards: {data['awards']}")
|
|
316
|
+
print(f"IMDb Rating: {data['imdb_rating']}")
|
|
317
|
+
print(f"Rotten Tomatoes: {data['rotten_tomatoes']}")
|
|
318
|
+
print(f"Metacritic: {data['metacritic']}")
|
|
319
|
+
print(f"Box Office: {data['box_office']}")
|
|
320
|
+
print(f"Production/Studio: {data['production']}")
|
|
321
|
+
print("---\n")
|
|
322
|
+
else:
|
|
323
|
+
show = data.get("show_info")
|
|
324
|
+
|
|
325
|
+
if not show:
|
|
326
|
+
print(f"[i] {name}")
|
|
327
|
+
continue
|
|
328
|
+
|
|
329
|
+
print(f"Show Name: {show.get('name', 'N/A')}")
|
|
330
|
+
|
|
331
|
+
premiered = show.get("premiered")
|
|
332
|
+
if premiered:
|
|
333
|
+
print(f"Premiered: {premiered[:4]}")
|
|
334
|
+
|
|
335
|
+
ended = show.get("ended")
|
|
336
|
+
if ended:
|
|
337
|
+
print(f"Ended: {ended[:4]}")
|
|
338
|
+
|
|
339
|
+
print(f"Status: {show.get('status', 'N/A')}")
|
|
340
|
+
print(f"Genres: {', '.join(show.get('genres', [])) or 'N/A'}")
|
|
341
|
+
print(f"Language: {show.get('language', 'N/A')}")
|
|
342
|
+
|
|
343
|
+
network = show.get("network")
|
|
344
|
+
country = None
|
|
345
|
+
if network:
|
|
346
|
+
country = network.get("country", {}).get("name")
|
|
347
|
+
if country:
|
|
348
|
+
print(f"Country: {country}")
|
|
349
|
+
|
|
350
|
+
runtime = show.get("runtime")
|
|
351
|
+
if runtime:
|
|
352
|
+
print(f"Runtime: {runtime} min")
|
|
353
|
+
|
|
354
|
+
cast_names = [actor["person"]["name"] for actor in data.get("cast", [])[:5]]
|
|
355
|
+
if cast_names:
|
|
356
|
+
print(f"Main Cast: {', '.join(cast_names)}")
|
|
357
|
+
|
|
358
|
+
rating = show.get("rating", {})
|
|
359
|
+
if rating.get("average"):
|
|
360
|
+
print(f"Rating: {rating['average']}")
|
|
361
|
+
|
|
362
|
+
summary = show.get("summary")
|
|
363
|
+
if summary:
|
|
364
|
+
summary = re.sub(r"<[^>]*>", "", summary)
|
|
365
|
+
wrapped_summary = textwrap.fill(
|
|
366
|
+
summary, width=80, subsequent_indent=" "
|
|
367
|
+
)
|
|
368
|
+
print(f"Summary: {wrapped_summary}")
|
|
369
|
+
|
|
370
|
+
official = {}
|
|
371
|
+
for ep in data["all_episodes"]:
|
|
372
|
+
season = ep["season"]
|
|
373
|
+
if season not in official:
|
|
374
|
+
official[season] = set()
|
|
375
|
+
official[season].add(ep["number"])
|
|
376
|
+
|
|
377
|
+
print(f"Total Seasons: {len(official)}")
|
|
378
|
+
total_episode_count = sum(len(eps) for eps in official.values())
|
|
379
|
+
print(f"Total Episodes: {total_episode_count}")
|
|
380
|
+
|
|
381
|
+
print("\t")
|
|
382
|
+
print("Local Collection Status:")
|
|
383
|
+
present_seasons = set(data["seasons"].keys())
|
|
384
|
+
official_seasons = set(official.keys())
|
|
385
|
+
for season_num in sorted(present_seasons):
|
|
386
|
+
owned = data["seasons"][season_num]
|
|
387
|
+
total = official.get(season_num, set())
|
|
388
|
+
owned_count = len(owned)
|
|
389
|
+
total_count = len(total)
|
|
390
|
+
if total_count and owned_count == total_count:
|
|
391
|
+
print(
|
|
392
|
+
f"[✓] Season {season_num:02}: "
|
|
393
|
+
f"{owned_count} / {total_count} episodes (Complete)"
|
|
394
|
+
)
|
|
395
|
+
else:
|
|
396
|
+
print(
|
|
397
|
+
f"[!] Season {season_num:02}: "
|
|
398
|
+
f"{owned_count} / {total_count} episodes"
|
|
399
|
+
)
|
|
400
|
+
if total:
|
|
401
|
+
missing = sorted(total - owned)
|
|
402
|
+
if missing:
|
|
403
|
+
available = sorted(owned)
|
|
404
|
+
print(
|
|
405
|
+
f" Available: Episodes {format_episode_ranges(available)}"
|
|
406
|
+
)
|
|
407
|
+
print(
|
|
408
|
+
f" Missing: Episodes {format_episode_ranges(missing)}"
|
|
409
|
+
)
|
|
410
|
+
|
|
411
|
+
# Missing Seasons
|
|
412
|
+
missing_seasons = sorted(official_seasons - present_seasons)
|
|
413
|
+
print()
|
|
414
|
+
if missing_seasons:
|
|
415
|
+
print(f"Missing Seasons: {format_ranges(missing_seasons)}")
|
|
416
|
+
else:
|
|
417
|
+
print("Missing Seasons: None")
|
|
418
|
+
present_episode_count = sum(
|
|
419
|
+
len(season) for season in data["seasons"].values()
|
|
420
|
+
)
|
|
421
|
+
print()
|
|
422
|
+
|
|
423
|
+
# Collection Summary
|
|
424
|
+
print("Collection Summary:")
|
|
425
|
+
print(f"Seasons Present: {len(present_seasons)} / {len(official_seasons)}")
|
|
426
|
+
print(f"Episodes Present: {present_episode_count} / {total_episode_count}")
|
|
427
|
+
|
|
428
|
+
if present_episode_count == total_episode_count and len(
|
|
429
|
+
present_seasons
|
|
430
|
+
) == len(official_seasons):
|
|
431
|
+
print("Collection Complete")
|
|
432
|
+
|
|
433
|
+
print("---\n")
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shownamer
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.3.0
|
|
4
4
|
Summary: A powerful yet lightweight command-line tool that automatically renames your TV show and movie files.
|
|
5
|
-
Author-email:
|
|
5
|
+
Author-email: Amal Lalgi <theamallalgi+shownamer@google.com>
|
|
6
6
|
Classifier: Programming Language :: Python :: 3
|
|
7
7
|
Classifier: License :: OSI Approved :: MIT License
|
|
8
8
|
Classifier: Operating System :: OS Independent
|
|
@@ -1,180 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
from pathlib import Path
|
|
4
|
-
from . import utils, api
|
|
5
|
-
from . import titleEmbed
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def process_directory(args):
|
|
9
|
-
if args.name:
|
|
10
|
-
list_detected_media(args.dir, args.ext, args.movie, args)
|
|
11
|
-
return
|
|
12
|
-
|
|
13
|
-
for filename in os.listdir(args.dir):
|
|
14
|
-
file_ext = os.path.splitext(filename)[1][1:]
|
|
15
|
-
if file_ext.lower() in [e.lower() for e in args.ext]:
|
|
16
|
-
process_file(filename, args)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def process_file(filename, args):
|
|
20
|
-
if args.format:
|
|
21
|
-
try:
|
|
22
|
-
utils.validate_format(args.format)
|
|
23
|
-
except ValueError as e:
|
|
24
|
-
print(f"[!] Error: {e}")
|
|
25
|
-
return
|
|
26
|
-
|
|
27
|
-
if args.verbose:
|
|
28
|
-
print(f"Processing: {filename}")
|
|
29
|
-
|
|
30
|
-
file_path = os.path.join(args.dir, filename)
|
|
31
|
-
file_info = utils.parse_filename(os.path.splitext(filename)[0], args.movie)
|
|
32
|
-
|
|
33
|
-
if not file_info:
|
|
34
|
-
if args.verbose:
|
|
35
|
-
print(f"[skip] Could not parse file information from '{filename}'")
|
|
36
|
-
return
|
|
37
|
-
|
|
38
|
-
new_name = ""
|
|
39
|
-
if args.movie:
|
|
40
|
-
new_name = rename_movie(file_info, args)
|
|
41
|
-
else:
|
|
42
|
-
if not file_info.get("is_movie"):
|
|
43
|
-
new_name = rename_show(file_info, args)
|
|
44
|
-
|
|
45
|
-
if new_name:
|
|
46
|
-
file_ext = os.path.splitext(filename)[1]
|
|
47
|
-
new_filename = new_name + file_ext
|
|
48
|
-
new_filepath = os.path.join(args.dir, new_filename)
|
|
49
|
-
|
|
50
|
-
print(f"[rename] '{filename}' → '{new_filename}'")
|
|
51
|
-
|
|
52
|
-
if not args.dry_run:
|
|
53
|
-
if os.path.exists(new_filepath):
|
|
54
|
-
print(f"[skip] '{new_filename}' already exists.")
|
|
55
|
-
else:
|
|
56
|
-
try:
|
|
57
|
-
shutil.move(file_path, new_filepath)
|
|
58
|
-
if args.title:
|
|
59
|
-
titleStr = _buildTitleStr(new_name, args, file_info)
|
|
60
|
-
success = titleEmbed.embedTitle(Path(new_filepath), titleStr)
|
|
61
|
-
if args.verbose:
|
|
62
|
-
if success:
|
|
63
|
-
print(f" → [title] Embedded: {titleStr}")
|
|
64
|
-
else:
|
|
65
|
-
print(f" → [title] Failed to embed metadata (ffmpeg/mutagen required)")
|
|
66
|
-
except OSError as e:
|
|
67
|
-
print(f" → [!] Error renaming file: {e}")
|
|
68
|
-
else:
|
|
69
|
-
if args.title:
|
|
70
|
-
titleStr = _buildTitleStr(new_name, args, file_info)
|
|
71
|
-
print(f" → [title] Would embed: {titleStr}")
|
|
72
|
-
elif args.verbose:
|
|
73
|
-
print(f"[skip] No new name generated for '{filename}'")
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _buildTitleStr(resolvedName: str, args, fileInfo: dict) -> str:
|
|
77
|
-
if args.format:
|
|
78
|
-
return resolvedName
|
|
79
|
-
if args.movie:
|
|
80
|
-
name, _, year = resolvedName.rpartition(" (")
|
|
81
|
-
return titleEmbed.buildMovieTitle(name, year.rstrip(")"))
|
|
82
|
-
season = fileInfo.get("season", 1)
|
|
83
|
-
episode = fileInfo.get("episode", 1)
|
|
84
|
-
title = resolvedName.split(" - ", 1)[-1] if " - " in resolvedName else resolvedName
|
|
85
|
-
showName = fileInfo.get("name", resolvedName)
|
|
86
|
-
return titleEmbed.buildShowTitle(showName, season, episode, title)
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def rename_show(file_info, args):
|
|
90
|
-
media_info = api.search_media(file_info["name"], "shows")
|
|
91
|
-
if not media_info:
|
|
92
|
-
if args.verbose:
|
|
93
|
-
print(f" → [API Error] Could not find show '{file_info['name']}'")
|
|
94
|
-
return None
|
|
95
|
-
|
|
96
|
-
show_id = media_info["id"]
|
|
97
|
-
episode_info = api.get_episode_by_number(show_id, file_info["season"], file_info["episode"])
|
|
98
|
-
if not episode_info:
|
|
99
|
-
if args.verbose:
|
|
100
|
-
print(f" → [API Error] Could not find episode S{file_info['season']:02}E{file_info['episode']:02} for '{media_info['name']}'")
|
|
101
|
-
return None
|
|
102
|
-
|
|
103
|
-
format_str = args.format or "{name} S{season:02}E{episode:02} - {title}"
|
|
104
|
-
|
|
105
|
-
try:
|
|
106
|
-
return format_str.format(
|
|
107
|
-
name=utils.clean_show_name(media_info["name"], args.char),
|
|
108
|
-
season=file_info["season"],
|
|
109
|
-
episode=file_info["episode"],
|
|
110
|
-
title=utils.clean_show_name(episode_info["name"], args.char),
|
|
111
|
-
year=media_info.get("premiered", "N/A").split("-")[0] if media_info.get("premiered") else "N/A",
|
|
112
|
-
)
|
|
113
|
-
except KeyError as e:
|
|
114
|
-
print(f"[!] Unknown placeholder in --format: {e}")
|
|
115
|
-
return None
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def rename_movie(file_info, args):
|
|
119
|
-
api_key = args.api_key or api.get_omdb_key()
|
|
120
|
-
media_info = api.fetch_omdb_metadata(file_info["name"], file_info["year"], api_key)
|
|
121
|
-
|
|
122
|
-
if not media_info:
|
|
123
|
-
if args.verbose:
|
|
124
|
-
print(f" → [API Error] Could not find movie '{file_info['name']}'")
|
|
125
|
-
return None
|
|
126
|
-
|
|
127
|
-
format_str = args.format or "{name} ({year})"
|
|
128
|
-
|
|
129
|
-
try:
|
|
130
|
-
return format_str.format(
|
|
131
|
-
name=utils.clean_show_name(media_info["Title"], args.char),
|
|
132
|
-
year=media_info.get("Year", "N/A"),
|
|
133
|
-
director=media_info.get("Director", "N/A").split(",")[0],
|
|
134
|
-
genre=media_info.get("Genre", "N/A").split(",")[0],
|
|
135
|
-
)
|
|
136
|
-
except KeyError as e:
|
|
137
|
-
print(f"[!] Unknown placeholder in --format: {e}")
|
|
138
|
-
return None
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def list_detected_media(directory, extensions, is_movie=False, args=None):
|
|
142
|
-
if is_movie and args is None:
|
|
143
|
-
raise ValueError("args is required when is_movie=True")
|
|
144
|
-
|
|
145
|
-
media = {}
|
|
146
|
-
if is_movie:
|
|
147
|
-
api_key = args.api_key or api.get_omdb_key()
|
|
148
|
-
|
|
149
|
-
for filename in os.listdir(directory):
|
|
150
|
-
file_ext = os.path.splitext(filename)[1][1:]
|
|
151
|
-
if file_ext.lower() in [e.lower() for e in extensions]:
|
|
152
|
-
info = utils.parse_filename(os.path.splitext(filename)[0], is_movie)
|
|
153
|
-
if info:
|
|
154
|
-
name = info["name"]
|
|
155
|
-
if is_movie:
|
|
156
|
-
if name not in media:
|
|
157
|
-
media_info = api.fetch_omdb_metadata(info["name"], info["year"], api_key)
|
|
158
|
-
if media_info:
|
|
159
|
-
media[name] = {
|
|
160
|
-
"filename": filename,
|
|
161
|
-
"year": media_info.get("Year", "N/A"),
|
|
162
|
-
"director": media_info.get("Director", "N/A"),
|
|
163
|
-
"genre": media_info.get("Genre", "N/A"),
|
|
164
|
-
}
|
|
165
|
-
else:
|
|
166
|
-
if name not in media:
|
|
167
|
-
media[name] = {"seasons": set(), "episodes": 0}
|
|
168
|
-
media[name]["seasons"].add(info["season"])
|
|
169
|
-
media[name]["episodes"] += 1
|
|
170
|
-
|
|
171
|
-
for name, data in media.items():
|
|
172
|
-
if is_movie:
|
|
173
|
-
print(f"Movie Name: {name}")
|
|
174
|
-
print(f"Filename: {data['filename']}")
|
|
175
|
-
print(f"Year of Release: {data['year']}")
|
|
176
|
-
print(f"Director: {data['director']}")
|
|
177
|
-
print(f"Genre: {data['genre']}")
|
|
178
|
-
print("---\n")
|
|
179
|
-
else:
|
|
180
|
-
print(f"[i] {name}: {len(data['seasons'])} season(s), {data['episodes']} episode(s)")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|