Skip to content

Commit

Permalink
adds kitsu and mal sync (MALSync#18)
Browse files Browse the repository at this point in the history
  • Loading branch information
SwagOtaku authored and DxCx committed Mar 2, 2019
1 parent 9ad238d commit 695238e
Show file tree
Hide file tree
Showing 11 changed files with 229 additions and 49 deletions.
2 changes: 1 addition & 1 deletion 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)
Expand Down
11 changes: 8 additions & 3 deletions default.py
Expand Up @@ -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)]
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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
Expand All @@ -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):
Expand Down
4 changes: 4 additions & 0 deletions resources/language/English/strings.xml
Expand Up @@ -14,4 +14,8 @@
<string id="30101">Processing %s</string>
<string id="30102">Please choose source: </string>
<string id="30103">Couldn't find eligible sources</string>

<string id="30200">Watchlist</string>
<string id="30201">Update Watchlist Episode Progress</string>
<string id="30202">Not Yet</string>
</strings>
83 changes: 68 additions & 15 deletions 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"]),
'',
Expand All @@ -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):
Expand All @@ -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))
Expand All @@ -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",
Expand All @@ -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
Expand All @@ -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",
Expand Down
80 changes: 64 additions & 16 deletions resources/lib/WatchlistFlavor/MyAnimeList.py
Expand Up @@ -6,14 +6,15 @@
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"

def login(self):
s = requests.session()

crsf_res = s.get('https://myanimelist.net/').text
crsf_res = s.get(self._URL).text
crsf = (re.compile("<meta name='csrf_token' content='(.+?)'>").findall(crsf_res))[0]

payload = {
Expand All @@ -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')
Expand All @@ -37,49 +38,54 @@ 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": '',
}

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"]),
Expand All @@ -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,
Expand Down
25 changes: 25 additions & 0 deletions 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
Expand Down Expand Up @@ -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
Expand All @@ -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,
Expand All @@ -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)

0 comments on commit 695238e

Please sign in to comment.