anipy-cli 2.7.17__py3-none-any.whl → 3.8.2__py3-none-any.whl

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.
Files changed (67) hide show
  1. anipy_cli/__init__.py +2 -20
  2. anipy_cli/anilist_proxy.py +229 -0
  3. anipy_cli/arg_parser.py +109 -21
  4. anipy_cli/cli.py +98 -0
  5. anipy_cli/clis/__init__.py +17 -0
  6. anipy_cli/clis/anilist_cli.py +62 -0
  7. anipy_cli/clis/base_cli.py +34 -0
  8. anipy_cli/clis/binge_cli.py +96 -0
  9. anipy_cli/clis/default_cli.py +115 -0
  10. anipy_cli/clis/download_cli.py +85 -0
  11. anipy_cli/clis/history_cli.py +96 -0
  12. anipy_cli/clis/mal_cli.py +71 -0
  13. anipy_cli/{cli/clis → clis}/seasonal_cli.py +9 -6
  14. anipy_cli/colors.py +14 -8
  15. anipy_cli/config.py +387 -90
  16. anipy_cli/discord.py +34 -0
  17. anipy_cli/download_component.py +194 -0
  18. anipy_cli/logger.py +200 -0
  19. anipy_cli/mal_proxy.py +228 -0
  20. anipy_cli/menus/__init__.py +6 -0
  21. anipy_cli/menus/anilist_menu.py +671 -0
  22. anipy_cli/{cli/menus → menus}/base_menu.py +9 -14
  23. anipy_cli/menus/mal_menu.py +657 -0
  24. anipy_cli/menus/menu.py +265 -0
  25. anipy_cli/menus/seasonal_menu.py +270 -0
  26. anipy_cli/prompts.py +387 -0
  27. anipy_cli/util.py +268 -0
  28. anipy_cli-3.8.2.dist-info/METADATA +71 -0
  29. anipy_cli-3.8.2.dist-info/RECORD +31 -0
  30. {anipy_cli-2.7.17.dist-info → anipy_cli-3.8.2.dist-info}/WHEEL +1 -2
  31. anipy_cli-3.8.2.dist-info/entry_points.txt +3 -0
  32. anipy_cli/cli/__init__.py +0 -1
  33. anipy_cli/cli/cli.py +0 -37
  34. anipy_cli/cli/clis/__init__.py +0 -6
  35. anipy_cli/cli/clis/base_cli.py +0 -43
  36. anipy_cli/cli/clis/binge_cli.py +0 -54
  37. anipy_cli/cli/clis/default_cli.py +0 -46
  38. anipy_cli/cli/clis/download_cli.py +0 -92
  39. anipy_cli/cli/clis/history_cli.py +0 -64
  40. anipy_cli/cli/clis/mal_cli.py +0 -27
  41. anipy_cli/cli/menus/__init__.py +0 -3
  42. anipy_cli/cli/menus/mal_menu.py +0 -411
  43. anipy_cli/cli/menus/menu.py +0 -102
  44. anipy_cli/cli/menus/seasonal_menu.py +0 -174
  45. anipy_cli/cli/util.py +0 -118
  46. anipy_cli/download.py +0 -454
  47. anipy_cli/history.py +0 -83
  48. anipy_cli/mal.py +0 -645
  49. anipy_cli/misc.py +0 -227
  50. anipy_cli/player/__init__.py +0 -1
  51. anipy_cli/player/player.py +0 -33
  52. anipy_cli/player/players/__init__.py +0 -3
  53. anipy_cli/player/players/base.py +0 -106
  54. anipy_cli/player/players/mpv.py +0 -19
  55. anipy_cli/player/players/mpv_contrl.py +0 -37
  56. anipy_cli/player/players/syncplay.py +0 -19
  57. anipy_cli/player/players/vlc.py +0 -18
  58. anipy_cli/query.py +0 -92
  59. anipy_cli/run_anipy_cli.py +0 -14
  60. anipy_cli/seasonal.py +0 -106
  61. anipy_cli/url_handler.py +0 -442
  62. anipy_cli/version.py +0 -1
  63. anipy_cli-2.7.17.dist-info/LICENSE +0 -674
  64. anipy_cli-2.7.17.dist-info/METADATA +0 -159
  65. anipy_cli-2.7.17.dist-info/RECORD +0 -43
  66. anipy_cli-2.7.17.dist-info/entry_points.txt +0 -2
  67. anipy_cli-2.7.17.dist-info/top_level.txt +0 -1
anipy_cli/mal.py DELETED
@@ -1,645 +0,0 @@
1
- import base64
2
- import datetime
3
- import json
4
- import os
5
- import sys
6
- import time
7
- from copy import deepcopy
8
- from multiprocessing import Pool
9
-
10
- import requests
11
- from requests.adapters import HTTPAdapter, Retry
12
-
13
- from anipy_cli.url_handler import epHandler
14
- from anipy_cli.seasonal import Seasonal
15
- from anipy_cli.query import query
16
- from anipy_cli.colors import colors, cprint
17
- from anipy_cli.config import Config
18
- from anipy_cli.misc import read_json, error, Entry
19
-
20
-
21
- def _base64_decode(b64: str):
22
- return base64.b64decode(b64).decode("ascii")
23
-
24
-
25
- def _base64_encode(string: str):
26
- message_bytes = string.encode("ascii")
27
- return base64.b64encode(message_bytes).decode("ascii")
28
-
29
-
30
- class MAL:
31
- """
32
- MyAnimeList API client
33
- """
34
-
35
- def __init__(self, user=None, password=None):
36
- # API information taken from here: https://github.com/SuperMarcus/myanimelist-api-specification
37
- self.entry = Entry()
38
- self.local_mal_list_json = None
39
-
40
- self.anime_fields = [
41
- "id",
42
- "title",
43
- "main_picture",
44
- "alternative_titles",
45
- "my_list_status{tags,is_rewatching,num_episodes_watched,score,status,updated_at}",
46
- "start_season",
47
- ]
48
- self.access_token_expire_time = None
49
- self.access_token = None
50
- self.refresh_token = os.environ.get("mal_api_refresh_token")
51
- self.api_client_id = "6114d00ca681b7701d1e15fe11a4987e"
52
- self.api_baseurl = "https://api.myanimelist.net/v2/"
53
- self.mal_user = (
54
- Config().mal_user
55
- if Config().mal_user and Config().mal_user != ""
56
- else (user if user else False)
57
- )
58
- self.mal_password = (
59
- password
60
- if password
61
- else (
62
- Config().mal_password
63
- if Config().mal_password and Config().mal_password != ""
64
- else False
65
- )
66
- )
67
- self.anime_list = None
68
- self.gogo_baseurl = Config().gogoanime_url
69
- self.data = {
70
- "client_id": self.api_client_id,
71
- }
72
- self.shows_failed_automap = set()
73
-
74
- self.headers = {
75
- "Content-Type": "application/x-www-form-urlencoded",
76
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/86.0.4240.198 Safari/537.36",
77
- "X-MAL-Client-ID": self.api_client_id,
78
- }
79
- self.session = requests.Session()
80
- retry = Retry(connect=3, backoff_factor=0.5)
81
- adapter = HTTPAdapter(max_retries=retry)
82
- self.session.mount("http://", adapter)
83
- self.session.mount("https://", adapter)
84
- self.session.headers.update(self.headers)
85
- self.read_save_data()
86
- if self.mal_user:
87
- if not self.auth():
88
- error(
89
- "Could not authenticate with MyAnimeList. Please check your credentials..."
90
- )
91
- self.get_anime_list()
92
- if Config().auto_map_mal_to_gogo:
93
- self.auto_map_all_without_map()
94
- if Config().auto_sync_mal_to_seasonals:
95
- self.sync_mal_with_seasonal()
96
-
97
- def auto_map_all_without_map(self):
98
- with Pool(processes=None) as pool:
99
- mal_entries = self.local_mal_list_json["data"]
100
- # apply the search function to each search value in parallel
101
- results = pool.map(self.auto_map_gogo_mal, mal_entries, True)
102
-
103
-
104
- new_mal_entries = list()
105
- for result in results:
106
- if type(result) is dict:
107
- new_mal_entries.append(result["mal_entry"])
108
- if result["failed_to_map"] is True:
109
- self.shows_failed_automap.add(result["mal_entry"]["node"]["title"])
110
- else:
111
- self.shows_failed_automap.discard(
112
- result["mal_entry"]["node"]["title"]
113
- )
114
- self.local_mal_list_json["data"] = new_mal_entries
115
- self.write_save_data()
116
-
117
- return self.shows_failed_automap
118
-
119
- def get_all_without_gogo_map(self):
120
- shows_with_no_map = set()
121
- for mal_entry in self.local_mal_list_json["data"]:
122
- if "gogo_map" not in mal_entry or len(mal_entry["gogo_map"]) < 1:
123
- shows_with_no_map.add(mal_entry["node"]["title"])
124
- return shows_with_no_map
125
-
126
- def _make_request(
127
- self,
128
- url: str,
129
- method: str,
130
- data: dict = None,
131
- query_params: dict = None,
132
- body: dict = None,
133
- retry: int = 0,
134
- is_auth: bool = False,
135
- ):
136
- try:
137
- response = self.session.request(
138
- method, url, data=data, params=query_params, json=body
139
- )
140
- response.raise_for_status()
141
- return response.json()
142
-
143
- except requests.exceptions.RequestException as request_error:
144
- if retry == 0 and not is_auth:
145
- self.auth(retry=retry)
146
- return self._make_request(
147
- url,
148
- method,
149
- data=data,
150
- query_params=query_params,
151
- body=body,
152
- retry=1,
153
- )
154
-
155
- if "message" in request_error.response.json():
156
- error(
157
- "MyAnimeList Error: {}".format(
158
- request_error.response.json()["message"]
159
- )
160
- )
161
- if "hint" in request_error.response.json():
162
- print(
163
- "{}Hint:{} {}{}".format(
164
- colors.BLUE,
165
- colors.YELLOW,
166
- request_error.response.json()["hint"],
167
- colors.END,
168
- )
169
- )
170
- sys.exit(1)
171
- error("MyAnimeList - {}".format(request_error.response.json()))
172
- return error
173
-
174
- def auth(self, retry: int = 0):
175
- data = self.data
176
- time_now = datetime.datetime.now()
177
- if (
178
- self.access_token
179
- and self.access_token_expire_time > time_now
180
- and retry == 0
181
- ):
182
- return True
183
-
184
- elif self.mal_user:
185
- print("MAL: Auth with user and password")
186
- url = "https://api.myanimelist.net/v2/auth/token"
187
- data.update(
188
- {
189
- "grant_type": "password",
190
- "password": self.mal_password,
191
- "username": self.mal_user,
192
- }
193
- )
194
-
195
- else:
196
- url = "https://myanimelist.net/v1/oauth2/token"
197
- data.update({"client_id": self.api_client_id})
198
-
199
- response = self._make_request(url, "post", data, is_auth=True)
200
- if isinstance(response, dict):
201
- if response["access_token"]:
202
- self.access_token = response["access_token"]
203
- self.session.headers.update(
204
- {"Authorization": "Bearer " + self.access_token}
205
- )
206
- self.access_token_expire_time = time_now + datetime.timedelta(
207
- 0, int(response["expires_in"])
208
- )
209
- self.refresh_token = response["refresh_token"]
210
- os.environ["mal_api_refresh_token"] = self.refresh_token
211
- return True
212
-
213
- else:
214
- return None
215
-
216
- else:
217
- if retry == 0:
218
- return self.auth(retry=1)
219
-
220
- else:
221
- return None
222
-
223
- def get_anime_list(self, status_filter: str = None, automap: bool = False):
224
- if status_filter is not None:
225
- status_filter = status_filter.lower()
226
-
227
- allowed_filters = [
228
- "watching",
229
- "completed",
230
- "on_hold",
231
- "dropped",
232
- "plan_to_watch",
233
- ]
234
- parameters = {}
235
- if status_filter in allowed_filters:
236
- parameters = {"status": status_filter}
237
-
238
- url = f"{self.api_baseurl}users/@me/animelist"
239
- anime_list = self.get_all_anime_pages(
240
- url, limit=20, additional_params=parameters
241
- )
242
- if not isinstance(anime_list, dict):
243
- return False
244
- if not isinstance(anime_list, requests.exceptions.RequestException):
245
- self.anime_list = deepcopy(anime_list)
246
- self.write_mal_list()
247
- return anime_list["data"]
248
-
249
- else:
250
- return None
251
-
252
- def update_anime_list(self, anime_id: int, update_data: dict = None):
253
- url = f"{self.api_baseurl}anime/{anime_id}/my_list_status"
254
- allowed_update_data_keys = ["status", "num_watched_episodes", "tags"]
255
- data = {}
256
- for key, value in update_data.items():
257
- if key in allowed_update_data_keys:
258
- data[key] = value
259
- if data:
260
- self._make_request(url, "patch", data)
261
-
262
- def get_seasonal_anime(
263
- self, year: int, season: str, limit: int = 100, automap: bool = False
264
- ):
265
- season = season.lower()
266
- url = f"{self.api_baseurl}anime/season/{year}/{season}"
267
-
268
- anime_season_list = self.get_all_anime_pages(url, limit)["data"]
269
-
270
- return anime_season_list
271
-
272
- def get_anime(self, query: str, limit: int = 2, automap: bool = True):
273
- url = f"{self.api_baseurl}anime"
274
- params = {
275
- "q": query,
276
- "limit": 10,
277
- }
278
- anime_found = self._make_request(url, "get", query_params=params)
279
- if (
280
- isinstance(anime_found, requests.exceptions.RequestException)
281
- or not isinstance(anime_found, dict)
282
- or len(anime_found["data"]) < 1
283
- ):
284
- return []
285
-
286
- return anime_found["data"]
287
-
288
- def get_all_anime_pages(self, url, limit, additional_params: dict = None):
289
- params = {
290
- "fields": ",".join(self.anime_fields),
291
- "limit": limit,
292
- "offset": 0,
293
- }
294
- if additional_params:
295
- params.update(additional_params)
296
- next_page = True
297
- offset = 0
298
- anime_list = {"data": []}
299
-
300
- while next_page:
301
- response = self._make_request(url, "get", query_params=params)
302
- if not "data" in response or not isinstance(response["data"], list):
303
- continue
304
- if (
305
- response["paging"]
306
- and "next" in response["paging"]
307
- and response["paging"]["next"]
308
- ):
309
- offset += limit
310
-
311
- else:
312
- next_page = False
313
-
314
- anime_list["data"] += response["data"]
315
- params["offset"] = offset
316
-
317
- # sleep for 0.5 sec to limit rate
318
- time.sleep(0.5)
319
- return anime_list
320
-
321
- def add_show(self, show_name, category_url, picked_ep):
322
- search = self.get_anime(show_name, automap=True)
323
-
324
- if isinstance(search, requests.exceptions.RequestException):
325
- return None
326
-
327
- else:
328
- if search[0]["node"]["title"].lower() == show_name.lower().rstrip(
329
- " (dub)"
330
- ).strip("(japanese dub)"):
331
- print("Exact match")
332
- self.update_anime_list(
333
- search[0]["node"]["id"],
334
- {
335
- "status": "watching",
336
- "num_watched_episodes": picked_ep,
337
- "tags": "anipy-cli",
338
- },
339
- )
340
- show = [
341
- x
342
- for x in self.local_mal_list_json["data"]
343
- if x["node"]["title"] == search[0]["node"]["title"]
344
- ]
345
- try:
346
- new_item = self.make_gogo_map(category_url, show_name)
347
- if "gogo_map" in show[0]:
348
- self.update_gogo_map_list(show[0]["gogo_map"], new_item)
349
-
350
- else:
351
- new_map = {"gogo_map": []}
352
- self.update_gogo_map_list(new_map["gogo_map"], new_item)
353
- show[0].update(new_map)
354
- except IndexError:
355
- error("No show found for {}".format(search[0]["node"]["title"]))
356
-
357
- self.write_mal_list()
358
- else:
359
- return search
360
-
361
- def make_gogo_map(self, link_href, name):
362
- link = (
363
- link_href
364
- if len(link_href.split("/category/")[0]) > 3
365
- else Config().gogoanime_url + link_href
366
- )
367
- if "(dub)" in name.lower():
368
- key = "dub"
369
-
370
- else:
371
- key = "sub"
372
-
373
- return [{"name": name, "link": link, "type": key}]
374
-
375
- def update_gogo_map_list(self, gogo_map, new_entry):
376
- if len(gogo_map) > 0:
377
- for index, item in enumerate(gogo_map):
378
- if item["name"] == new_entry[0]["name"]:
379
- if item.__eq__(new_entry):
380
- return True
381
-
382
- else:
383
- gogo_map[index] = new_entry
384
- return True
385
- if isinstance(gogo_map, list):
386
- gogo_map.extend(new_entry)
387
-
388
- return True
389
-
390
- def read_save_data(self):
391
- try:
392
- self.local_mal_list_json = read_json(Config().mal_local_user_list_path)
393
-
394
- except json.decoder.JSONDecodeError:
395
- pass
396
-
397
- if (
398
- not isinstance(self.local_mal_list_json, dict)
399
- or "data" not in self.local_mal_list_json
400
- ):
401
- self.local_mal_list_json = {"data": []}
402
- self.get_anime_list(automap=True)
403
-
404
- def write_save_data(self):
405
- try:
406
- with Config().mal_local_user_list_path.open("w") as f:
407
- json.dump(self.local_mal_list_json, f, indent=4)
408
-
409
- except PermissionError:
410
- error("Unable to write to local MAL-list file due permissions.")
411
- sys.exit()
412
-
413
- def write_mal_list(self):
414
- for show_entry in self.anime_list["data"]:
415
- if "data" not in self.local_mal_list_json:
416
- self.local_mal_list_json = {"data": []}
417
- show = [
418
- x
419
- for x in self.local_mal_list_json["data"]
420
- if x["node"]["title"] == show_entry["node"]["title"]
421
- ]
422
- if len(show) > 0:
423
- update_dict_recursive(show[0]["node"], show_entry["node"])
424
-
425
- else:
426
- self.local_mal_list_json["data"].append(show_entry)
427
-
428
- self.write_save_data()
429
-
430
- def delete_mal_entry(self, anime_id):
431
- show = [
432
- x
433
- for x in self.local_mal_list_json["data"]
434
- if str(x["node"]["id"]) == str(anime_id)
435
- ]
436
- show[0] = None
437
- self.get_anime_list()
438
-
439
- def del_show(self, anime_id):
440
- url = f"{self.api_baseurl}anime/{anime_id}/my_list_status"
441
- self._make_request(url, "delete")
442
- self.delete_mal_entry(anime_id)
443
-
444
- pass
445
-
446
- def latest_eps(self, all_eps: bool = False):
447
- """
448
- returns a dict like so:
449
- {"name": {
450
- "ep_list": [[ep, ep-link], [], ...],
451
- "category_url": "https://"
452
- },
453
- "another anime": {
454
- ...
455
- },
456
- }
457
- """
458
-
459
- self.read_save_data()
460
- entries = [
461
- i
462
- for i in self.local_mal_list_json["data"]
463
- if i["node"]["my_list_status"]["status"] in Config().mal_status_categories
464
- ]
465
-
466
- latest_urls = {}
467
- for i in entries:
468
- if all_eps:
469
- start_ep = 0
470
- else:
471
- start_ep = i["node"]["my_list_status"]["num_episodes_watched"]
472
-
473
- if "gogo_map" not in i:
474
- error(
475
- 'MAL entry "{}" is missing gogo-map, please re-add to MAL...'.format(
476
- i["node"]["title"]
477
- )
478
- )
479
- continue
480
-
481
- for key, link in enumerate(i["gogo_map"]):
482
- self.entry.category_url = link["link"]
483
- ep_class = epHandler(self.entry)
484
- latest = ep_class.get_latest()
485
- eps_range = list(range(start_ep + 1, latest + 1))
486
- ep_urls = []
487
- for j in eps_range:
488
- self.entry.ep = j
489
- ep_class = epHandler(self.entry)
490
- entry = ep_class.gen_eplink()
491
- ep_urls.append([j, entry.ep_url])
492
-
493
- latest_urls.update(
494
- {
495
- link["name"]: {
496
- "ep_list": ep_urls,
497
- "category_url": self.entry.category_url,
498
- }
499
- }
500
- )
501
-
502
- return latest_urls
503
-
504
- def update_watched(self, gogo_show_name, ep):
505
- show = []
506
- for mal_entry in self.local_mal_list_json["data"]:
507
- if "gogo_map" in mal_entry.keys():
508
- for gogomap in mal_entry["gogo_map"]:
509
- if gogo_show_name in gogomap["name"]:
510
- show.append(mal_entry)
511
- break
512
- if len(show) > 0:
513
- anime_id = show[0]["node"]["id"]
514
- self.update_anime_list(anime_id, {"num_watched_episodes": ep})
515
-
516
- def sync_mal_with_seasonal(self):
517
- self.get_anime_list()
518
- seasonal = Seasonal()
519
- seasonal_list = seasonal.list_seasonals()
520
- for mal_with_gogo_map in self.local_mal_list_json["data"]:
521
- if (
522
- mal_with_gogo_map["node"]["my_list_status"]["status"]
523
- in Config().mal_status_categories
524
- and "gogo_map" in mal_with_gogo_map
525
- and len(mal_with_gogo_map["gogo_map"]) > 0
526
- ):
527
- for anime_entry in mal_with_gogo_map["gogo_map"]:
528
- print(
529
- "{}Syncing {}{}".format(
530
- colors.GREEN, anime_entry["name"], colors.END
531
- )
532
- )
533
- if anime_entry["name"] in [x[0] for x in seasonal_list]:
534
- seasonal.update_show(
535
- anime_entry["name"],
536
- anime_entry["link"],
537
- mal_with_gogo_map["node"]["my_list_status"][
538
- "num_episodes_watched"
539
- ],
540
- )
541
-
542
- else:
543
- seasonal.add_show(
544
- anime_entry["name"],
545
- anime_entry["link"],
546
- mal_with_gogo_map["node"]["my_list_status"][
547
- "num_episodes_watched"
548
- ],
549
- )
550
-
551
- def sync_seasonals_with_mal(self):
552
- seasonal = Seasonal()
553
- seasonal_list = seasonal.export_seasonals()
554
- for anime in seasonal_list:
555
- print(f"{colors.GREEN}Syncing {anime[0]} into MAL{colors.END} ...")
556
- self.add_show(anime[0], anime[1], anime[2])
557
- print(f"{colors.GREEN}Done.{colors.END}")
558
-
559
- def auto_map_gogo_mal(self, mal_entry, mp=False):
560
- # Look issue 122
561
- try:
562
- if "gogo_map" in mal_entry and len(mal_entry["gogo_map"]) > 0:
563
- return {"failed_to_map": False, "mal_entry": mal_entry}
564
- failed_to_map = True
565
- cprint(colors.GREEN, "Auto mapping: ", colors.BLUE, mal_entry["node"]["title"])
566
-
567
- search_values = [
568
- mal_entry["node"]["title"],
569
- mal_entry["node"]["alternative_titles"]["en"],
570
- ] + mal_entry["node"]["alternative_titles"]["synonyms"]
571
-
572
- found = {}
573
- for search in search_values:
574
- query_class = query(search, Entry)
575
- query_class.get_pages()
576
- found["search"] = query_class.get_links()
577
-
578
- if found["search"] == 0:
579
- self.shows_failed_automap.add(mal_entry["node"]["title"])
580
- continue
581
- if "gogo_map" not in mal_entry:
582
- mal_entry["gogo_map"] = []
583
- for i, anime in enumerate(found["search"][1]):
584
- if any(
585
- anime.lower().rstrip("(dub)").rstrip("(japanese dub)").strip(" ")
586
- in show.lower()
587
- for show in [search_values[0], search_values[1]]
588
- ):
589
- gogo_map = mal_entry["gogo_map"]
590
- current_map = self.make_gogo_map(found["search"][0][i], anime)
591
-
592
- self.update_gogo_map_list(gogo_map, current_map)
593
- failed_to_map = False
594
- if not mp:
595
- self.shows_failed_automap.discard(mal_entry["node"]["title"])
596
- self.update_anime_list(
597
- mal_entry["node"]["id"],
598
- {
599
- "num_watched_episodes": mal_entry["node"]["my_list_status"][
600
- "num_episodes_watched"
601
- ],
602
- "tags": "anipy-cli",
603
- },
604
- )
605
- else:
606
- if not mp:
607
- self.shows_failed_automap.add(mal_entry["node"]["title"])
608
- if mp:
609
- self.write_save_data()
610
- return {"failed_to_map": failed_to_map, "mal_entry": mal_entry}
611
- except json.JSONDecodeError:
612
- return {"failed_to_map": True, "mal_entry": mal_entry}
613
-
614
- def manual_map_gogo_mal(self, mal_anime_name: str, gogo: dict):
615
- mal_entry = [
616
- x
617
- for x in self.local_mal_list_json["data"]
618
- if x["node"]["title"] == mal_anime_name
619
- ]
620
- if len(mal_entry) > 0:
621
- current_mal_entry = mal_entry[0]
622
-
623
- else:
624
- return False
625
-
626
- if "gogo_map" not in current_mal_entry or not isinstance(
627
- current_mal_entry["gogo_map"], list
628
- ):
629
- current_mal_entry["gogo_map"] = []
630
-
631
- self.update_gogo_map_list(
632
- current_mal_entry["gogo_map"],
633
- self.make_gogo_map(gogo["link"], gogo["name"]),
634
- )
635
- if mal_anime_name in self.shows_failed_automap:
636
- self.shows_failed_automap.discard(mal_anime_name)
637
- return True
638
-
639
-
640
- def update_dict_recursive(dct, merge_dct):
641
- for k, v in merge_dct.items():
642
- if k in dct and isinstance(dct[k], dict) and isinstance(merge_dct[k], dict):
643
- update_dict_recursive(dct[k], merge_dct[k])
644
- else:
645
- dct[k] = merge_dct[k]