anipy-cli 2.7.30__py3-none-any.whl → 3.0.0__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.

Potentially problematic release.


This version of anipy-cli might be problematic. Click here for more details.

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