From 695238e1317fb992d4dbd6d52896f468bb9d9688 Mon Sep 17 00:00:00 2001 From: SwagOtaku Date: Sat, 2 Mar 2019 10:31:29 -0800 Subject: [PATCH] adds kitsu and mal sync (#18) --- changelog.txt | 2 +- default.py | 11 ++- resources/language/English/strings.xml | 4 + resources/lib/WatchlistFlavor/Kitsu.py | 83 +++++++++++++++---- resources/lib/WatchlistFlavor/MyAnimeList.py | 80 ++++++++++++++---- .../WatchlistFlavor/WatchlistFlavorBase.py | 25 ++++++ .../lib/WatchlistFlavor/WonderfulSubs.py | 18 ++-- resources/lib/WatchlistFlavor/__init__.py | 4 + resources/lib/WatchlistIntegration.py | 7 ++ resources/lib/WonderfulSubsBrowser.py | 10 ++- resources/lib/ui/control.py | 34 +++++++- 11 files changed, 229 insertions(+), 49 deletions(-) diff --git a/changelog.txt b/changelog.txt index 2a621f8bbb..b12203a2df 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,5 @@ [B]0.0.5[/B] -- Change "A-B Listings" to "A-Z Listings" (Thanks @SwagOtaku) +- Change "A-B Listings" to "A-Z Listings" (Thanks @RedNinjaX) - Fixes batch (NA And Watchlists) (Thanks @SwagOtaku) [B]0.0.4[/B] - Fix parsing of partial keys (By @SwagOtaku) diff --git a/default.py b/default.py index a0ae21fcdd..72c7f5515b 100644 --- a/default.py +++ b/default.py @@ -3,7 +3,7 @@ from resources.lib.ui.SourcesList import SourcesList from resources.lib.ui.router import on_param, route, router_process from resources.lib.WonderfulSubsBrowser import WonderfulSubsBrowser -from resources.lib.WatchlistIntegration import add_watchlist +from resources.lib.WatchlistIntegration import add_watchlist, watchlist_update import urlparse AB_LIST = ["none"] + [chr(i) for i in range(ord("a"), ord("z")+1)] @@ -55,6 +55,10 @@ def sortResultsByRes(fetched_urls): utils.parse_resolution_of_source(x[0]), reverse=True) +#Will be called when player is stopped in the middle of the episode +def on_stopped(): + return control.yesno_dialog(control.lang(30200), control.lang(30201), control.lang(30202)) + @route('settings') def SETTINGS(payload, params): return control.settingsMenu(); @@ -159,7 +163,8 @@ def SEARCH_PAGES(payload, params): @route('play/*') def PLAY(payload, params): - anime_url, episode = payload.rsplit("/", 1) + anime_url, kitsu_id = payload.rsplit("/", 1) + anime_url, episode = anime_url.rsplit("/", 1) anime_url, season = anime_url.rsplit("/", 1) anime_url, flavor = anime_url.rsplit("/", 1) is_dubbed = True if "dub" == flavor else False @@ -175,7 +180,7 @@ def PLAY(payload, params): }) __set_last_watched(anime_url, is_dubbed, name, image) - return control.play_source(s.get_video_link()) + control.play_source(s.get_video_link(), watchlist_update(episode, kitsu_id), on_stopped) @route('') def LIST_MENU(payload, params): diff --git a/resources/language/English/strings.xml b/resources/language/English/strings.xml index 3f7735e7ae..1d6bb7d250 100644 --- a/resources/language/English/strings.xml +++ b/resources/language/English/strings.xml @@ -14,4 +14,8 @@ Processing %s Please choose source: Couldn't find eligible sources + + Watchlist + Update Watchlist Episode Progress + Not Yet diff --git a/resources/lib/WatchlistFlavor/Kitsu.py b/resources/lib/WatchlistFlavor/Kitsu.py index 7b2be81c80..837f9e0cc8 100644 --- a/resources/lib/WatchlistFlavor/Kitsu.py +++ b/resources/lib/WatchlistFlavor/Kitsu.py @@ -1,28 +1,26 @@ import itertools import json -import requests from WatchlistFlavorBase import WatchlistFlavorBase class KitsuWLF(WatchlistFlavorBase): + _URL = "https://kitsu.io/api" _TITLE = "Kitsu" _NAME = "kitsu" _IMAGE = "https://canny.io/images/13895523beb5ed9287424264980221d4.png" def login(self): - r = requests.post('https://kitsu.io/api/oauth/token', params={ + params = { "grant_type": "password", "username": self._username, "password": self._password - }) + } + resp = self._post_request(self._to_url("oauth/token"), params=params) - if r.status_code != 200: + if resp.status_code != 200: return - data = json.loads(r.text) - user_res = requests.get('https://kitsu.io/api/edge/users?filter[self]=true', - headers=self.__header(data['access_token'])) - - data2 = json.loads(user_res.text)["data"][0] + data = json.loads(resp.text) + data2 = json.loads(self._send_request(self._to_url("edge/users?filter[self]=true"), headers=self.__header(data['access_token'])))["data"][0] return self._format_login_data((data2["attributes"]["name"]), '', @@ -41,7 +39,7 @@ def watchlist(self): _id, token = self._login_token.rsplit("/", 1) headers = self.__header(token) params = {"filter[user_id]": _id} - url = "https://kitsu.io/api/edge/library-entries" + url = self._to_url("edge/library-entries") return self._process_watchlist_status_view(url, params, headers, "watchlist/%d", page=1) def _base_watchlist_status_view(self, res): @@ -55,7 +53,7 @@ def _base_watchlist_status_view(self, res): return self._parse_view(base) def _process_watchlist_status_view(self, url, params, headers, base_plugin_url, page): - result = requests.get(url, params=params, headers=headers).text + result = self._send_request(url, headers=headers, params=params) results = json.loads(result)["meta"]["statusCounts"] all_results = map(self._base_watchlist_status_view, results) all_results = list(itertools.chain(*all_results)) @@ -67,7 +65,7 @@ def get_watchlist_status(self, status): _id, token = self._login_token.rsplit("/", 1) headers = self.__header(token) - url = "https://kitsu.io/api/edge/library-entries" + url = self._to_url("edge/library-entries") params = { "fields[anime]": "slug,posterImage,canonicalTitle,titles,synopsis,subtype,startDate,status,averageRating,popularityRank,ratingRank,episodeCount", @@ -84,9 +82,9 @@ def get_watchlist_status(self, status): return self._process_watchlist_view(url, params, headers, "watchlist/%d", page=1) def _process_watchlist_view(self, url, params, headers, base_plugin_url, page): - result = requests.get(url, params=params, headers=headers).text - results = json.loads(result)["included"][1:] - results2 = json.loads(result)["data"] + result = json.loads(self._send_request(url, headers=headers, params=params)) + results = result["included"][1:] + results2 = result["data"] all_results = map(self._base_watchlist_view, results, results2) all_results = list(itertools.chain(*all_results)) return all_results @@ -103,6 +101,61 @@ def _base_watchlist_view(self, res, res2): return self._parse_view(base) + def watchlist_update(self, episode, kitsu_id): + uid, token = self._login_token.rsplit("/", 1) + url = self._to_url("edge/library-entries") + params = { + "filter[user_id]": uid, + "filter[anime_id]": kitsu_id + } + scrobble = self._send_request(url, headers=self.__header(token), params=params) + item_dict = json.loads(scrobble) + if len(item_dict['data']) == 0: + return lambda: self.__post_params(url, episode, kitsu_id, token, uid) + + animeid = item_dict['data'][0]['id'] + return lambda: self.__patch_params(url, animeid, episode, token) + + def __post_params(self, url, episode, kitsu_id, token, uid): + params = { + "data": { + "type": "libraryEntries", + "attributes": { + 'status': 'current', + 'progress': int(episode) + }, + "relationships":{ + "user":{ + "data":{ + "id": int(uid), + "type": "users" + } + }, + "anime":{ + "data":{ + "id": int(kitsu_id), + "type": "anime" + } + } + } + } + } + + self._post_request(url, headers=self.__header(token), json=params) + + def __patch_params(self, url, animeid, episode, token): + params = { + 'data': { + 'id': int(animeid), + 'type': 'libraryEntries', + 'attributes': { + 'progress': int(episode) + } + } + } + + self._patch_request("%s/%s" %(url, animeid), headers=self.__header(token), json=params) + def __get_sort(self): sort_types = { "Date Updated": "-progressed_at", diff --git a/resources/lib/WatchlistFlavor/MyAnimeList.py b/resources/lib/WatchlistFlavor/MyAnimeList.py index 5452010776..c1451ec990 100644 --- a/resources/lib/WatchlistFlavor/MyAnimeList.py +++ b/resources/lib/WatchlistFlavor/MyAnimeList.py @@ -6,6 +6,7 @@ from WatchlistFlavorBase import WatchlistFlavorBase class MyAnimeListWLF(WatchlistFlavorBase): + _URL = "https://myanimelist.net" _TITLE = "MyAnimeList" _NAME = "mal" _IMAGE = "https://myanimelist.cdn-dena.com/images/mal-logo-xsmall@2x.png?v=160803001" @@ -13,7 +14,7 @@ class MyAnimeListWLF(WatchlistFlavorBase): def login(self): s = requests.session() - crsf_res = s.get('https://myanimelist.net/').text + crsf_res = s.get(self._URL).text crsf = (re.compile("").findall(crsf_res))[0] payload = { @@ -25,7 +26,7 @@ def login(self): "csrf_token": crsf } - url = "https://myanimelist.net/login.php?from=%2F" + url = self._to_url("login.php?from=%2F") s.get(url) result = s.post(url, data=payload) soup = bs.BeautifulSoup(result.text, 'html.parser') @@ -37,13 +38,13 @@ def login(self): return self._format_login_data(self._username, '', ('%s/%s' % (s.cookies['MALHLOGSESSID'], s.cookies['MALSESSIONID']))) def watchlist(self): - url = "https://myanimelist.net/animelist/%s" % (self._login_name) + url = self._to_url("animelist/%s" % (self._login_name)) return self._process_watchlist_view(url, '', "watchlist/%d", page=1) def _base_watchlist_view(self, res): base = { - "name": res.text, - "url": 'watchlist_status_type/' + (res['href']).rsplit('=', 1)[-1], + "name": res[0], + "url": 'watchlist_status_type/' + str(res[1]), "image": '', "plot": '', } @@ -51,35 +52,40 @@ def _base_watchlist_view(self, res): return self._parse_view(base) def _process_watchlist_view(self, url, params, base_plugin_url, page): - result = requests.get(url) - soup = bs.BeautifulSoup(result.text, 'html.parser') - results = [x for x in soup.find_all('a', {'class': 'status-button'})] - all_results = map(self._base_watchlist_view, results) + all_results = map(self._base_watchlist_view, self._mal_statuses()) all_results = list(itertools.chain(*all_results)) return all_results + def _mal_statuses(self): + statuses = [ + ("All Anime", 7), + ("Currently Watching", 1), + ("Completed", 2), + ("On Hold", 3), + ("Dropped", 4), + ("Plan to Watch", 6), + ] + + return statuses + def get_watchlist_status(self, status): params = { "status": status, "order": self.__get_sort(), } - url = "https://myanimelist.net/animelist/%s" % (self._login_name) + url = self._to_url("animelist/%s/load.json" % (self._login_name)) return self._process_status_view(url, params, "watchlist/%d", page=1) def _process_status_view(self, url, params, base_plugin_url, page): - result = requests.get(url, params=params).text - soup = bs.BeautifulSoup(result, 'html.parser') - table = soup.find('table', attrs={'class':'list-table'}) - table_body = table.attrs['data-items'] - results = json.loads(table_body) + results = json.loads(self._send_request(url, params=params)) all_results = map(self._base_watchlist_status_view, results) all_results = list(itertools.chain(*all_results)) return all_results def _base_watchlist_status_view(self, res): IMAGE_ID_RE = re.search('anime/(.*).jpg', res["anime_image_path"]) - image_id = IMAGE_ID_RE.group(1) + image_id = IMAGE_ID_RE.group(1) if IMAGE_ID_RE else "" base = { "name": '%s - %d/%d' % (res["anime_title"], res["num_watched_episodes"], res["anime_num_episodes"]), @@ -90,6 +96,48 @@ def _base_watchlist_status_view(self, res): return self._parse_view(base) + def __cookies(self): + logsess_id, sess_id = self._login_token.rsplit("/", 1) + + cookies = { + 'MALHLOGSESSID': logsess_id, + 'MALSESSIONID': sess_id, + 'is_logged_in': '1' + } + + return cookies + + def _kitsu_to_mal_id(self, kitsu_id): + arm_resp = requests.get("https://arm.now.sh/api/v1/search?type=kitsu&id=" + kitsu_id) + if arm_resp.status_code != 200: + raise Exception("AnimeID not found") + + mal_id = json.loads(arm_resp.text)["services"]["mal"] + return mal_id + + def watchlist_update(self, episode, kitsu_id): + mal_id = self._kitsu_to_mal_id(kitsu_id) + result = self._send_request(self._to_url("anime/%s" % (mal_id)), cookies=self.__cookies()) + soup = bs.BeautifulSoup(result, 'html.parser') + csrf = soup.find("meta", {"name":"csrf_token"})["content"] + match = soup.find('h2', {'class' : 'mt8'}) + if match: + url = self._to_url("ownlist/anime/edit.json") + else: + url = self._to_url("ownlist/anime/add.json") + + return lambda: self.__update_library(url, episode, mal_id, csrf) + + def __update_library(self, url, episode, mal_id, csrf): + payload = { + "anime_id": int(mal_id), + "status": 1, + "num_watched_episodes": int(episode), + "csrf_token": csrf + } + + self._post_request(url, cookies=self.__cookies(), json=payload) + def __get_sort(self): sort_types = { "Anime Title": 1, diff --git a/resources/lib/WatchlistFlavor/WatchlistFlavorBase.py b/resources/lib/WatchlistFlavor/WatchlistFlavorBase.py index d18d03a4f6..04e38980ff 100644 --- a/resources/lib/WatchlistFlavor/WatchlistFlavorBase.py +++ b/resources/lib/WatchlistFlavor/WatchlistFlavorBase.py @@ -1,6 +1,8 @@ +import requests from ..ui import utils class WatchlistFlavorBase(object): + _URL = None _TITLE = None _NAME = None _IMAGE = None @@ -38,6 +40,13 @@ def title(self): return self._TITLE + @property + def url(self): + if self._URL is None: + raise Exception("Missing Url") + + return self._URL + @property def login_name(self): return self._login_name @@ -51,6 +60,9 @@ def watchlist(self): def get_watchlist_status(self, status): raise NotImplementedError("get_watchlist_status should be implemented by subclass") + def watchlist_update(self, episode, kitsu_id): + raise NotImplementedError("watchlist_update should be implemented by subclass") + def _format_login_data(self, name, image, token): login_data = { "name": name, @@ -69,3 +81,16 @@ def _parse_view(self, base): base["plot"]) ] + def _to_url(self, url=''): + if url.startswith("/"): + url = url[1:] + return "%s/%s" % (self._URL, url) + + def _send_request(self, url, headers=None, cookies=None, data=None, params=None): + return requests.get(url, headers=headers, cookies=cookies, data=data, params=params).text + + def _post_request(self, url, headers=None, cookies=None, params=None, json=None): + return requests.post(url, headers=headers, cookies=cookies, params=params, json=json) + + def _patch_request(self, url, headers=None, cookies=None, params=None, json=None): + return requests.patch(url, headers=headers, cookies=cookies, params=params, json=json) diff --git a/resources/lib/WatchlistFlavor/WonderfulSubs.py b/resources/lib/WatchlistFlavor/WonderfulSubs.py index 180a504f7b..10f8d84097 100644 --- a/resources/lib/WatchlistFlavor/WonderfulSubs.py +++ b/resources/lib/WatchlistFlavor/WonderfulSubs.py @@ -1,10 +1,10 @@ import itertools import json -import requests from ..ui import utils from WatchlistFlavorBase import WatchlistFlavorBase class WonderfulSubsWLF(WatchlistFlavorBase): + _URL = "https://www.wonderfulsubs.com" _TITLE = "WonderfulSubs" _NAME = "wonderfulsubs" @@ -13,10 +13,8 @@ def image(self): return self._login_image def login(self): - r = requests.post('https://www.wonderfulsubs.com/api/users/login', - json={"username": self._username, "password": self._password}) - - data = json.loads(r.text) + url = self._to_url("api/users/login") + data = json.loads(self._post_request(url, json={"username": self._username, "password": self._password}).text) if data['success'] is not True: return @@ -26,7 +24,7 @@ def login(self): ('%s/%s' % (data['data']['_id'], data['token']))) def watchlist(self): - url = 'https://www.wonderfulsubs.com/api/watchlist/list?_id=%s' % (self._login_token.split("/")[0]) + url = self._to_url("api/watchlist/list?_id=%s" % (self._login_token.split("/")[0])) return self._process_watchlist_view(url, "watchlist/%d", page=1) def __header(self): @@ -63,9 +61,11 @@ def _base_watchlist_view(self, res): return result - def _process_watchlist_view(self, url, base_plugin_url, page): - r = requests.get(url, headers=self.__header()) - results = json.loads(r.text)['data']['watch_list'] + def _process_watchlist_view(self, url, base_plugin_url, page): + results = json.loads(self._send_request(url, headers=self.__header()))['data']['watch_list'] all_results = map(self._base_watchlist_view, results) all_results = list(itertools.chain(*all_results)) return all_results + + def watchlist_update(self, episode, kitsu_id): + return False diff --git a/resources/lib/WatchlistFlavor/__init__.py b/resources/lib/WatchlistFlavor/__init__.py index bb3fd11190..b555a279df 100644 --- a/resources/lib/WatchlistFlavor/__init__.py +++ b/resources/lib/WatchlistFlavor/__init__.py @@ -37,6 +37,10 @@ def watchlist_request(): def watchlist_status_request(status): return WatchlistFlavor.get_active_flavor().get_watchlist_status(status) + @staticmethod + def watchlist_update_request(episode, kitsu_id): + return WatchlistFlavor.get_active_flavor().watchlist_update(episode, kitsu_id) + @staticmethod def login_request(flavor): if not WatchlistFlavor.__is_flavor_valid(flavor): diff --git a/resources/lib/WatchlistIntegration.py b/resources/lib/WatchlistIntegration.py index a5973b0f77..cf1a611be9 100644 --- a/resources/lib/WatchlistIntegration.py +++ b/resources/lib/WatchlistIntegration.py @@ -23,6 +23,13 @@ def WATCHLIST_STATUS_TYPE(payload, params): def WATCHLIST_QUERY(payload, params): return control.draw_items(WonderfulSubsBrowser().search_site(payload.rsplit("/")[0])) +def watchlist_update(episode, kitsu_id): + flavor = WatchlistFlavor.get_active_flavor() + if not flavor: + return + + return WatchlistFlavor.watchlist_update_request(episode, kitsu_id) + def add_watchlist(items): flavor = WatchlistFlavor.get_active_flavor() if not flavor: diff --git a/resources/lib/WonderfulSubsBrowser.py b/resources/lib/WonderfulSubsBrowser.py index f1da9eea21..6e09ecd970 100644 --- a/resources/lib/WonderfulSubsBrowser.py +++ b/resources/lib/WonderfulSubsBrowser.py @@ -92,7 +92,7 @@ def _process_anime_view(self, url, data, base_plugin_url, page): all_results += self._handle_paging(total_results, base_plugin_url, page) return all_results - def _format_episode(self, sname, anime_url, is_dubbed, ses_idx, einfo): + def _format_episode(self, sname, anime_url, is_dubbed, ses_idx, einfo, kitsu_id): desc = None if not einfo.has_key("description") else einfo["description"] image = None if einfo.has_key("thumbnail") and len(einfo["thumbnail"]): @@ -119,10 +119,11 @@ def _format_episode(self, sname, anime_url, is_dubbed, ses_idx, einfo): base.update({ "name": einfo["title"] if "Episode" in einfo["title"] else "Ep. %s (%s)" %(einfo["episode_number"], einfo["title"]), "id": str(einfo["episode_number"]), - "url": "play/%s/%s/%d/%s" % (anime_url, + "url": "play/%s/%s/%d/%s/%s" % (anime_url, "dub" if is_dubbed else "sub", ses_idx, - str(einfo["episode_number"])), + str(einfo["episode_number"]), + kitsu_id), "sources": sources, "image": image, "plot": desc, @@ -217,7 +218,8 @@ def _get_anime_info(self, anime_url, is_dubbed): ep_info = self._format_episode("Server %d" % sindex, anime_url, is_dubbed, - ses_obj["id"], einfo) + ses_obj["id"], einfo, + season_col.get("kitsu_id", None)) if not eps.has_key(ep_info["id"]): eps[ep_info["id"]] = ep_info continue diff --git a/resources/lib/ui/control.py b/resources/lib/ui/control.py index 8bdb0a188a..436307c34a 100644 --- a/resources/lib/ui/control.py +++ b/resources/lib/ui/control.py @@ -37,6 +37,34 @@ def __call__(self, func): self.__MIME_HOOKS[self._type] = func return func +class watchlistPlayer(xbmc.Player): + + def __init__(self, *args, **kwargs): + self._on_playback_done = kwargs['action'] + self._on_stopped = kwargs['dialog'] + super(watchlistPlayer, self).__init__() + + def onPlayBackStarted(self): + pass + + def onPlayBackStopped(self): + if not self._on_stopped(): + return + + self._on_playback_done() + + def onPlayBackEnded(self): + self._on_playback_done() + +def handle_player(on_playback_done, on_stopped): + if not on_playback_done: + return + + get_player = watchlistPlayer(action=on_playback_done, dialog=on_stopped) + xbmc.sleep(500) # Wait until playback starts + while xbmc.Player().isPlaying(): + xbmc.sleep(500) + def setContent(contentType): xbmcplugin.setContent(HANDLE, contentType) @@ -79,6 +107,9 @@ def keyboard(text): def ok_dialog(title, text): return xbmcgui.Dialog().ok(title, text) +def yesno_dialog(title, text, nolabel=None, yeslabel=None): + return xbmcgui.Dialog().yesno(title, text, nolabel=nolabel, yeslabel=yeslabel) + def xbmc_add_player_item(name, url, iconimage='', description='', draw_cm=None): ok=True u=addon_url(url) @@ -121,7 +152,7 @@ def _prefetch_play_link(link): "headers": linkInfo.headers, } -def play_source(link): +def play_source(link, on_episode_done=None, on_stopped=None): linkInfo = _prefetch_play_link(link) if not linkInfo: xbmcplugin.setResolvedUrl(HANDLE, False, xbmcgui.ListItem()) @@ -134,6 +165,7 @@ def play_source(link): # Run any mimetype hook item = hook_mimetype.trigger(linkInfo['headers']['Content-Type'], item) xbmcplugin.setResolvedUrl(HANDLE, True, item) + handle_player(on_episode_done, on_stopped) def draw_items(video_data, draw_cm=None): for vid in video_data: