-
-
Notifications
You must be signed in to change notification settings - Fork 5
/
overlay_reset.py
552 lines (512 loc) · 27.6 KB
/
overlay_reset.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
import os, sys
from xml.etree.ElementTree import ParseError
from urllib.parse import quote
if sys.version_info[0] != 3 or sys.version_info[1] < 11:
print("Version Error: Version: %s.%s.%s incompatible please use Python 3.11+" % (sys.version_info[0], sys.version_info[1], sys.version_info[2]))
sys.exit(0)
try:
import cv2, numpy, plexapi, requests
from kometautils import util, KometaArgs, KometaLogger, Failed
from PIL import Image, ImageFile, UnidentifiedImageError
from plexapi.exceptions import BadRequest, NotFound, Unauthorized
from plexapi.server import PlexServer
from plexapi.video import Movie, Show, Season, Episode
from tmdbapis import TMDbAPIs, TMDbException
except (ModuleNotFoundError, ImportError) as e:
print(e)
print("Requirements Error: Requirements are not installed")
sys.exit(0)
options = [
{"arg": "u", "key": "url", "env": "PLEX_URL", "type": "str", "default": None, "help": "Plex URL of the Server you want to connect to."},
{"arg": "t", "key": "token", "env": "PLEX_TOKEN", "type": "str", "default": None, "help": "Plex Token of the Server you want to connect to."},
{"arg": "l", "key": "library", "env": "PLEX_LIBRARY", "type": "str", "default": None, "help": "Plex Library Name you want to reset."},
{"arg": "a", "key": "asset", "env": "KOMETA_ASSET", "type": "str", "default": None, "help": "Kometa Asset Folder to Scan for restoring posters."},
{"arg": "o", "key": "original", "env": "KOMETA_ORIGINAL", "type": "str", "default": None, "help": "Kometa Original Folder to Scan for restoring posters."},
{"arg": "ta", "key": "tmdbapi", "env": "TMDBAPI", "type": "str", "default": None, "help": "TMDb V3 API Key for restoring posters from TMDb."},
{"arg": "st", "key": "start", "env": "START", "type": "str", "default": None, "help": "Plex Item Title to Start restoring posters from."},
{"arg": "it", "key": "items", "env": "ITEMS", "type": "str", "default": None, "help": "Restore specific Plex Items by Title. Can use a bar-separated (|) list."},
{"arg": "lb", "key": "labels", "env": "LABELS", "type": "str", "default": None, "help": "Additional labels to remove. Can use a bar-separated (|) list."},
{"arg": "di", "key": "discord", "env": "DISCORD", "type": "str", "default": None, "help": "Webhook URL to channel for Notifications."},
{"arg": "ti", "key": "timeout", "env": "TIMEOUT", "type": "int", "default": 600, "help": "Timeout can be any number greater then 0. (Default: 600)"},
{"arg": "d", "key": "dry", "env": "DRY_RUN", "type": "bool", "default": False, "help": "Run as a Dry Run without making changes in Plex."},
{"arg": "f", "key": "flat", "env": "KOMETA_FLAT", "type": "bool", "default": False, "help": "Kometa Asset Folder uses Flat Assets Image Paths."},
{"arg": "nm", "key": "no-main", "env": "NO_MAIN", "type": "bool", "default": False, "help": "Do not restore the Main Movie/Show posters during run."},
{"arg": "s", "key": "season", "env": "SEASON", "type": "bool", "default": False, "help": "Restore Season posters during run."},
{"arg": "e", "key": "episode", "env": "EPISODE", "type": "bool", "default": False, "help": "Restore Episode posters during run."},
{"arg": "ir", "key": "ignore-resume", "env": "IGNORE_RESUME", "type": "bool", "default": None, "help": "Ignores the automatic resume."},
{"arg": "tr", "key": "trace", "env": "TRACE", "type": "bool", "default": False, "help": "Run with extra trace logs."},
{"arg": "lr", "key": "log-requests", "env": "LOG_REQUESTS", "type": "bool", "default": False, "help": "Run with every request logged."}
]
script_name = "Overlay Reset"
base_dir = os.path.dirname(os.path.abspath(__file__))
config_dir = os.path.join(base_dir, "config")
resume_file = os.path.join(config_dir, "resume.kor")
args = KometaArgs("Kometa-Team/Overlay-Reset", base_dir, options, use_nightly=False)
logger = KometaLogger(script_name, "overlay_reset", os.path.join(config_dir, "logs"), discord_url=args["discord"], is_trace=args["trace"], log_requests=args["log-requests"])
logger.secret([args["url"], args["discord"], args["tmdbapi"], args["token"], quote(str(args["url"])), requests.utils.urlparse(args["url"]).netloc])
requests.Session.send = util.update_send(requests.Session.send, args["timeout"])
plexapi.BASE_HEADERS["X-Plex-Client-Identifier"] = args.uuid
ImageFile.LOAD_TRUNCATED_IMAGES = True
logger.header(args, sub=True, discord_update=True)
logger.separator("Validating Options", space=False, border=False)
try:
logger.info("Script Started", log=False, discord=True, start="script")
except Failed as e:
logger.error(f"Discord URL Error: {e}")
report = []
current_rk = None
run_type = ""
try:
# Connect to Plex
if not args["url"]:
raise Failed("Error: No Plex URL Provided")
if not args["token"]:
raise Failed("Error: No Plex Token Provided")
if not args["library"]:
raise Failed("Error: No Plex Library Name Provided")
try:
server = PlexServer(args["url"], args["token"], timeout=args["timeout"])
plexapi.server.TIMEOUT = args["timeout"]
os.environ["PLEXAPI_PLEXAPI_TIMEOUT"] = str(args["timeout"])
logger.info("Plex Connection Successful")
except Unauthorized:
raise Failed("Plex Error: Plex token is invalid")
except (requests.exceptions.ConnectionError, ParseError):
raise Failed("Plex Error: Plex url is invalid")
lib = next((s for s in server.library.sections() if s.title == args["library"]), None)
if not lib:
raise Failed(f"Plex Error: Library: {args['library']} not found. Options: {', '.join([s.title for s in server.library.sections()])}")
if lib.type not in ["movie", "show"]:
raise Failed("Plex Error: Plex Library must be Movie or Show")
# Connect to TMDb
tmdbapi = None
if args["tmdbapi"]:
try:
tmdbapi = TMDbAPIs(args["tmdbapi"])
logger.info("TMDb Connection Successful")
except TMDbException as e:
logger.error(e)
# Check Labels
labels = ["Overlay"]
if args["labels"]:
labels.extend(args["labels"].split("|"))
logger.info(f"Labels to be Removed: {', '.join(labels)}")
# Check for Overlay Files
overlay_directory = os.path.join(base_dir, "overlays")
config_overlay_directory = os.path.join(config_dir, "overlays")
if not os.path.exists(overlay_directory):
raise Failed(f"Folder Error: overlays Folder not found {os.path.abspath(overlay_directory)}")
if not os.path.exists(config_overlay_directory):
os.makedirs(config_overlay_directory)
overlay_images = util.glob_filter(os.path.join(overlay_directory, "*.png")) + util.glob_filter(os.path.join(config_overlay_directory, "*.png"))
if not overlay_images:
raise Failed(f"Images Error: overlays Folder Images not found {os.path.abspath(os.path.join(overlay_directory, '*.png'))}")
logger.info("overlays Folder Images Loaded Successfully ")
# Check for Assets Folder
assets_directory = os.path.join(base_dir, "assets")
if os.path.exists(assets_directory) and os.listdir(assets_directory) and not args["asset"]:
args["asset"] = assets_directory
if args["asset"]:
args["asset"] = os.path.abspath(args["asset"])
if not os.path.exists(args["asset"]):
raise Failed(f"Folder Error: Asset Folder Path Not Found: {args['asset']}")
logger.info(f"Asset Folder Loaded: {args['asset']}")
else:
logger.warning("No Asset Folder Found")
# Check for Originals Folder
originals_directory = os.path.join(base_dir, "originals")
if os.path.exists(originals_directory) and os.listdir(originals_directory) and not args["original"]:
args["original"] = originals_directory
if args["original"]:
args["original"] = os.path.abspath(args["original"])
if not os.path.exists(args["original"]):
raise Failed(f"Folder Error: Original Folder Path Not Found: {os.path.abspath(args['original'])}")
logger.info(f"Originals Folder Loaded: {args['original']}")
else:
logger.warning("No Originals Folder Found")
def detect_overlay_in_image(item_title, poster_source, shape, img_path=None, url_path=None):
out_path = img_path
if url_path:
img_path = util.download_image(url_path, config_dir)
out_path = url_path
try:
logger.trace(f"{img_path}: {os.path.exists(img_path)}")
with Image.open(img_path) as pil_image:
exif_tags = pil_image.getexif()
if 0x04bc in exif_tags and exif_tags[0x04bc] == "overlay":
logger.debug(f"Overlay Detected: EXIF Overlay Tag Found ignoring {poster_source}: {out_path}")
return True
if (shape == "portrait" and pil_image.size != (1000, 1500)) or (shape == "landscape" and pil_image.size != (1920, 1080)):
logger.debug("No Overlay: Image not standard overlay size")
return False
logger.trace(f"{img_path}: {os.path.exists(img_path)}")
target = cv2.imread(img_path, cv2.IMREAD_GRAYSCALE)
if target is None:
logger.error(f"Image Load Error: {poster_source}: {out_path}", group=item_title)
return None
if target.shape[0] < 500 or target.shape[1] < 500:
logger.info(f"Image Error: {poster_source}: Dimensions {target.shape[0]}x{target.shape[1]} must be greater then 500x500: {out_path}")
return False
for overlay_image in overlay_images:
overlay = cv2.imread(overlay_image, cv2.IMREAD_GRAYSCALE)
if overlay is None:
logger.error(f"Image Load Error: {overlay_image}", group=item_title)
continue
if overlay.shape[0] > target.shape[0] or overlay.shape[1] > target.shape[1]:
logger.error(f"Image Error: {overlay_image} is larger than {poster_source}: {out_path}", group=item_title)
continue
template_result = cv2.matchTemplate(target, overlay, cv2.TM_CCOEFF_NORMED)
loc = numpy.where(template_result >= 0.95)
if len(loc[0]) == 0:
continue
logger.debug(f"Overlay Detected: {overlay_image} found in {poster_source}: {out_path} with score {template_result.max()}")
return True
return False
except Exception:
logger.stacktrace()
logger.error(f"Image Load Error: {poster_source}: {out_path}", group=item_title)
return None
def reset_from_plex(item_title, item_with_posters, shape, ignore=0):
for p, plex_poster in enumerate(item_with_posters.posters(), 1):
logger.trace(f"Poster URL: {plex_poster.key}")
reset_url = None
if plex_poster.key.startswith("/"):
temp_url = f"{args['url']}{plex_poster.key}&X-Plex-Token={args['token']}"
user = plex_poster.ratingKey.startswith("upload")
if not user or (user and detect_overlay_in_image(item_title, f"Plex Poster {p}", shape, url_path=temp_url) is False):
reset_url = temp_url
else:
reset_url = plex_poster.key
if reset_url:
if ignore < 1:
return reset_url
else:
ignore -= 1
def reset_poster(item_title, plex_item, tmdb_poster_url, asset_directory, asset_file_name, parent=None, shape="portrait"):
poster_source = None
poster_path = None
# Check Assets
if asset_directory:
asset_matches = util.glob_filter(os.path.join(asset_directory, f"{asset_file_name}.*"))
if len(asset_matches) > 0:
poster_source = "Assets Folder"
poster_path = asset_matches[0]
else:
logger.info("No Asset Found")
# Check Original Folder
if not poster_source and args["original"]:
png = os.path.join(args["original"], f"{plex_item.ratingKey}.png")
jpg = os.path.join(args["original"], f"{plex_item.ratingKey}.jpg")
if os.path.exists(png) and detect_overlay_in_image(item_title, "Original Poster", shape, img_path=png) is False:
poster_source = "Originals Folder"
poster_path = png
elif os.path.exists(jpg) and detect_overlay_in_image(item_title, "Original Poster", shape, img_path=jpg) is False:
poster_source = "Originals Folder"
poster_path = jpg
else:
logger.info("No Original Found")
# Check Plex
if not poster_source:
poster_path = reset_from_plex(item_title, plex_item, shape)
if poster_path:
poster_source = "Plex"
else:
logger.info("No Clean Plex Image Found")
# TMDb
if not poster_source:
if tmdb_poster_url:
poster_source = "TMDb"
poster_path = tmdb_poster_url
else:
logger.info("No TMDb Image Found")
# Check Item's Show
if not poster_source and parent:
poster_path = reset_from_plex(item_title, parent, shape)
if poster_path:
poster_source = "Plex's Show"
else:
logger.info("No Clean Plex Show Image Found")
def upload(attempt=0):
nonlocal poster_path
is_url = poster_source in ["TMDb", "Plex", "Plex's Show"]
try:
if args["dry"]:
logger.info(f"Poster will be Reset by {'URL' if is_url else 'File'} from {poster_source}")
else:
logger.info(f"Reset From {poster_source}")
logger.info(f"{'URL' if is_url else 'File'} Path: {poster_path}")
if is_url:
plex_item.uploadPoster(url=poster_path)
else:
plex_item.uploadPoster(filepath=poster_path)
except BadRequest as eb:
logger.error(eb, group=item_title)
if poster_source in ["Plex", "Plex's Show"]:
attempt += 1
logger.info(f"Trying next poster #{attempt + 1}")
if poster_source == "Plex":
poster_path = reset_from_plex(item_title, plex_item, shape, ignore=attempt)
if not poster_path:
logger.info("No Clean Plex Image Found")
if poster_source == "Plex's Show":
poster_path = reset_from_plex(item_title, parent, shape, ignore=attempt)
if not poster_path:
logger.info("No Clean Plex Show Image Found")
if poster_path:
upload(attempt=attempt)
else:
item_labels = [la.tag for la in plex_item.labels]
remove_labels = [la for la in labels if la in item_labels]
if remove_labels:
if not args["dry"]:
for label in remove_labels:
plex_item.removeLabel(label)
logger.info(f"Labels Removed: {', '.join(remove_labels)}")
else:
logger.info(f"Labels To Be Removed: {', '.join(remove_labels)}")
else:
logger.info("No Labels to Remove")
# Upload poster and Remove "Overlay" Label
if poster_source:
upload()
else:
logger.error("Image Error: No Image Found to Restore", group=item_title)
def get_title(plex_item):
if isinstance(plex_item, Movie):
return f"Movie: {item.title}"
elif isinstance(plex_item, Show):
return f"Show: {item.title}"
elif isinstance(plex_item, Season):
if season.title == f"Season {season.seasonNumber}":
return season.title
return f"Season {season.seasonNumber}: {season.title}"
elif isinstance(plex_item, Episode):
return f"Episode {episode.seasonEpisode.upper()}: {episode.title}"
else:
return f"Item: {item.title}"
def reload(plex_item):
try:
plex_item.reload(checkFiles=False, includeAllConcerts=False, includeBandwidths=False, includeChapters=False,
includeChildren=False, includeConcerts=False, includeExternalMedia=False, includeExtras=False,
includeFields=False, includeGeolocation=False, includeLoudnessRamps=False, includeMarkers=False,
includeOnDeck=False, includePopularLeaves=False, includeRelated=False, includeRelatedCount=0,
includeReviews=False, includeStations=False)
plex_item._autoReload = False
except (BadRequest, NotFound) as e1:
raise Failed(f"Plex Error: {get_title(plex_item)} Failed to Load: {e1}")
start_from = None
run_items = []
resume_rk = None
if args["items"]:
run_items = [rs for r in args["items"].split("|") if (rs := r.strip())]
if len(run_items) > 1:
str_items = ""
current = ""
for r in run_items[:-1]:
current += f"{r}, "
if len(current) > 75:
str_items += f"{current}\n"
current = ""
str_items += f"and {run_items[-1]}"
else:
str_items = run_items[0]
logger.separator(f"Resetting Specific Posters\n{str_items}")
run_type = "of Specific Items "
elif args["start"]:
start_from = args["start"]
logger.separator(f'Resetting Posters\nStarting From "{start_from}"')
run_type = f'Starting From "{start_from}" '
elif not args["ignore-resume"] and os.path.exists(resume_file):
with open(resume_file) as handle:
for line in handle.readlines():
line = line.strip()
if len(line) > 0:
resume_rk = str(line).strip()
break
os.remove(resume_file)
if resume_rk:
logger.separator(f'Resetting Posters\nStarting From Rating Key "{resume_rk}"')
run_type = f'Starting From Rating Key "{resume_rk}" '
if not run_items and not start_from and not resume_rk:
logger.separator("Resetting All Posters")
items = lib.all()
total_items = len(items)
for i, item in enumerate(items):
if run_items or start_from or resume_rk:
if (run_items and item.title not in run_items) or \
(start_from and item.title != start_from) or \
(resume_rk and str(item.ratingKey) != resume_rk):
logger.info(f"Skipping {i + 1}/{total_items} {item.title}")
continue
elif start_from:
start_from = None
elif resume_rk:
resume_rk = None
title = item.title
current_rk = item.ratingKey
logger.separator(f"Resetting {i + 1}/{total_items} {title}", start="reset")
try:
reload(item)
except Failed as e:
logger.error(e, group=title)
continue
# Find Item's Kometa Asset Directory
item_asset_directory = None
asset_name = None
if args["asset"]:
if not item.locations:
logger.error(f"Asset Error: No video filepath found fo {title}", group=title)
else:
file_name = "poster"
path_test = str(item.locations[0])
if not os.path.dirname(path_test):
path_test = path_test.replace("\\", "/")
asset_name = util.validate_filename(os.path.basename(os.path.dirname(path_test) if isinstance(item, Movie) else path_test))
if args["flat"]:
item_asset_directory = args["asset"]
file_name = asset_name
elif os.path.isdir(os.path.join(args["asset"], asset_name)):
item_asset_directory = os.path.join(args["asset"], asset_name)
else:
for n in range(1, 5):
new_path = args["asset"]
for m in range(1, n + 1):
new_path = os.path.join(new_path, "*")
matches = util.glob_filter(os.path.join(new_path, asset_name))
if len(matches) > 0:
item_asset_directory = os.path.abspath(matches[0])
break
if not item_asset_directory:
logger.warning(f"Asset Warning: No Asset Directory Found")
tmdb_item = None
if tmdbapi:
guid = requests.utils.urlparse(item.guid) # noqa
item_type = guid.scheme.split(".")[-1]
check_id = guid.netloc
tmdb_id = None
tvdb_id = None
imdb_id = None
if item_type == "plex":
for guid_tag in item.guids:
url_parsed = requests.utils.urlparse(guid_tag.id) # noqa
if url_parsed.scheme == "tvdb":
tvdb_id = int(url_parsed.netloc)
elif url_parsed.scheme == "imdb":
imdb_id = url_parsed.netloc
elif url_parsed.scheme == "tmdb":
tmdb_id = int(url_parsed.netloc)
if not tvdb_id and not imdb_id and not tmdb_id:
item.refresh()
elif item_type == "imdb":
imdb_id = check_id
elif item_type == "thetvdb":
tvdb_id = int(check_id)
elif item_type == "themoviedb":
tmdb_id = int(check_id)
elif item_type in ["xbmcnfo", "xbmcnfotv"]:
if len(check_id) > 10:
logger.warning(f"XMBC NFO Local ID: {check_id}")
try:
if item_type == "xbmcnfo":
tmdb_id = int(check_id)
else:
tvdb_id = int(check_id)
except ValueError:
imdb_id = check_id
if not tvdb_id and not imdb_id and not tmdb_id:
logger.error("Plex Error: No External GUIDs found", group=title)
if not tmdb_id and imdb_id:
try:
results = tmdbapi.find_by_id(imdb_id=imdb_id)
if results.movie_results and isinstance(item, Movie):
tmdb_id = results.movie_results[0].id
elif results.tv_results and isinstance(item, Show):
tmdb_id = results.tv_results[0].id
except TMDbException as e:
logger.warning(e, group=title)
if not tmdb_id and tvdb_id and isinstance(item, Show):
try:
results = tmdbapi.find_by_id(tvdb_id=tvdb_id)
if results.tv_results:
tmdb_id = results.tv_results[0].id
except TMDbException as e:
logger.warning(e, group=title)
if tmdb_id:
try:
tmdb_item = tmdbapi.movie(tmdb_id) if isinstance(item, Movie) else tmdbapi.tv_show(tmdb_id)
except TMDbException as e:
logger.error(f"TMDb Error: {e}", group=title)
else:
logger.error("Plex Error: TMDb ID Not Found", group=title)
if not args["no-main"]:
reset_poster(title, item, tmdb_item.poster_url if tmdb_item else None, item_asset_directory, asset_name if args["flat"] else "poster")
logger.info(f"Runtime: {logger.runtime('reset')}")
if isinstance(item, Show) and (args["season"] or args["episode"]):
tmdb_seasons = {s.season_number: s for s in tmdb_item.seasons} if tmdb_item else {}
for season in item.seasons():
title = f"Season {season.seasonNumber}"
title = title if title == season.title else f"{title}: {season.title}"
title = f"{item.title}\n {title}"
if args["season"]:
logger.separator(f"Resetting {title}", start="reset")
try:
reload(season)
except Failed as e:
logger.error(e, group=title)
continue
tmdb_poster = tmdb_seasons[season.seasonNumber].poster_url if season.seasonNumber in tmdb_seasons else None
file_name = f"Season{'0' if not season.seasonNumber or season.seasonNumber < 10 else ''}{season.seasonNumber}"
reset_poster(title, season, tmdb_poster, item_asset_directory, f"{asset_name}_{file_name}" if args["flat"] else file_name, parent=item)
logger.info(f"Runtime: {logger.runtime('reset')}")
if args["episode"]:
if not args["season"]:
try:
reload(season)
except Failed as e:
logger.error(e, group=title)
continue
tmdb_episodes = {}
if season.seasonNumber in tmdb_seasons:
for episode in tmdb_seasons[season.seasonNumber].episodes:
episode._partial = False
try:
tmdb_episodes[episode.episode_number] = episode
except TMDbException:
logger.error(f"TMDb Error: An Episode of Season {season.seasonNumber} was Not Found", group=title)
for episode in season.episodes():
title = f"{item.title}\nEpisode {episode.seasonEpisode.upper()}: {episode.title}"
logger.separator(f"Resetting {title}", start="reset")
try:
reload(episode)
except Failed as e:
logger.error(e, group=title)
continue
tmdb_poster = tmdb_episodes[episode.episodeNumber].still_url if episode.episodeNumber in tmdb_episodes else None
file_name = episode.seasonEpisode.upper()
reset_poster(title, episode, tmdb_poster, item_asset_directory, f"{asset_name}_{file_name}" if args["flat"] else file_name, shape="landscape")
logger.info(f"Runtime: {logger.runtime('reset')}")
current_rk = None
except Failed as e:
logger.separator()
logger.critical(e, discord=True)
logger.separator()
except Exception as e:
logger.separator()
logger.stacktrace()
logger.critical(e, discord=True)
logger.separator()
except KeyboardInterrupt:
if current_rk:
with open(resume_file, "w") as handle:
handle.write(str(current_rk))
logger.separator(f"User Canceled Run {script_name}")
raise
if current_rk:
with open(resume_file, "w") as handle:
handle.write(str(current_rk))
logger.error_report()
logger.switch()
report.append([(f"{script_name} Finished", "")])
report.append([("Total Runtime", f"{logger.runtime()}")])
description = f"{args['library']} Library{' Dry' if args['dry'] else ''} Run {run_type}Finished"
logger.report(f"{script_name} Summary", description=description, rows=report, width=18, discord=True)