# Copyright © 2024 Jakob L. Kreuze # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU 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 General Public License for more details. # # You should have received a copy of the GNU General Public License # along with this program. If not, see . """ Video plugin for animations posted to Newgrounds. """ import concurrent.futures import os import sys from urllib.parse import urlencode, parse_qsl from bs4 import BeautifulSoup import requests import xbmc import xbmcgui import xbmcplugin from xbmcaddon import Addon from xbmcvfs import translatePath URL = sys.argv[0] HANDLE = int(sys.argv[1]) ADDON_PATH = translatePath(Addon().getAddonInfo("path")) ICONS_DIR = os.path.join(ADDON_PATH, "resources", "images", "icons") FANART_DIR = os.path.join(ADDON_PATH, "resources", "images", "fanart") def get_url(**kwargs): """ Create a URL for calling the plugin recursively from the given set of keyword arguments. :param kwargs: "argument=value" pairs :return: plugin call URL :rtype: str """ return "{}?{}".format(URL, urlencode(kwargs)) def list_genres(): """ Create the list of movie genres in the Kodi interface. """ xbmcplugin.setPluginCategory(HANDLE, "Newgrounds") xbmcplugin.setContent(HANDLE, "movies") genres = ["Featured", "Latest", "Popular"] for name in genres: list_item = xbmcgui.ListItem(label=name) # list_item.setArt({'icon': genre_info['icon'], 'fanart': genre_info['fanart']}) 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_LABEL_IGNORE_THE) 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) # list_item.setArt({'icon': genre_info['icon'], 'fanart': genre_info['fanart']}) 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_LABEL_IGNORE_THE) xbmcplugin.endOfDirectory(HANDLE) def get_video_url(url): """ TODO """ html = requests.get(url).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"}, ).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`. """ html = requests.get(url).text soup = BeautifulSoup(html, "html.parser") return { "title": soup.find("div", id="embed_header").h2.string, "thumbnail": soup.find("meta", property="og:image")["content"], "url": url, } def get_video_urls( genre="Featured", interval="today", sort="date", category=0, offset=0 ): 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 in ["date"] assert isinstance(genre, int) url = URLS[genre] response = requests.get( url, params={ "interval": interval, "sort": sort, "genre": category, "isAjaxRequest": True, "offset": offset, "inner": 1, }, headers={ "X-Requested-With": "XMLHttpRequest", }, ).json() soup = BeautifulSoup(response["content"], "html.parser") return [tag["href"] for tag in soup.find_all("a", "inline-card-portalsubmission")] def list_videos(genre, category=0, offset=0): """ 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 """ urls = get_video_urls(category=category, interval="all", offset=offset) 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 urls} for future in concurrent.futures.as_completed(future_to_url): url = future_to_url[future] videos.append(future.result()) xbmcplugin.setPluginCategory(HANDLE, "Featured") # , genre_info['genre']) xbmcplugin.setContent(HANDLE, "movies") # Get the list of videos in the category. # videos = genre_info['movies'] # Iterate through videos. for video in videos: list_item = xbmcgui.ListItem(label=video["title"]) list_item.setArt({"thumb": video["thumbnail"]}) info_tag = list_item.getVideoInfoTag() info_tag.setMediaType("movie") info_tag.setTitle(video["title"]) # info_tag.setGenres([genre_info['genre']]) # info_tag.setPlot(video['plot']) # info_tag.setYear(video['year']) 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. list_item = xbmcgui.ListItem(label="Next Page") info_tag = list_item.getVideoInfoTag() info_tag.setMediaType("video") info_tag.setTitle("Next Page") url = get_url(action="listing", genre=genre, category=category, offset=offset + 20) is_folder = True xbmcplugin.addDirectoryItem(HANDLE, url, list_item, is_folder) xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_LABEL_IGNORE_THE) xbmcplugin.addSortMethod(HANDLE, xbmcplugin.SORT_METHOD_VIDEO_YEAR) 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_genres() elif params["action"] == "listing" and "category" in params: list_videos( params["genre"], category=params["category"], offset=int(params.get("offset", 0)), ) elif params["action"] == "listing": list_categories(params["genre"]) elif params["action"] == "play": play_video(params["video"]) else: raise ValueError(f"Invalid paramstring: {paramstring}!") if __name__ == "__main__": router(sys.argv[2][1:])