diff options
Diffstat (limited to 'plugin.video.akibapass/resources/lib')
-rw-r--r-- | plugin.video.akibapass/resources/lib/__init__.py | 1 | ||||
-rw-r--r-- | plugin.video.akibapass/resources/lib/akibapass.py | 114 | ||||
-rw-r--r-- | plugin.video.akibapass/resources/lib/cmdargs.py | 51 | ||||
-rw-r--r-- | plugin.video.akibapass/resources/lib/login.py | 95 | ||||
-rw-r--r-- | plugin.video.akibapass/resources/lib/netapi.py | 294 | ||||
-rw-r--r-- | plugin.video.akibapass/resources/lib/view.py | 110 |
6 files changed, 665 insertions, 0 deletions
diff --git a/plugin.video.akibapass/resources/lib/__init__.py b/plugin.video.akibapass/resources/lib/__init__.py new file mode 100644 index 0000000..b93054b --- /dev/null +++ b/plugin.video.akibapass/resources/lib/__init__.py @@ -0,0 +1 @@ +# Dummy file to make this directory a package. diff --git a/plugin.video.akibapass/resources/lib/akibapass.py b/plugin.video.akibapass/resources/lib/akibapass.py new file mode 100644 index 0000000..5a6e7ac --- /dev/null +++ b/plugin.video.akibapass/resources/lib/akibapass.py @@ -0,0 +1,114 @@ +# -*- coding: utf-8 -*- +# Akibapass - Watch videos from the german anime platform Akibapass.de on Kodi. +# Copyright (C) 2016 - 2017 MrKrabat +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys + +import xbmc +import xbmcgui +import xbmcplugin + +import cmdargs +import login +import netapi +import view + + +def main(): + """Main function for the addon + """ + args = cmdargs.parse() + + # check if account is set + username = args._addon.getSetting("akiba_username") + password = args._addon.getSetting("akiba_password") + + if not (username and password): + # open addon settings + args._addon.openSettings() + return False + else: + # login + success = login.login(username, password, args) + if success: + # list menue + xbmcplugin.setContent(int(sys.argv[1]), "tvshows") + check_mode(args) + else: + # login failed + xbmc.log("[PLUGIN] %s: Login failed" % args._addonname, xbmc.LOGERROR) + xbmcgui.Dialog().ok(args._addonname, args._addon.getLocalizedString(30040)) + return False + + +def check_mode(args): + """Run mode-specific functions + """ + if hasattr(args, "mode"): + mode = args.mode + elif hasattr(args, "id"): + # call from other plugin + mode = "videoplay" + args.url = "/de/v2/catalogue/episode/" + args.id + elif hasattr(args, "url"): + # call from other plugin + mode = "videoplay" + args.url = args.url[24:] + else: + mode = None + + if not mode: + showMainMenue(args) + elif mode == "catalog": + netapi.showCatalog(args) + elif mode == "search": + netapi.searchAnime(args) + elif mode == "downloads": + netapi.myDownloads(args) + elif mode == "collection": + netapi.myCollection(args) + elif mode == "list_season": + netapi.listSeason(args) + elif mode == "list_episodes": + netapi.listEpisodes(args) + elif mode == "videoplay": + netapi.startplayback(args) + elif mode == "trailer": + item = xbmcgui.ListItem(getattr(args, "title", "Title not provided"), path=args.url) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, item) + else: + # unkown mode + xbmc.log("[PLUGIN] %s: Failed in check_mode '%s'" % (args._addonname, str(mode)), xbmc.LOGERROR) + xbmcgui.Dialog().notification(args._addonname, args._addon.getLocalizedString(30041), xbmcgui.NOTIFICATION_ERROR) + showMainMenue(args) + + +def showMainMenue(args): + """Show main menu + """ + view.add_item(args, + {"title": args._addon.getLocalizedString(30020), + "mode": "catalog"}) + view.add_item(args, + {"title": args._addon.getLocalizedString(30021), + "mode": "search"}) + view.add_item(args, + {"title": args._addon.getLocalizedString(30022), + "mode": "downloads"}) + view.add_item(args, + {"title": args._addon.getLocalizedString(30023), + "mode": "collection"}) + view.endofdirectory() diff --git a/plugin.video.akibapass/resources/lib/cmdargs.py b/plugin.video.akibapass/resources/lib/cmdargs.py new file mode 100644 index 0000000..61cc975 --- /dev/null +++ b/plugin.video.akibapass/resources/lib/cmdargs.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +# Akibapass - Watch videos from the german anime platform Akibapass.de on Kodi. +# Copyright (C) 2016 - 2017 MrKrabat +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import urllib +import urlparse + + +def parse(): + """Decode arguments + """ + if (sys.argv[2]): + return Args(urlparse.parse_qs(sys.argv[2][1:])) + else: + return Args({}) + + +class Args(object): + """Arguments class + Hold all arguments passed to the script and also persistent user data and + reference to the addon. It is intended to hold all data necessary for the + script. + """ + def __init__(self, kwargs): + """Initialize arguments object + Hold also references to the addon which can't be kept at module level. + """ + self._addon = sys.modules["__main__"]._addon + self._addonname = sys.modules["__main__"]._plugin + self._addonid = sys.modules["__main__"]._plugId + self._cj = None + + for key, value in kwargs.iteritems(): + if value: + kwargs[key] = urllib.unquote_plus(value[0]) + + self.__dict__.update(kwargs) diff --git a/plugin.video.akibapass/resources/lib/login.py b/plugin.video.akibapass/resources/lib/login.py new file mode 100644 index 0000000..d8df6ba --- /dev/null +++ b/plugin.video.akibapass/resources/lib/login.py @@ -0,0 +1,95 @@ +# -*- coding: utf-8 -*- +# Akibapass - Watch videos from the german anime platform Akibapass.de on Kodi. +# Copyright (C) 2016 - 2017 MrKrabat +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os +import cookielib +import urllib +import urllib2 + +import xbmc + + +def login(username, password, args): + """Login and session handler + """ + login_url = "https://www.akibapass.de/de/v2/account/login?ReturnUrl=%2Fde%2Fv2" + + # create cookie path + cookiepath = os.path.join( + xbmc.translatePath(args._addon.getAddonInfo("profile")).decode("utf-8"), + "cookies.lwp") + + # create cookiejar + cj = cookielib.LWPCookieJar() + args._cj = cj + + # lets urllib2 handle cookies + opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cj)) + opener.addheaders = [("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.113 Safari/537.36")] + opener.addheaders = [("Accept-Charset", "utf-8")] + urllib2.install_opener(opener) + + # check if session exists + try: + cj.load(cookiepath, ignore_discard=True) + + # check if session is valid + response = urllib2.urlopen("https://www.akibapass.de/de/v2/catalogue") + html = response.read() + + if "Meine persönlichen Informationen bearbeiten" in html: + # session is valid + return True + + except IOError: + # cookie file does not exist + pass + + # build POST data + post_data = urllib.urlencode({"username": username, + "password": password, + "remember": "1"}) + + # POST to login page + response = urllib2.urlopen(login_url, post_data) + + # check for login string + html = response.read() + + if "Meine persönlichen Informationen bearbeiten" in html: + # save session to disk + cj.save(cookiepath, ignore_discard=True) + return True + else: + return False + + +def getCookie(args): + """Returns all cookies as string and urlencoded + """ + # create cookie path + cookiepath = os.path.join( + xbmc.translatePath(args._addon.getAddonInfo("profile")).decode("utf-8"), + "cookies.lwp") + # save session to disk + args._cj.save(cookiepath, ignore_discard=True) + + ret = "" + for cookie in args._cj: + ret += urllib.urlencode({cookie.name : cookie.value}) + ";" + + return "|User-Agent=Mozilla%2F5.0%20%28Windows%20NT%2010.0%3B%20Win64%3B%20x64%29%20AppleWebKit%2F537.36%20%28KHTML%2C%20like%20Gecko%29%20Chrome%2F60.0.3112.113%20Safari%2F537.36&Cookie=" + ret[:-1] diff --git a/plugin.video.akibapass/resources/lib/netapi.py b/plugin.video.akibapass/resources/lib/netapi.py new file mode 100644 index 0000000..16abaa6 --- /dev/null +++ b/plugin.video.akibapass/resources/lib/netapi.py @@ -0,0 +1,294 @@ +# -*- coding: utf-8 -*- +# Akibapass - Watch videos from the german anime platform Akibapass.de on Kodi. +# Copyright (C) 2016 - 2017 MrKrabat +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import re +import sys +import urllib +import urllib2 +from bs4 import BeautifulSoup + +import xbmc +import xbmcgui +import xbmcplugin + +import login +import view + + +def showCatalog(args): + """Show all animes + """ + response = urllib2.urlopen("https://www.akibapass.de/de/v2/catalogue") + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + ul = soup.find("ul", {"class": "catalog_list"}) + + for li in ul.find_all("li"): + plot = li.find("p", {"class": "tooltip_text"}) + stars = li.find("div", {"class": "stars"}) + star = stars.find_all("span", {"class": "-no"}) + thumb = li.img["src"].replace(" ", "%20") + if thumb[:4] != "http": + thumb = "https:" + thumb + + view.add_item(args, + {"url": li.a["href"], + "title": li.find("div", {"class": "slider_item_description"}).span.strong.string.strip().encode("utf-8"), + "tvshowtitle": li.find("div", {"class": "slider_item_description"}).span.strong.string.strip().encode("utf-8"), + "mode": "list_season", + "thumb": thumb, + "fanart": thumb, + "rating": str(10 - len(star) * 2), + "plot": plot.contents[3].string.strip().encode("utf-8"), + "year": li.time.string.strip().encode("utf-8")}, + isFolder=True, mediatype="video") + + view.endofdirectory() + + +def searchAnime(args): + """Search for animes + """ + d = xbmcgui.Dialog().input(args._addon.getLocalizedString(30021), type=xbmcgui.INPUT_ALPHANUM) + if not d: + return + + post_data = urllib.urlencode({"search": d}) + response = urllib2.urlopen("https://www.akibapass.de/de/v2/catalogue/search", post_data) + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + ul = soup.find("ul", {"class": "catalog_list"}) + if not ul: + view.endofdirectory() + return + + for li in ul.find_all("li"): + plot = li.find("p", {"class": "tooltip_text"}) + stars = li.find("div", {"class": "stars"}) + star = stars.find_all("span", {"class": "-no"}) + thumb = li.img["src"].replace(" ", "%20") + if thumb[:4] != "http": + thumb = "https:" + thumb + + view.add_item(args, + {"url": li.a["href"], + "title": li.find("div", {"class": "slider_item_description"}).span.strong.string.strip().encode("utf-8"), + "mode": "list_season", + "thumb": thumb, + "fanart": thumb, + "rating": str(10 - len(star) * 2), + "plot": plot.contents[3].string.strip().encode("utf-8"), + "year": li.time.string.strip().encode("utf-8")}, + isFolder=True, mediatype="video") + + view.endofdirectory() + + +def myDownloads(args): + """View download able animes + May not every episode is download able. + """ + response = urllib2.urlopen("https://www.akibapass.de/de/v2/mydownloads") + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + container = soup.find("div", {"class": "big-item-list"}) + if not container: + view.endofdirectory() + return + + for div in container.find_all("div", {"class": "big-item-list_item"}): + thumb = div.img["src"].replace(" ", "%20") + if thumb[:4] != "http": + thumb = "https:" + thumb + + view.add_item(args, + {"url": div.a["href"].replace("mydownloads/detail", "catalogue/show"), + "title": div.find("h3", {"class": "big-item_title"}).string.strip().encode("utf-8"), + "mode": "list_season", + "thumb": thumb, + "fanart": thumb}, + isFolder=True, mediatype="video") + + view.endofdirectory() + + +def myCollection(args): + """View collection + """ + response = urllib2.urlopen("https://www.akibapass.de/de/v2/collection") + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + container = soup.find("div", {"class": "big-item-list"}) + if not container: + view.endofdirectory() + return + + for div in container.find_all("div", {"class": "big-item-list_item"}): + thumb = div.img["src"].replace(" ", "%20") + if thumb[:4] != "http": + thumb = "https:" + thumb + + view.add_item(args, + {"url": div.a["href"].replace("collection/detail", "catalogue/show"), + "title": div.find("h3", {"class": "big-item_title"}).string.strip().encode("utf-8"), + "mode": "list_season", + "thumb": thumb, + "fanart": thumb}, + isFolder=True, mediatype="video") + + view.endofdirectory() + + +def listSeason(args): + """Show all seasons/arcs of an anime + """ + response = urllib2.urlopen("https://www.akibapass.de" + args.url) + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + + date = soup.find_all("span", {"class": "border-list_text"})[0].find_all("span") + year = date[2].string.strip().encode("utf-8") + date = year + "-" + date[1].string.strip().encode("utf-8") + "-" + date[0].string.strip().encode("utf-8") + originaltitle = soup.find_all("span", {"class": "border-list_text"})[1].string.strip().encode("utf-8") + studio = soup.find_all("span", {"class": "border-list_text"})[2].string.strip().encode("utf-8") + plot = soup.find("div", {"class": "serie_description"}).string.strip().encode("utf-8") + credits = soup.find("div", {"class": "serie_description_more"}).p.string.strip().encode("utf-8") + try: + trailer = soup.find("span", {"class": "js-video-open"})["data-video"] + trailer = "plugin://plugin.video.youtube/play/?video_id=" + trailer + view.add_item(args, + {"url": trailer, + "mode": "trailer", + "thumb": args.thumb.replace(" ", "%20"), + "fanart": args.fanart.replace(" ", "%20"), + "title": args._addon.getLocalizedString(30024)}, + isFolder=False, mediatype="video") + except TypeError: + trailer = "" + + for section in soup.find_all("h2", {"class": "slider-section_title"}): + if not section.span: + continue + title = section.get_text()[6:].strip() + + view.add_item(args, + {"url": args.url, + "title": title.encode("utf-8"), + "mode": "list_episodes", + "thumb": args.thumb.replace(" ", "%20"), + "fanart": args.fanart.replace(" ", "%20"), + "season": title.encode("utf-8"), + "plot": plot, + "plotoutline": getattr(args, "plot", ""), + "studio": studio, + "year": year, + "premiered": date, + "trailer": trailer, + "originaltitle": originaltitle, + "credits": credits}, + isFolder=True, mediatype="video") + + view.endofdirectory() + + +def listEpisodes(args): + """Show all episodes of an season/arc + """ + response = urllib2.urlopen("https://www.akibapass.de" + args.url) + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + + for season in soup.findAll(text=args.title): + parent = season.find_parent("li") + if not parent: + continue + + thumb = parent.img["src"].replace(" ", "%20") + if thumb[:4] != "http": + thumb = "https:" + thumb + + view.add_item(args, + {"url": parent.a["href"], + "title": parent.img["alt"].encode("utf-8"), + "mode": "videoplay", + "thumb": args.thumb.replace(" ", "%20"), + "fanart": args.fanart.replace(" ", "%20")}, + isFolder=False, mediatype="video") + + view.endofdirectory() + + +def startplayback(args): + """Plays a video + """ + response = urllib2.urlopen("https://www.akibapass.de" + args.url) + html = response.read() + + soup = BeautifulSoup(html, "html.parser") + + # check if not premium + if "Dieses Video ist nur für Nutzer eines Abos verfügbar" in html: + xbmc.log("[PLUGIN] %s: You need to own this video or be a premium member '%s'" % (args._addonname, args.url), xbmc.LOGERROR) + xbmcgui.Dialog().ok(args._addonname, args._addon.getLocalizedString(30043)) + return + + # check if we have to reactivate video + if "reactivate" in html: + # reactivate video + a = soup.find("div", {"id": "jwplayer-container"}).a["href"] + response = urllib2.urlopen("https://www.akibapass.de" + a) + html = response.read() + + # reload page + response = urllib2.urlopen("https://www.akibapass.de" + args.url) + html = response.read() + soup = BeautifulSoup(html, "html.parser") + + # check if successfull + if "reactivate" in html: + xbmc.log("[PLUGIN] %s: Reactivation failed '%s'" % (args._addonname, args.url), xbmc.LOGERROR) + xbmcgui.Dialog().ok(args._addonname, args._addon.getLocalizedString(30042)) + return + + # using stream with hls+aes + if "Klicke hier, um den Flash-Player zu benutzen" in html: + # get stream file + regex = r"file: \"(.*?)\"," + matches = re.search(regex, html).group(1) + + if matches: + # manifest url + url = "https://www.akibapass.de" + matches + + # play stream + item = xbmcgui.ListItem(getattr(args, "title", "Title not provided"), path=url + login.getCookie(args)) + item.setMimeType("application/vnd.apple.mpegurl") + item.setContentLookup(False) + xbmcplugin.setResolvedUrl(int(sys.argv[1]), True, item) + else: + xbmc.log("[PLUGIN] %s: Failed to play stream" % args._addonname, xbmc.LOGERROR) + xbmcgui.Dialog().ok(args._addonname, args._addon.getLocalizedString(30044)) + + else: + xbmc.log("[PLUGIN] %s: You need to own this video or be a premium member '%s'" % (args._addonname, args.url), xbmc.LOGERROR) + xbmcgui.Dialog().ok(args._addonname, args._addon.getLocalizedString(30043)) diff --git a/plugin.video.akibapass/resources/lib/view.py b/plugin.video.akibapass/resources/lib/view.py new file mode 100644 index 0000000..f72b985 --- /dev/null +++ b/plugin.video.akibapass/resources/lib/view.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# Akibapass - Watch videos from the german anime platform Akibapass.de on Kodi. +# Copyright (C) 2016 - 2017 MrKrabat +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import sys +import urllib + +import xbmc +import xbmcgui +import xbmcplugin + + +# keys allowed in setInfo +types = ["count", "size", "date", "genre", "year", "episode", "season", "top250", "tracknumber", + "rating", "userrating", "watched", "playcount", "overlay", "cast", "castandrole", "director", + "mpaa", "plot", "plotoutline", "title", "originaltitle", "sorttitle", "duration", "studio", + "tagline", "writer", "tvshowtitle", "premiered", "status", "code", "aired", "credits", "lastplayed", + "album", "artist", "votes", "trailer", "dateadded", "mediatype"] + +def endofdirectory(): + # sort methods are required in library mode + xbmcplugin.addSortMethod(int(sys.argv[1]), xbmcplugin.SORT_METHOD_NONE) + + # let xbmc know the script is done adding items to the list + xbmcplugin.endOfDirectory(handle = int(sys.argv[1])) + + +def add_item(args, info, isFolder=True, total_items=0, mediatype="video"): + """Add item to directory listing. + """ + + # create list item + li = xbmcgui.ListItem(label = info["title"]) + + # get infoLabels + infoLabels = make_infolabel(args, info) + infoLabels["genre"] = "Anime" + + # get url + u = build_url(args, info) + + if isFolder: + # directory + li.setInfo(mediatype, infoLabels) + else: + # playable video + infoLabels["mediatype"] = "video" + li.setInfo(mediatype, infoLabels) + li.setProperty("IsPlayable", "true") + + # set media image + li.setArt({"thumb": info.get("thumb", "DefaultFolder.png"), + "poster": info.get("thumb", "DefaultFolder.png"), + "banner": info.get("thumb", "DefaultFolder.png"), + "fanart": info.get("fanart", xbmc.translatePath(args._addon.getAddonInfo("fanart"))), + "icon": info.get("thumb", "DefaultFolder.png")}) + + # add item to list + xbmcplugin.addDirectoryItem(handle = int(sys.argv[1]), + url = u, + listitem = li, + isFolder = isFolder, + totalItems = total_items) + + +def build_url(args, info): + """Create url + """ + s = "" + # step 1 copy new information from info + for key, value in info.iteritems(): + if value: + s = s + "&" + key + "=" + urllib.quote_plus(value) + + # step 2 copy old information from args, but don't append twice + for key, value in args.__dict__.iteritems(): + if value and key in types and not "&" + str(key) + "=" in s: + s = s + "&" + key + "=" + urllib.quote_plus(value) + + return sys.argv[0] + "?" + s[1:] + + +def make_infolabel(args, info): + """Generate infoLabels from existing dict + """ + infoLabels = {} + # step 1 copy new information from info + for key, value in info.iteritems(): + if value and key in types: + infoLabels[key] = value + + # step 2 copy old information from args, but don't overwrite + for key, value in args.__dict__.iteritems(): + if value and key in types and key not in infoLabels: + infoLabels[key] = value + + return infoLabels |