forked from RickDB/PlexAniSync
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathanilist.py
838 lines (737 loc) · 35.6 KB
/
anilist.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
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
# coding=utf-8
import logging
import re
from typing import Dict, List, Optional
from dataclasses import dataclass
import inflect
from guessit import guessit
from plexmodule import PlexWatchedSeries
from custom_mappings import AnilistCustomMapping
from graphql import fetch_user_list, search_by_name, search_by_id, update_series
logger = logging.getLogger("PlexAniSync")
CUSTOM_MAPPINGS: Dict[str, List[AnilistCustomMapping]] = {}
ANILIST_PLEX_EPISODE_COUNT_PRIORITY = False
# Set this to True for logging failed AniList matches to
# failed_matches.txt file
ANILIST_LOG_FAILED_MATCHES = False
def int_to_roman_numeral(decimal: int) -> str:
if not isinstance(decimal, type(1)):
return decimal
if not 0 < decimal < 4000:
return str(decimal)
ints = (1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1)
nums = ("M", "CM", "D", "CD", "C", "XC", "L", "XL", "X", "IX", "V", "IV", "I")
result = []
for i, number in enumerate(ints):
count = int(decimal / number)
result.append(nums[i] * count)
decimal -= number * count
return "".join(result)
def log_to_file(message: str):
file = open("failed_matches.txt", "a+", encoding="utf-8")
file.write(f"{message}\n")
file.close()
def clean_failed_matches_file():
try:
# create or overwrite the file with empty content
open("failed_matches.txt", 'w', encoding="utf-8").close()
except BaseException:
pass
@dataclass
class AnilistSeries:
anilist_id: int
series_type: str
series_format: str
source: str
status: str
media_status: str
progress: int
season: str
episodes: int
title_english: str
title_romaji: str
synonyms: List[str]
started_year: int
ended_year: int
def process_user_list(username: str) -> Optional[List[AnilistSeries]]:
logger.info(f"[ANILIST] Retrieving AniList list for user: {username}")
anilist_series = []
try:
list_items = fetch_user_list(username)
if not list_items:
logger.critical(f"[ANILIST] Failed to return list for user: {username}")
return None
else:
for item in list_items:
for media_collection in item.MediaListCollection.lists:
if hasattr(media_collection, "entries"):
for list_entry in media_collection.entries:
if (hasattr(list_entry, "status")
and list_entry.status in ["CURRENT", "PLANNING", "COMPLETED", "DROPPED", "PAUSED", "REPEATING"]
and list_entry.media is not None):
series_obj = mediaitem_to_object(list_entry)
anilist_series.append(series_obj)
except BaseException as exception:
logger.critical(f"[ANILIST] Failed to return list for user: {username}", exception)
return None
logger.info(f"[ANILIST] Found {len(anilist_series)} anime series on list")
return anilist_series
def search_item_to_obj(item) -> Optional[AnilistSeries]:
try:
if item:
return mediaitem_to_object(item.data)
except BaseException:
pass
return None
def mediaitem_to_object(media_item) -> AnilistSeries:
anilist_id = media_item.media.id
series_type = ""
series_format = ""
source = ""
status = ""
media_status = ""
progress = 0
season = ""
episodes = 0
title_english = ""
title_romaji = ""
synonyms = []
started_year = 0
ended_year = 0
if hasattr(media_item, "status"):
status = media_item.status
if hasattr(media_item, "progress"):
progress = media_item.progress
if hasattr(media_item.media, "status"):
media_status = media_item.media.status
if hasattr(media_item.media, "type"):
series_type = media_item.media.type
if hasattr(media_item.media, "format"):
series_format = media_item.media.format
if hasattr(media_item.media, "source"):
source = media_item.media.source
if hasattr(media_item.media, "season"):
season = media_item.media.season
if hasattr(media_item.media, "episodes"):
episodes = media_item.media.episodes
if hasattr(media_item.media.title, "english"):
title_english = media_item.media.title.english
if hasattr(media_item.media.title, "romaji"):
title_romaji = media_item.media.title.romaji
if hasattr(media_item.media, "synonyms"):
synonyms = media_item.media.synonyms
if hasattr(media_item.media.startDate, "year"):
started_year = media_item.media.startDate.year
if hasattr(media_item.media.endDate, "year"):
ended_year = media_item.media.endDate.year
series = AnilistSeries(
anilist_id,
series_type,
series_format,
source,
status,
media_status,
progress,
season,
episodes,
title_english,
title_romaji,
synonyms,
started_year,
ended_year,
)
return series
def match_to_plex(anilist_series: List[AnilistSeries], plex_series_watched: List[PlexWatchedSeries]):
logger.info("[ANILIST] Matching Plex series to Anilist")
for plex_series in plex_series_watched:
plex_title = plex_series.title
plex_title_sort = plex_series.title_sort
plex_title_original = plex_series.title_original
plex_year = plex_series.year
plex_seasons = plex_series.seasons
plex_anilist_id = plex_series.anilist_id
custom_mapping_seasons_anilist_id = 0
mapped_season_count = 0
plex_watched_episode_count_custom_mapping = 0
logger.info("--------------------------------------------------")
# Check if we have custom mappings for all seasons (One Piece for example)
if len(plex_seasons) > 1:
custom_mapping_season_count = 0
for plex_season in plex_seasons:
season_mappings = retrieve_season_mappings(
plex_title, plex_season.season_number
)
matched_id = 0
if season_mappings:
matched_id = season_mappings[0].anime_id
if custom_mapping_seasons_anilist_id in (0, matched_id):
plex_watched_episode_count_custom_mapping += plex_season.watched_episodes
custom_mapping_season_count += 1
custom_mapping_seasons_anilist_id = matched_id
# If we had custom mappings for multiple seasons with the same ID use
# cumulative episode count and skip per season processing
if custom_mapping_season_count > 1:
logger.warning(
"[ANILIST] Found same custom mapping id for multiple seasons "
"so not using per season processing but updating as one | "
f"title: {plex_title} | anilist id: {custom_mapping_seasons_anilist_id} | "
f"total watched episodes: {plex_watched_episode_count_custom_mapping}"
)
add_or_update_show_by_id(
anilist_series, plex_title, plex_year, True, plex_watched_episode_count_custom_mapping, custom_mapping_seasons_anilist_id
)
mapped_season_count = custom_mapping_season_count
if custom_mapping_season_count == len(plex_seasons):
continue
# Start processing of any remaining seasons
for plex_season in plex_seasons[mapped_season_count:]:
season_number = plex_season.season_number
plex_watched_episode_count = plex_season.watched_episodes
if plex_watched_episode_count == 0:
logger.info(
f"[ANILIST] Series {plex_title} has 0 watched episodes for "
f"season {season_number}, skipping"
)
continue
matched_anilist_series = []
skip_year_check = False
# for first season use regular search
if season_number == 1:
found_match = False
plex_title_clean = clean_title(plex_title)
plex_title_sort_clean = clean_title(plex_title_sort)
plex_title_original_clean = clean_title(plex_title_original)
plex_title_without_year = re.sub(r"\(\d{4}\)", "", plex_title).strip()
plex_title_sort_without_year = re.sub(r"\(\d{4}\)", "", plex_title_sort).strip()
plex_title_original_without_year = re.sub(r"\(\d{4}\)", "", plex_title_original).strip()
plex_title_guessit = plex_title
plex_title_sort_guessit = plex_title_sort
plex_title_original_guessit = plex_title_original
try:
plex_title_guessit = guessit(plex_title.lower())["title"].lower()
plex_title_sort_guessit = guessit(plex_title_sort.lower())[
"title"
].lower()
plex_title_original_guessit = guessit(plex_title_original.lower())[
"title"
].lower()
except Exception:
logger.exception(
f"Error parsing parsing guessit title for: {plex_title} | {plex_title_sort} | {plex_title_original}"
)
# For linting, the variable is currently unused by might be useful in
# the future
assert plex_title_original_guessit
potential_titles = [
plex_title.lower(),
plex_title_sort.lower(),
plex_title_original.lower(),
plex_title_guessit,
plex_title_sort_guessit,
plex_title_clean,
plex_title_sort_clean,
plex_title_original_clean,
plex_title_without_year,
plex_title_sort_without_year,
plex_title_original_without_year,
]
# Remove duplicates from potential title list
potential_titles_cleaned = [
i
for n, i in enumerate(potential_titles)
if i not in potential_titles[:n]
]
potential_titles = list(potential_titles_cleaned)
season_mappings = retrieve_season_mappings(plex_title, season_number)
# Custom mapping check - check user list
if season_mappings:
watchcounts = map_watchcount_to_seasons(plex_title, season_mappings, plex_season.watched_episodes)
for anime_id in watchcounts:
logger.info(
f"[ANILIST] Used custom mapping | title: {plex_title} | season: {season_number} | anilist id: {anime_id}"
)
add_or_update_show_by_id(anilist_series, plex_title, plex_year, True, watchcounts[anime_id], anime_id)
# If custom match found continue to next
continue
# Reordered checks from above to ensure that custom mappings always take precedent
if plex_anilist_id:
logger.info(f"[ANILIST] Series {plex_title} has Anilist ID {plex_anilist_id} in its metadata, using that for updating")
add_or_update_show_by_id(anilist_series, plex_title, plex_year, True, plex_watched_episode_count, plex_anilist_id)
continue
# Regular matching
if found_match is False:
for series in anilist_series:
match_series_against_potential_titles(series, potential_titles, matched_anilist_series)
# Series not listed so search for it
if not all(matched_anilist_series) or not matched_anilist_series:
logger.warning(f"[ANILIST] Plex series was not on your AniList list: {plex_title}")
potential_titles_search = [
plex_title.lower(),
plex_title_sort.lower(),
plex_title_original.lower(),
plex_title_without_year,
plex_title_sort_without_year,
plex_title_original_without_year,
]
# Remove duplicates from potential title list
potential_titles_search_cleaned = [
i
for n, i in enumerate(potential_titles_search)
if i not in potential_titles_search[:n]
]
potential_titles_search = []
potential_titles_search = list(potential_titles_search_cleaned)
media_id_search = None
for potential_title in potential_titles_search:
logger.warning(
f"[ANILIST] Searching best match using title: {potential_title}"
)
media_id_search = find_id_best_match(potential_title, plex_year)
if media_id_search:
logger.warning(
f"[ANILIST] Adding new series id to list: {media_id_search} | Plex episodes watched: {plex_watched_episode_count}"
)
add_by_id(
media_id_search,
plex_title,
plex_year,
plex_watched_episode_count,
False,
)
break
if not media_id_search:
error_message = (
f"[ANILIST] Failed to find valid match on AniList for: {plex_title}"
)
logger.error(error_message)
if ANILIST_LOG_FAILED_MATCHES:
log_to_file(error_message)
# Series exists on list so checking if update required
else:
update_entry(
plex_title,
plex_year,
plex_watched_episode_count,
matched_anilist_series,
skip_year_check,
)
matched_anilist_series = []
else:
media_id_search = None
# ignore the Plex year since Plex does not have years for seasons
skip_year_check = True
season_mappings = retrieve_season_mappings(plex_title, season_number)
if season_mappings:
watchcounts = map_watchcount_to_seasons(plex_title, season_mappings, plex_season.watched_episodes)
for anime_id in watchcounts:
logger.info(
f"[ANILIST] Used custom mapping | title: {plex_title} | season: {season_number} | anilist id: {anime_id}"
)
add_or_update_show_by_id(anilist_series, plex_title, plex_year, True, watchcounts[anime_id], anime_id)
# If custom match found continue to next
continue
else:
if plex_year is not None:
media_id_search = find_id_season_best_match(
plex_title, season_number, plex_year
)
else:
logger.error(
"[ANILIST] Skipped season lookup as Plex did not supply "
"a show year for {plex_title}, recommend checking Plex Web "
"and correcting the show year manually."
)
plex_title_lookup = plex_title
if media_id_search:
add_or_update_show_by_id(anilist_series, plex_title, plex_year, skip_year_check, plex_watched_episode_count, media_id_search)
else:
error_message = (
f"[ANILIST] Failed to find valid season title match on AniList for: {plex_title_lookup} season {season_number}"
)
logger.error(error_message)
if ANILIST_LOG_FAILED_MATCHES:
log_to_file(error_message)
def find_mapped_series(anilist_series: List[AnilistSeries], anime_id: int):
return next(filter(lambda s: s.anilist_id == anime_id, anilist_series), None)
def match_series_against_potential_titles(
series: AnilistSeries, potential_titles: List[str], matched_anilist_series: List[AnilistSeries]
):
if series.title_english:
if series.title_english.lower() in potential_titles:
matched_anilist_series.append(series)
else:
series_title_english_clean = clean_title(series.title_english)
if series_title_english_clean in potential_titles:
matched_anilist_series.append(series)
if series.title_romaji:
if series.title_romaji.lower() in potential_titles:
if series not in matched_anilist_series:
matched_anilist_series.append(series)
else:
series_title_romaji_clean = clean_title(series.title_romaji)
if series_title_romaji_clean in potential_titles:
if series not in matched_anilist_series:
matched_anilist_series.append(series)
if series.synonyms:
for synonym in series.synonyms:
if synonym.lower() in potential_titles:
if series not in matched_anilist_series:
matched_anilist_series.append(series)
else:
synonym_clean = clean_title(synonym)
if synonym_clean in potential_titles:
matched_anilist_series.append(series)
def find_id_season_best_match(title: str, season: int, year: int) -> Optional[int]:
media_id = None
# logger.warning('[ANILIST] Searching AniList for title: %s | season: %s' % (title, season))
match_title = clean_title(title)
match_year = int(year)
match_title_season_suffix1 = f"{match_title} {int_to_roman_numeral(season)}"
match_title_season_suffix2 = f"{match_title} season {season}"
match_title_season_suffix3 = f"{match_title} part {season}"
match_title_season_suffix4 = f"{match_title} {season}"
# oridinal season (1st 2nd etc..)
try:
p_engine = inflect.engine()
match_title_season_suffix5 = f"{match_title} {p_engine.ordinal(season)} season"
except BaseException:
logger.error(
"Error while converting season to ordinal string, make sure Inflect pip package is installed"
)
match_title_season_suffix5 = match_title_season_suffix2
# oridinal season - variation 1 (1st 2nd Thread) - see AniList ID: 21000
try:
p_engine = inflect.engine()
match_title_season_suffix6 = f"{match_title} {p_engine.ordinal(season)} thread"
except BaseException:
logger.error(
"Error while converting season to ordinal string, make sure Inflect pip package is installed"
)
match_title_season_suffix6 = match_title_season_suffix2
potential_titles = [
match_title_season_suffix1.lower().strip(),
match_title_season_suffix2.lower().strip(),
match_title_season_suffix3.lower().strip(),
match_title_season_suffix4.lower().strip(),
match_title_season_suffix5.lower().strip(),
match_title_season_suffix6.lower().strip(),
]
list_items = search_by_name(title)
if list_items:
for item in list_items:
if item[0] is not None and item[0].media:
for media_item in item[0].media:
title_english = ""
title_english_for_matching = ""
title_romaji = ""
title_romaji_for_matching = ""
started_year = ""
if hasattr(media_item.title, "english") and media_item.title.english is not None:
title_english = media_item.title.english
title_english_for_matching = clean_title(title_english)
if hasattr(media_item.title, "romaji") and media_item.title.romaji is not None:
title_romaji = media_item.title.romaji
title_romaji_for_matching = clean_title(title_romaji)
if hasattr(media_item.startDate, "year") and media_item.startDate.year is not None:
started_year = int(media_item.startDate.year)
else:
logger.warning(
"[ANILIST] Anilist series did not have year attribute so skipping this result and moving to next: "
f"{title_english} | {title_romaji}"
)
continue
for potential_title in potential_titles:
potential_title = clean_title(potential_title)
# logger.info('Comparing AniList: %s | %s[%s] <===> %s' %
# (title_english_for_matching, title_romaji_for_matching, started_year, potential_title))
if title_english_for_matching == potential_title:
if started_year < match_year:
logger.warning(
f"[ANILIST] Found match: {title_english} [{media_id}] | "
f"skipping as it was released before first season ({started_year} <==> {match_year})"
)
else:
media_id = media_item.id
logger.info(
f"[ANILIST] Found match: {title_english} [{media_id}]"
)
break
if title_romaji_for_matching == potential_title:
if started_year < match_year:
logger.warning(
f"[ANILIST] Found match: {title_romaji} [{media_id}] | "
f"skipping as it was released before first season ({started_year} <==> {match_year})"
)
else:
media_id = media_item.id
logger.info(
f"[ANILIST] Found match: {title_romaji} [{media_id}]"
)
break
if media_id == 0:
logger.error(f"[ANILIST] No match found for title: {title}")
return media_id
def find_id_best_match(title: str, year: int) -> Optional[int]:
media_id = None
# logger.warning('[ANILIST] Searching AniList for title: %s' % (title))
match_title = clean_title(title)
match_year = str(year)
list_items = search_by_name(title)
if list_items:
for item in list_items:
if item[0] is not None and item[0].media:
for media_item in item[0].media:
title_english = ""
title_english_for_matching = ""
title_romaji = ""
title_romaji_for_matching = ""
synonyms = ""
synonyms_for_matching = ""
started_year = ""
if hasattr(media_item.title, "english") and media_item.title.english is not None:
title_english = media_item.title.english
title_english_for_matching = clean_title(title_english)
if hasattr(media_item.title, "romaji") and media_item.title.romaji is not None:
title_romaji = media_item.title.romaji
title_romaji_for_matching = clean_title(title_romaji)
if hasattr(media_item.startDate, "year"):
started_year = str(media_item.startDate.year)
# logger.info('Comparing AniList: %s | %s[%s] <===> %s[%s]' % (title_english, title_romaji, started_year, match_title, match_year))
if (
match_title == title_english_for_matching
and match_year == started_year
):
media_id = media_item.id
logger.warning(
f"[ANILIST] Found match: {title_english} [{media_id}]"
)
break
if (
match_title == title_romaji_for_matching
and match_year == started_year
):
media_id = media_item.id
logger.warning(
f"[ANILIST] Found match: {title_romaji} [{media_id}]"
)
break
if hasattr(media_item, "synonyms") and media_item.synonyms is not None:
for synonym in media_item.synonyms:
synonyms = synonym
synonyms_for_matching = clean_title(synonyms)
if (
match_title == synonyms_for_matching
and match_year == started_year
):
media_id = media_item.id
logger.warning(
f"[ANILIST] Found match in synonyms: {synonyms} [{media_id}]"
)
break
if (
match_title == title_romaji_for_matching
and match_year != started_year
):
logger.info(
f"[ANILIST] Found match however started year is a mismatch: {title_romaji} [AL: {started_year} <==> Plex: {match_year}] "
)
elif (
match_title == title_english_for_matching
and match_year != started_year
):
logger.info(
f"[ANILIST] Found match however started year is a mismatch: {title_english} [AL: {started_year} <==> Plex: {match_year}] "
)
if media_id is None:
logger.error(f"[ANILIST] No match found for title: {title}")
return media_id
def add_or_update_show_by_id(anilist_series: List[AnilistSeries], plex_title: str, plex_year: int, skip_year_check: bool, watched_episodes: int, anime_id: int):
series = find_mapped_series(anilist_series, anime_id)
if series:
logger.info(
f"[ANILIST] Updating series: {series.title_english} | Episodes watched: {watched_episodes}"
)
update_entry(
plex_title,
plex_year,
watched_episodes,
[series],
skip_year_check,
)
else:
logger.warning(
f"[ANILIST] Adding new series id to list: {anime_id} | Episodes watched: {watched_episodes}"
)
add_by_id(
anime_id,
plex_title,
plex_year,
watched_episodes,
skip_year_check,
)
def add_by_id(
anilist_id: int, plex_title: str, plex_year: int, plex_watched_episode_count: int, ignore_year: bool
):
media_lookup_result = search_by_id(anilist_id)
if media_lookup_result:
anilist_obj = search_item_to_obj(media_lookup_result)
if anilist_obj:
update_entry(
plex_title,
plex_year,
plex_watched_episode_count,
[anilist_obj],
ignore_year,
)
else:
logger.error(
"[ANILIST] failed to get anilist object for list adding, skipping series"
)
else:
logger.error(
f"[ANILIST] failed to get anilist search result for id: {anilist_id}"
)
def update_entry(
title: str, year: int, watched_episode_count: int, matched_anilist_series: List[AnilistSeries], ignore_year: bool
):
for series in matched_anilist_series:
status = ""
logger.info(f"[ANILIST] Found AniList entry for Plex title: {title}")
if hasattr(series, "status"):
status = series.status
if status == "COMPLETED":
logger.info(
"[ANILIST] Series is already marked as completed on AniList so skipping update"
)
return
if hasattr(series, "started_year") and year != series.started_year:
if ignore_year is False:
logger.error(
f"[ANILIST] Series year did not match (skipping update) => Plex has {year} and AniList has {series.started_year}"
)
continue
elif ignore_year is True:
logger.info(
f"[ANILIST] Series year did not match however skip year check was given so adding anyway => "
f"Plex has {year} and AniList has {series.started_year}"
)
anilist_total_episodes = 0
anilist_episodes_watched = 0
anilist_media_status = ""
if hasattr(series, "media_status"):
anilist_media_status = series.media_status
if hasattr(series, "episodes"):
if series.episodes is not None:
try:
anilist_total_episodes = int(series.episodes)
except BaseException:
logger.error(
"Series has unknown total total episodes on AniList "
"(not an Integer), will most likely not match up properly"
)
anilist_total_episodes = 0
else:
logger.error(
"Series has no total episodes which is normal for shows "
"with undetermined end-date otherwise can be invalid info "
"on AniList (NoneType), using Plex watched count as fallback"
)
anilist_total_episodes = watched_episode_count
if hasattr(series, "progress"):
try:
anilist_episodes_watched = int(series.progress)
except BaseException:
pass
if (
watched_episode_count >= anilist_total_episodes > 0
and anilist_media_status == "FINISHED"
):
# series completed watched
logger.warning(
f"[ANILIST] Plex episode watch count [{watched_episode_count}] was higher than the "
f"one on AniList total episodes for that series [{anilist_total_episodes}] | updating "
"AniList entry to completed"
)
update_episode_incremental(series, watched_episode_count, anilist_episodes_watched, "COMPLETED")
return
elif (
watched_episode_count > anilist_episodes_watched
and anilist_total_episodes > 0
):
# episode watch count higher than plex
new_status = status if status == "REPEATING" else "CURRENT"
logger.warning(
f"[ANILIST] Plex episode watch count [{watched_episode_count}] was higher than the one"
f" on AniList [{anilist_episodes_watched}] which has total of {anilist_total_episodes} "
f"episodes | updating AniList entry to {new_status}"
)
update_episode_incremental(series, watched_episode_count, anilist_episodes_watched, new_status)
return
elif watched_episode_count == anilist_episodes_watched:
logger.info(
"[ANILIST] Episodes watched was the same on AniList and Plex so skipping update"
)
return
elif (
anilist_episodes_watched > watched_episode_count
and ANILIST_PLEX_EPISODE_COUNT_PRIORITY
):
if watched_episode_count > 0:
logger.info(
f"[ANILIST] Episodes watched was higher on AniList [{anilist_episodes_watched}] than on Plex [{watched_episode_count}] "
"however Plex episode count override is active so updating"
)
# Since AniList episode count is higher we don't loop thru
# updating the notification feed and just set the AniList
# episode count once
update_series(series.anilist_id, watched_episode_count, "CURRENT")
return
else:
logger.info(
f"[ANILIST] Episodes watched was higher on AniList [{anilist_episodes_watched}] than "
f"on Plex [{watched_episode_count}] with Plex episode count override active however "
"Plex watched count is 0 so skipping update"
)
elif anilist_episodes_watched > watched_episode_count:
logger.info(
f"[ANILIST] Episodes watched was higher on AniList [{anilist_episodes_watched}] than on Plex [{watched_episode_count}] so skipping update"
)
elif anilist_total_episodes <= 0:
logger.info(
"[ANILIST] AniList total episodes was 0 so most likely invalid data"
)
def update_episode_incremental(series: AnilistSeries, watched_episode_count: int, anilist_episodes_watched: int, new_status: str):
# calculate episode difference and iterate up so activity stream lists
# episodes watched if episode difference exceeds 32 only update most
# recent as otherwise will flood the notification feed
episode_difference = watched_episode_count - anilist_episodes_watched
if episode_difference > 32:
update_series(series.anilist_id, watched_episode_count, new_status)
else:
for current_episodes_watched in range(anilist_episodes_watched + 1, watched_episode_count + 1):
update_series(series.anilist_id, current_episodes_watched, new_status)
def retrieve_season_mappings(title: str, season: int) -> List[AnilistCustomMapping]:
season_mappings: List[AnilistCustomMapping] = []
if CUSTOM_MAPPINGS and title.lower() in CUSTOM_MAPPINGS:
season_mappings = CUSTOM_MAPPINGS[title.lower()]
# filter mappings by season
season_mappings = [e for e in season_mappings if e.season == season]
return season_mappings
def map_watchcount_to_seasons(title: str, season_mappings: List[AnilistCustomMapping], watched_episodes: int) -> Dict[int, int]:
# mapping from anilist-id to watched episodes
episodes_in_anilist_entry: Dict[int, int] = {}
total_mapped_episodes = 0
season = season_mappings[0].season
for mapping in season_mappings:
if watched_episodes >= mapping.start:
episodes_in_season = (watched_episodes - mapping.start + 1)
total_mapped_episodes += episodes_in_season
episodes_in_anilist_entry[mapping.anime_id] = episodes_in_season
if total_mapped_episodes < watched_episodes:
logger.warning(
f"[ANILIST] Custom mapping is incomplete for {title} season {season}. "
f"Watch count is {watched_episodes}, but number of mapped episodes is {total_mapped_episodes}"
)
return episodes_in_anilist_entry
def clean_title(title: str) -> str:
return re.sub("[^A-Za-z0-9]+", "", title.lower().strip())