From 4715a137f9fbf326fdf88eb4dbc3cddcbcdf8eeb Mon Sep 17 00:00:00 2001 From: "Jakob L. Kreuze" Date: Sun, 14 Jul 2024 13:47:31 -0400 Subject: Lift Newgrounds API interactions into separate library --- README.md | 17 +- addon.py | 885 ++++++++++++++++++-------------------------------------------- addon.xml | 7 +- 3 files changed, 261 insertions(+), 648 deletions(-) diff --git a/README.md b/README.md index 9f903b7..b2e82a0 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,8 @@ Video plugin for animations posted to Newgrounds. ## Features -- (Partial) support for the "Movies" section - - Navigating by Featured/Latest/Popular - - Navigating by category +- Navigating by Featured/Latest/Popular +- Navigating by category ## Versions Supported @@ -22,16 +21,12 @@ Please send patches to me [via email](https://jakob.space/pages/about.html). ## Roadmap -- [x] Complete support for the "Movies" section - - [x] Extraction of additional video metadata - - [x] Search - - [x] Support for collections - - [x] Support for series +- [x] Extraction of additional video metadata +- [x] Search +- [x] Support for collections +- [x] Support for series - [x] Authentication - [x] Support for "Your Feed" -- [ ] Support for the "Audio" section -- [ ] Support for [Newgrounds Radio](https://www.newgrounds.com/radio) -- [ ] Support for the "Art" section ## License diff --git a/addon.py b/addon.py index f8400d3..10d3b1d 100644 --- a/addon.py +++ b/addon.py @@ -17,16 +17,11 @@ Video plugin for animations posted to Newgrounds. """ -import concurrent.futures -from datetime import datetime -import dateutil -import json import os import sys from urllib.parse import urlencode, parse_qsl -from bs4 import BeautifulSoup -import requests +from newgrounds import Grouping, Newgrounds, MOVIE_CATEGORIES import xbmc import xbmcgui import xbmcplugin @@ -49,610 +44,7 @@ def get_url(**kwargs): :return: plugin call URL :rtype: str """ - return "{}?{}".format(URL, urlencode(kwargs)) - - -def get_series(official=True, sort_by="date", offset=1): - """ - Return a list of series on Newgrounds - - :param official: whether to exclude non-official series - :param sort_by: one of "date", "views" - :param offset: pagination offset - :return: list of video list entries - :rtype: list - """ - html = requests.get( - "https://www.newgrounds.com/series", - params={ - "filter": "all" if not official else None, - "order": sort_by, - "page": offset, - }, - cookies=get_session() - ).text - soup = BeautifulSoup(html, "html.parser") - visual_links = [ - json.loads(li["data-visual-link"]) - for li in soup.find_all("li", "visual-link-container") - ] - return fetch_visual_links(visual_links) - - -def get_collections(official=True, sort_by="date", offset=1): - """ - Return a list of playlists on Newgrounds - - :param official: whether to exclude non-official collections - :param sort_by: one of "date", "views" - :param offset: pagination offset - :return: list of video list entries - :rtype: list - """ - html = requests.get( - "https://www.newgrounds.com/collections", - params={ - "filter": "all" if not official else None, - "order": sort_by, - "page": offset, - }, - cookies=get_session() - ).text - soup = BeautifulSoup(html, "html.parser") - visual_links = [ - json.loads(li["data-visual-link"]) - for li in soup.find_all("li", "visual-link-container") - ] - return fetch_visual_links(visual_links) - - -def get_feed_videos(): - """ - Return a list of video URLs from the "Your Feed" page - - :return: list of video URLs - :rtype: list - """ - html = requests.get( - "https://www.newgrounds.com/social/feeds/show/favorite-artists-movies", - cookies=get_session() - ).text - soup = BeautifulSoup(html, "html.parser") - return [tag["href"] for tag in soup.find_all("a", "item-portalsubmission")] - - -def get_series_videos(url): - """ - Return videos in a series on Newgrounds - - :param url: URL of series page to scrape - :return: list of video list entries - :rtype: list - """ - html = requests.get(url, cookies=get_session()).text - soup = BeautifulSoup(html, "html.parser") - visual_links = [ - json.loads(li["data-visual-link"]) - for li in soup.find_all("li", "visual-link-container") - ] - return fetch_visual_links(visual_links) - - -def fetch_visual_links(visual_links): - """ - Fetch a list of video playlist entries. - - :param kwargs: "argument=value" pairs - :return: parsed out video metadata - :rtype: list - """ - - r = requests.post( - "https://www.newgrounds.com/visual-links-fetch", - params={ - "X-Requested-With": "XMLHttpRequest", - }, - data={ - "ids": json.dumps(visual_links).replace(" ", ""), - "component_params[include_author]": "1", - "include_all_suitabilities": "0", - "isAjaxRequest": "1", - }, - cookies=get_session() - ) - - result = r.json() - entries = [] - if isinstance(result["partials"], dict): - for partial in result["partials"].values(): - if isinstance(partial, dict): - for partial in partial.values(): - soup = BeautifulSoup(partial, "html.parser") - title = soup.find("h4").string - thumbnail = soup.find("img")["src"] - author = soup.find("strong").string - url = soup.find("a")["href"] - entries.append( - { - "title": title, - "thumbnail": thumbnail, - "author": author, - "url": url, - } - ) - return entries - - -def list_top_level(): - """ - Create the list of entrypoints in the Kodi interface. - """ - xbmcplugin.setPluginCategory(HANDLE, "Newgrounds") - xbmcplugin.setContent(HANDLE, "movies") - - genres = ["Featured", "Latest", "Popular"] - - # Add a search dialog - list_item = xbmcgui.ListItem(label="Search") - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle("Search") - url = get_url(action="prompt_search") - is_folder = False - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - - # Add a dialog for listing the series running on Newgrounds - list_item = xbmcgui.ListItem(label="Series") - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle("Series") - url = get_url(action="series") - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - - # Add a dialog for listing playlists - list_item = xbmcgui.ListItem(label="Collections") - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle("Collections") - url = get_url(action="collections") - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - - # Add a dialog for displaying the user's feed - if authentication_specified(): - list_item = xbmcgui.ListItem(label="Your Feed") - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle("Your Feed") - url = get_url(action="listing_feed") - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - - for name in genres: - list_item = xbmcgui.ListItem(label=name) - - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle(name) - info_tag.setGenres([name]) - - url = get_url(action="listing", genre=name) - is_folder = True - - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) - xbmcplugin.endOfDirectory(HANDLE) - - -def list_categories(genre="Featured"): - """ - Create the list of movie categories in the Kodi interface. - """ - xbmcplugin.setPluginCategory(HANDLE, "Newgrounds") - xbmcplugin.setContent(HANDLE, "movies") - - categories = { - "All": 0, - "Action": 45, - "Comedy - Original": 60, - "Comedy - Parody": 61, - "Drama": 47, - "Experimental": 49, - "Informative": 48, - "Music Video": 50, - "Other": 51, - "Spam": 55, - } - - for name in categories.keys(): - list_item = xbmcgui.ListItem(label=name) - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle(name) - info_tag.setGenres([name]) - url = get_url(action="listing", genre=genre, category=categories[name]) - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) - xbmcplugin.endOfDirectory(HANDLE) - - -def get_video_url(url): - """ - Return the location of the stream for a movie. - - :param url: URL of Newgrounds movie page - :return: stream URL - :rtype: str - """ - html = requests.get(url, cookies=get_session()).text - soup = BeautifulSoup(html, "html.parser") - video_id = soup.find("meta", property="og:url")["content"].strip( - "https://www.newgrounds.com/portal/view/" - ) - response = requests.get( - "https://www.newgrounds.com/portal/video/{}".format(video_id), - headers={"X-Requested-With": "XMLHttpRequest"}, - cookies=get_session() - ).json() - preferences = ["1080p", "720p", "360p"] - for preference in preferences: - if preference in response["sources"]: - source = next( - filter( - lambda x: x["type"] == "video/mp4", response["sources"][preference] - ) - ) - if source is not None: - return source["src"] - - -def get_addon_video_info(url): - """ - Scrape metadata from `url`. - - :param url: URL of Newgrounds movie page - :return: dictionary of video metadata - :rtype: dict - """ - html = requests.get(url, cookies=get_session()).text - soup = BeautifulSoup(html, "html.parser") - - title = soup.find("div", id="embed_header").h2.string - thumbnail = soup.find("meta", property="og:image") - if thumbnail is not None: - thumbnail = thumbnail["content"] - - metadata = { - "title": title, - "thumbnail": thumbnail, - "url": url, - "views": 0, - "faves": 0, - "votes": 0, - "score": 0.0, - "upload_date": datetime.fromtimestamp(0), - "genre": "Unknown", - "tags": [], - "description": "", - "authors": [], - "rating": "e", - } - - sidestats_popularity = soup.find_all("dl", "sidestats")[0].find_all("dd") - if len(sidestats_popularity) == 4: - metadata["views"] = int(sidestats_popularity[0].string.replace(",", "")) - faves = sidestats_popularity[1].a - if faves is not None: - metadata["faves"] = int(faves.string.replace(",", "")) - metadata["votes"] = int(sidestats_popularity[2].string.replace(",", "")) - score = sidestats_popularity[3].span - if score is not None: - metadata["score"] = float(score.string) - - sidestats_date_and_genre = soup.find_all("dl", "sidestats")[1].find_all("dd") - if len(sidestats_date_and_genre) == 3: - metadata["upload_date"] = dateutil.parser.parse( - sidestats_date_and_genre[0].string - + " " - + sidestats_date_and_genre[1].string - ) - genre = sidestats_date_and_genre[2].a - if genre is not None: - metadata["genre"] = genre.string - - tags = soup.find("dd", "tags") - if tags is not None: - metadata["tags"] = [tag.string for tag in tags.find_all("a")] - - author_comments = soup.find("div", id="author_comments") - metadata["description"] = " ".join([s for s in author_comments.strings]).strip() - - authors = soup.find("ul", "authorlinks") - metadata["authors"] = [ - author.string for author in authors.find_all("a") if author.get("class") is None - ] - - if len(soup.find_all("h2", "rated-a")) != 0: - rating = "a" - elif len(soup.find_all("h2", "rated-m")) != 0: - rating = "m" - elif len(soup.find_all("h2", "rated-t")) != 0: - rating = "t" - else: - rating = "e" - - return metadata - - -def get_frontpage_video_urls( - grouping="Featured", interval="today", sort_by="date", category=0, offset=0 -): - """ - Return a list of movies from the Newgrounds frontpage. - - :param grouping: one of "Featured", "Latest", or "Popular" - :param interval: one of "today", "yesterday", "week", "month", "year", "all" - :param sort_by: one of "date", score", "views" - :param category: numeric identifier for video category - :param offset: pagination offset - :return: list of video URLs - :rtype: list - """ - URLS = { - "Featured": "https://www.newgrounds.com/movies/featured", - "Latest": "https://www.newgrounds.com/movies/browse", - "Popular": "https://www.newgrounds.com/movies/popular", - } - - assert interval in ["today", "yesterday", "week", "month", "year", "all"] - assert sort_by in ["date", "score", "views"] - assert isinstance(category, int) - assert isinstance(offset, int) - - url = URLS[grouping] - response = requests.get( - url, - params={ - "interval": interval, - "sort": sort_by, - "genre": category, - "isAjaxRequest": True, - "offset": offset, - "inner": 1, - }, - headers={ - "X-Requested-With": "XMLHttpRequest", - }, - cookies=get_session() - ).json() - - soup = BeautifulSoup(response["content"], "html.parser") - return [tag["href"] for tag in soup.find_all("a", "inline-card-portalsubmission")] - - -def search(term, sort_by="relevance", offset=1): - """ - Return a list of movies matching a given search term. - - :param term: Term to search for - :param sort_by: one of "relevance", "date-asc", "date-desc", "score-asc", "score-desc", "views-asc", "views-desc" - :param offset: pagination offset - :return: list of video URLs - :rtype: list - """ - assert isinstance(term, str) - assert sort_by in [ - "relevance", - "date-asc", - "date-desc", - "score-asc", - "score-desc", - "views-asc", - "views-desc", - ] - assert isinstance(offset, int) - - response = requests.get( - "https://www.newgrounds.com/search/conduct/movies", - params={ - "suitabilities": "etm", - "sort": sort_by, - "terms": term, - "page": offset, - "inner": 1, - }, - headers={ - "X-Requested-With": "XMLHttpRequest", - }, - cookies=get_session(), - ).json() - - soup = BeautifulSoup(response["content"], "html.parser") - return [tag["href"] for tag in soup.find_all("a", "item-portalsubmission")] - - -def list_videos(video_urls, title=None, next_page=None): - """ - Create the list of playable videos in the Kodi interface. - - :param genre_index: the index of genre in the list of movie genres - :type genre: str - :type offset: int - """ - videos = [] - with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor: - # Start the load operations and mark each future with its URL - future_to_url = { - executor.submit(get_addon_video_info, url): url for url in video_urls - } - for future in concurrent.futures.as_completed(future_to_url): - url = future_to_url[future] - videos.append(future.result()) - xbmcplugin.setPluginCategory(HANDLE, title) - xbmcplugin.setContent(HANDLE, "movies") - - for video in videos: - list_item = xbmcgui.ListItem(label=video["title"]) - if video["thumbnail"] is not None: - list_item.setArt({"thumb": video["thumbnail"]}) - - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("movie") - info_tag.setTitle(video["title"]) - info_tag.setYear(video["upload_date"].year) - info_tag.setRating(video["score"], video["votes"]) - info_tag.setPlaycount(video["views"]) - info_tag.setPlot(video["description"]) - info_tag.setGenres([video["genre"]]) - info_tag.setDirectors(video["authors"]) - info_tag.setPremiered(video["upload_date"].strftime("%Y-%m-%d")) - info_tag.setTags(video["tags"]) - - if video["rating"] == "a": - info_tag.setMpaa("NC-17") - elif video["rating"] == "m": - info_tag.setMpaa("R") - elif video["rating"] == "t": - info_tag.setMpaa("PG-13") - else: - info_tag.setMpaa("PG") - - list_item.setProperty("IsPlayable", "true") - - url = get_url(action="play", video=video["url"]) - is_folder = False - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - - # Add a "continue" folder. - if next_page is not None and len(video_urls) > 0: - list_item = xbmcgui.ListItem(label="Next Page") - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle("Next Page") - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, next_page, list_item, is_folder) - - xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) - xbmcplugin.endOfDirectory(HANDLE) - - -def list_cards(cards, title="Cards", next_page=None): - """ - Create the list of folders in the Kodi interface. - - :param cards: card summaries from `fetch_visual_links` - """ - videos = [] - xbmcplugin.setPluginCategory(HANDLE, title) - xbmcplugin.setContent(HANDLE, "movies") - - for card in cards: - list_item = xbmcgui.ListItem(label=card["title"]) - if card["thumbnail"] is not None: - list_item.setArt({"thumb": card["thumbnail"]}) - - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("movie") - info_tag.setTitle(card["title"]) - info_tag.setDirectors([card["author"]]) - - url = get_url(action="listing_series", url=card["url"]) - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) - - # Add a "continue" folder. - if next_page is not None and len(cards) > 0: - list_item = xbmcgui.ListItem(label="Next Page") - info_tag = list_item.getVideoInfoTag() - info_tag.setMediaType("video") - info_tag.setTitle("Next Page") - is_folder = True - xbmcplugin.addDirectoryItem(HANDLE, next_page, list_item, is_folder) - - xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) - xbmcplugin.endOfDirectory(HANDLE) - - -def play_video(path): - play_item = xbmcgui.ListItem(path=get_video_url(path)) - play_item.setProperty("IsPlayable", "true") - xbmcplugin.setResolvedUrl(HANDLE, True, play_item) - - -def router(paramstring): - """ - Router function that calls other functions - depending on the provided paramstring - - :param paramstring: URL encoded plugin paramstring - :type paramstring: str - """ - params = dict(parse_qsl(paramstring)) - if not params: - list_top_level() - elif params["action"] == "prompt_search": - dialog = xbmcgui.Dialog() - term = dialog.input(heading="Search") - url = get_url(action="search", term=term) - xbmc.executebuiltin("Container.Update(%s)" % url) - elif params["action"] == "listing" and "category" in params: - offset = int(params.get("offset", 0)) - video_urls = get_frontpage_video_urls( - grouping=params["genre"], - interval="all", - category=params["category"], - offset=offset, - ) - next_page = get_url( - action="listing", - genre=params["genre"], - category=params["category"], - offset=offset + 20, - ) - list_videos(video_urls, params["genre"], next_page) - elif params["action"] == "listing": - list_categories(params["genre"]) - elif params["action"] == "series": - offset = int(params.get("offset", 1)) - cards = get_series(offset=offset) - next_page = get_url(action="series", offset=offset + 1) - list_cards(cards, "Series", next_page) - elif params["action"] == "collections": - offset = int(params.get("offset", 1)) - cards = get_collections(offset=offset) - next_page = get_url(action="collections", offset=offset + 1) - list_cards(cards, "Collections", next_page) - elif params["action"] == "listing_series": - cards = get_series_videos(params["url"]) - video_urls = [card["url"] for card in cards] - list_videos(video_urls) - elif params["action"] == "listing_feed": - video_urls = get_feed_videos() - list_videos(video_urls) - elif params["action"] == "search": - term = params["term"] - offset = int(params.get("offset", 1)) - video_urls = search(term, offset=offset) - next_page = get_url(action="search", term=term, offset=offset + 1) - list_videos(video_urls, "Search Results: {}".format(term), next_page) - elif params["action"] == "play": - play_video(params["video"]) - else: - raise ValueError(f"Invalid paramstring: {paramstring}!") - - -SESSION_COOKIE_NAME = "vmkIdu5l8m" - - -def test_logged_in(session): - return ( - requests.get( - "https://www.newgrounds.com/account", cookies={SESSION_COOKIE_NAME: session} - ).status_code - != 401 - ) + return f"{URL}?{urlencode(kwargs)}" def authentication_specified(): @@ -666,7 +58,7 @@ def get_session(): addon = Addon() session = addon.getSetting("newgrounds.session") - if session is not None: # and test_logged_in(session): + if session is not None: # and test_logged_in(session): return {SESSION_COOKIE_NAME: session} username = addon.getSetting("newgrounds.username") @@ -681,30 +73,257 @@ def get_session(): return {SESSION_COOKIE_NAME: session} -def open_new_session(username, password): - html = requests.get("https://www.newgrounds.com/login").text - soup = BeautifulSoup(html, "html.parser") +class NewgroundsPlugin(object): + def __init__(self): + self.ng = Newgrounds() + + def invoke(paramstring): + ng = NewgroundsPlugin() + params = dict(parse_qsl(paramstring)) + if "action" in params: + method = getattr(ng, params["action"]) + if method is None: + raise ValueError(f"Invalid paramstring: {paramstring}!") + else: + method = ng.default + method(**params) + + def default(self, **kwargs): + self.list_front_page() + + def prompt_search(self, **kwargs): + dialog = xbmcgui.Dialog() + term = dialog.input(heading="Search") + if term is not None: + url = get_url(action="search", term=term) + xbmc.executebuiltin("Container.Update(%s)" % url) + + def search(self, **kwargs): + assert "term" in kwargs + term = kwargs["term"] + offset = int(kwargs.get("offset", 1)) + video_urls = self.ng.search(term, offset=offset) + next_page = get_url(action="search", term=term, offset=offset + 1) + self._list_videos(video_urls, "Search Results: {}".format(term), next_page) + + def list_front_page(self, **kwargs): + """ + Create the list of entrypoints in the Kodi interface. + """ + xbmcplugin.setPluginCategory(HANDLE, "Newgrounds") + xbmcplugin.setContent(HANDLE, "movies") + + groupings = { + "Featured": Grouping.FEATURED, + "Latest": Grouping.LATEST, + "Popular": Grouping.POPULAR, + } + + # Add a search dialog + list_item = xbmcgui.ListItem(label="Search") + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("video") + info_tag.setTitle("Search") + url = get_url(action="prompt_search") + is_folder = False + xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) + + # Add a dialog for displaying the user's feed + if authentication_specified(): + list_item = xbmcgui.ListItem(label="Your Feed") + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("video") + info_tag.setTitle("Your Feed") + url = get_url(action="list_feed") + is_folder = True + xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) + + for name, enum in groupings.items(): + list_item = xbmcgui.ListItem(label=name) + + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("video") + info_tag.setTitle(name) + info_tag.setGenres([name]) + + url = get_url(action="list_categories", grouping=enum.value) + is_folder = True + + xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) + + xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.endOfDirectory(HANDLE) + + def list_categories(self, **kwargs): + """ + Create the list of movie categories in the Kodi interface. + """ + grouping = kwargs.get("grouping", Grouping.FEATURED.value) + xbmcplugin.setPluginCategory(HANDLE, "Categories") + xbmcplugin.setContent(HANDLE, "movies") + for name in MOVIE_CATEGORIES.keys(): + list_item = xbmcgui.ListItem(label=name) + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("video") + info_tag.setTitle(name) + info_tag.setGenres([name]) + url = get_url(action="list_videos", grouping=grouping, category=name) + is_folder = True + xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) + xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.endOfDirectory(HANDLE) + + def list_videos(self, **kwargs): + """ + Create the list of playable videos in the Kodi interface. + """ + grouping = int(kwargs.get("grouping", 0)) + offset = int(kwargs.get("offset", 0)) + category = kwargs.get("category", "All") + videos = self.ng.front_page( + grouping=int(grouping), + interval="all", + category=category, + offset=offset, + ) + next_page = get_url( + action="list_videos", + grouping=grouping, + category=category, + offset=offset + 20, + ) + self._list_videos(videos, f"{category} - {grouping}", next_page) + + # TODO: Support offsets. + def list_feed(self, **kwargs): + # offset = int(kwargs.get("offset", 1)) + # next_page = get_url(action="list_series", offset=offset + 1) + addon = Addon() + username = addon.getSetting("newgrounds.username") + password = addon.getSetting("newgrounds.password") + self.ng.login(username, password) + videos = self.ng.feed() + self._list_videos(videos, f"Feed - {username}") + + def list_series(self, **kwargs): + offset = int(kwargs.get("offset", 1)) + cards = self.ng.series(offset=offset) + next_page = get_url(action="list_series", offset=offset + 1) + self._list_cards( + title="Series", + cards=cards, + next_page=next_page, + ) + + def list_collections(self, **kwargs): + offset = int(kwargs.get("offset", 1)) + cards = self.ng.collections(offset=offset) + next_page = get_url(action="list_collections", offset=offset + 1) + self._list_cards( + title="Collections", + cards=cards, + next_page=next_page, + ) - form = soup.find("form", method="post") - endpoint = form["action"] - auth = form.find("input")["value"] + # TODO: Support offsets. + def list_playlist(self, **kwargs): + assert "url" in kwargs + cards = self.ng.playlist_entries(kwargs["url"]) + # next_page = get_url(action="list_playlist", offset=offset + 1) + self._list_cards( + title="Series", + cards=cards, + # next_page=next_page, + ) - r = requests.post( - "https://www.newgrounds.com" + endpoint, - data={ - "auth": auth, - "username": username, - "password": password, - "code": "", - "codehint": "------", - }, - cookies={ - "passport-auth": auth, - }, - ) - session = r.cookies[SESSION_COOKIE_NAME] - return session + def _list_cards(self, cards, next_page=None, title=""): + """ + Create the list of folders in the Kodi interface. + """ + xbmcplugin.setPluginCategory(HANDLE, title) + xbmcplugin.setContent(HANDLE, "movies") + + for card in cards: + list_item = xbmcgui.ListItem(label=card.title) + if card.thumbnail is not None: + list_item.setArt({"thumb": card.thumbnail}) + + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("movie") + info_tag.setTitle(card.title) + info_tag.setDirectors([card.author]) + + url = get_url(action="list_playlist", url=card.url) + is_folder = True + xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) + + # Add a "continue" folder. + if next_page is not None and len(cards) > 0: + list_item = xbmcgui.ListItem(label="Next Page") + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("video") + info_tag.setTitle("Next Page") + is_folder = True + xbmcplugin.addDirectoryItem(HANDLE, next_page, list_item, is_folder) + + xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.endOfDirectory(HANDLE) + + def _list_videos(self, videos, title="Videos", next_page=None): + xbmcplugin.setPluginCategory(HANDLE, title) + xbmcplugin.setContent(HANDLE, "movies") + + for video in videos: + list_item = xbmcgui.ListItem(label=video.title) + if video.thumbnail is not None: + list_item.setArt({"thumb": video.thumbnail}) + + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("movie") + info_tag.setTitle(video.title) + info_tag.setYear(video.upload_date.year) + info_tag.setRating(video.score, video.votes) + info_tag.setPlaycount(video.views) + info_tag.setPlot(video.description) + info_tag.setGenres([video.genre]) + info_tag.setDirectors(video.authors) + info_tag.setPremiered(video.upload_date.strftime("%Y-%m-%d")) + info_tag.setTags(video.tags) + + if video.suitability == "a": + info_tag.setMpaa("NC-17") + elif video.suitability == "m": + info_tag.setMpaa("R") + elif video.suitability == "t": + info_tag.setMpaa("PG-13") + else: + info_tag.setMpaa("PG") + + list_item.setProperty("IsPlayable", "true") + + url = get_url(action="play_video", video=video.content_url) + is_folder = False + xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) + + # Add a "continue" folder. + if next_page is not None and len(videos) > 0: + list_item = xbmcgui.ListItem(label="Next Page") + info_tag = list_item.getVideoInfoTag() + info_tag.setMediaType("video") + info_tag.setTitle("Next Page") + is_folder = True + xbmcplugin.addDirectoryItem(HANDLE, next_page, list_item, is_folder) + + xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE) + xbmcplugin.endOfDirectory(HANDLE) + + def play_video(self, **kwargs): + assert "video" in kwargs + path = kwargs["video"] + play_item = xbmcgui.ListItem(path=path) + play_item.setProperty("IsPlayable", "true") + xbmcplugin.setResolvedUrl(HANDLE, True, play_item) if __name__ == "__main__": - router(sys.argv[2][1:]) + NewgroundsPlugin.invoke(sys.argv[2][1:]) diff --git a/addon.xml b/addon.xml index b79c46e..db27111 100644 --- a/addon.xml +++ b/addon.xml @@ -1,16 +1,15 @@ - + - - + video Add-on for watching videos from Newgrounds - Add-on that displays Newgrounds movies. Newgrounds is a company and entertainment website founded by Tom Fulp in 1995. + Add-on that displays Newgrounds movies. GPLv3-only all https://git.sr.ht/~jakob/newgrounds-addon/ -- cgit v1.3