summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJakob L. Kreuze <zerodaysfordays@sdf.org>2024-07-14 13:47:31 -0400
committerJakob L. Kreuze <zerodaysfordays@sdf.org>2024-07-14 13:47:31 -0400
commit4715a137f9fbf326fdf88eb4dbc3cddcbcdf8eeb (patch)
tree74f852c439442c9dc3b8f05e95900c2cf7af5eb0
parent0bd2d7b7ecbcc442df508f6bdcda3af0248f1b70 (diff)
Lift Newgrounds API interactions into separate library
-rw-r--r--README.md17
-rw-r--r--addon.py837
-rw-r--r--addon.xml7
3 files changed, 237 insertions, 624 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,662 +44,286 @@ def get_url(**kwargs):
:return: plugin call URL
:rtype: str
"""
- return "{}?{}".format(URL, urlencode(kwargs))
+ return f"{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"
+def authentication_specified():
+ addon = Addon()
+ username = addon.getSetting("newgrounds.username")
+ password = addon.getSetting("newgrounds.password")
+ return username != "" and password != ""
- return metadata
+def get_session():
+ addon = Addon()
-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.
+ session = addon.getSetting("newgrounds.session")
+ if session is not None: # and test_logged_in(session):
+ return {SESSION_COOKIE_NAME: session}
- :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",
- }
+ username = addon.getSetting("newgrounds.username")
+ password = addon.getSetting("newgrounds.password")
- assert interval in ["today", "yesterday", "week", "month", "year", "all"]
- assert sort_by in ["date", "score", "views"]
- assert isinstance(category, int)
- assert isinstance(offset, int)
+ if username == "" or password == "":
+ return {}
- 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()
+ session = open_new_session(username, password)
+ addon.setSetting("newgrounds.session", session)
- soup = BeautifulSoup(response["content"], "html.parser")
- return [tag["href"] for tag in soup.find_all("a", "inline-card-portalsubmission")]
+ return {SESSION_COOKIE_NAME: session}
-def search(term, sort_by="relevance", offset=1):
- """
- Return a list of movies matching a given search term.
+class NewgroundsPlugin(object):
+ def __init__(self):
+ self.ng = Newgrounds()
- :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)
+ 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)
- 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()
+ def default(self, **kwargs):
+ self.list_front_page()
- soup = BeautifulSoup(response["content"], "html.parser")
- return [tag["href"] for tag in soup.find_all("a", "item-portalsubmission")]
+ 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_videos(video_urls, title=None, next_page=None):
- """
- Create the list of playable videos in the Kodi interface.
+ def list_front_page(self, **kwargs):
+ """
+ Create the list of entrypoints in the Kodi interface.
+ """
+ xbmcplugin.setPluginCategory(HANDLE, "Newgrounds")
+ xbmcplugin.setContent(HANDLE, "movies")
- :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
+ groupings = {
+ "Featured": Grouping.FEATURED,
+ "Latest": Grouping.LATEST,
+ "Popular": Grouping.POPULAR,
}
- 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"]})
+ # Add a search dialog
+ list_item = xbmcgui.ListItem(label="Search")
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"])
+ 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 "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)
+ # 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)
- xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE)
- xbmcplugin.endOfDirectory(HANDLE)
+ 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])
-def list_cards(cards, title="Cards", next_page=None):
- """
- Create the list of folders in the Kodi interface.
+ url = get_url(action="list_categories", grouping=enum.value)
+ is_folder = True
- :param cards: card summaries from `fetch_visual_links`
- """
- videos = []
- xbmcplugin.setPluginCategory(HANDLE, title)
- xbmcplugin.setContent(HANDLE, "movies")
+ xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder)
- for card in cards:
- list_item = xbmcgui.ListItem(label=card["title"])
- if card["thumbnail"] is not None:
- list_item.setArt({"thumb": card["thumbnail"]})
+ xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE)
+ xbmcplugin.endOfDirectory(HANDLE)
- info_tag = list_item.getVideoInfoTag()
- info_tag.setMediaType("movie")
- info_tag.setTitle(card["title"])
- info_tag.setDirectors([card["author"]])
+ 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)
- 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"],
+ 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=params["category"],
+ category=category,
offset=offset,
)
next_page = get_url(
- action="listing",
- genre=params["genre"],
- category=params["category"],
+ action="list_videos",
+ grouping=grouping,
+ category=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}!")
+ 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,
+ )
-SESSION_COOKIE_NAME = "vmkIdu5l8m"
+ # 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,
+ )
+ 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")
-def test_logged_in(session):
- return (
- requests.get(
- "https://www.newgrounds.com/account", cookies={SESSION_COOKIE_NAME: session}
- ).status_code
- != 401
- )
+ 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])
-def authentication_specified():
- addon = Addon()
- username = addon.getSetting("newgrounds.username")
- password = addon.getSetting("newgrounds.password")
- return username != "" and password != ""
+ 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)
-def get_session():
- addon = Addon()
+ xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE)
+ xbmcplugin.endOfDirectory(HANDLE)
- session = addon.getSetting("newgrounds.session")
- if session is not None: # and test_logged_in(session):
- return {SESSION_COOKIE_NAME: session}
+ def _list_videos(self, videos, title="Videos", next_page=None):
+ xbmcplugin.setPluginCategory(HANDLE, title)
+ xbmcplugin.setContent(HANDLE, "movies")
- username = addon.getSetting("newgrounds.username")
- password = addon.getSetting("newgrounds.password")
+ for video in videos:
+ list_item = xbmcgui.ListItem(label=video.title)
+ if video.thumbnail is not None:
+ list_item.setArt({"thumb": video.thumbnail})
- if username == "" or password == "":
- return {}
+ 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)
- session = open_new_session(username, password)
- addon.setSetting("newgrounds.session", session)
+ 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")
- return {SESSION_COOKIE_NAME: session}
+ 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)
-def open_new_session(username, password):
- html = requests.get("https://www.newgrounds.com/login").text
- soup = BeautifulSoup(html, "html.parser")
+ # 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)
- form = soup.find("form", method="post")
- endpoint = form["action"]
- auth = form.find("input")["value"]
+ xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_NONE)
+ xbmcplugin.endOfDirectory(HANDLE)
- 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 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 @@
<?xml version="1.0" encoding="UTF-8"?>
-<addon id="plugin.video.newgrounds" version="0.1.0" name="Newgrounds" provider-name="zerodaysfordays">
+<addon id="plugin.video.newgrounds" version="0.1.0" name="Newgrounds Movies" provider-name="zerodaysfordays">
<requires>
<import addon="xbmc.python" version="3.0.1"/>
- <import addon="script.module.beautifulsoup4" version="4.12.2"/>
- <import addon="script.module.requests" version="2.31.0"/>
+ <import addon="script.module.newgrounds" version="0.1.0"/>
</requires>
<extension point="xbmc.python.pluginsource" library="addon.py">
<provides>video</provides>
</extension>
<extension point="xbmc.addon.metadata">
<summary lang="en_US">Add-on for watching videos from Newgrounds</summary>
- <description lang="en_US">Add-on that displays Newgrounds movies. Newgrounds is a company and entertainment website founded by Tom Fulp in 1995.</description>
+ <description lang="en_US">Add-on that displays Newgrounds movies. </description>
<license>GPLv3-only</license>
<platform>all</platform>
<source>https://git.sr.ht/~jakob/newgrounds-addon/</source>