From f2f968f16aa2eec3fe2cbf49997aaca88e63fab4 Mon Sep 17 00:00:00 2001 From: Fribb Date: Thu, 26 Aug 2021 09:33:31 +0200 Subject: [PATCH] MyAnimeList Agent v7.0.0 Total rework of the Agent with new features: * API backend changed from Atarashii to Jikan (Atarashii is not maintained anymore while Jikan is) * Additional Images can now be requested from either TheMovieDB or TheTVDB for either of the TV-Show or Movie libraries (this requires an API Key for either that needs to be provided by the User) * HAMA and the XBMCNFO importer (#18) for TV and Movies were added as accepted metadata provider * With Jikan providing more information, the Studio metadata is now correctly filles with the Studio instead of the Producers (#4) * The user can now select which preferred language should be used as the metadat title (#5) * The Cast is now being added to the Metadata (#6) * The user can select which Image should be used for the Metadata "Role". Either the Voice Actor themselves or the Character they Voice * added Ant build script to handle the version and packaging of the project --- Contents/Code/__init__.py | 198 +++---------- Contents/Code/myanimelist.py | 278 ------------------ Contents/Code/theMovieDB.py | 102 ------- Contents/Code/theTVDB.py | 101 ------- Contents/Code/utils.py | 55 ---- Contents/Code/utils/__init__.py | 5 + Contents/Code/utils/common.py | 223 +++++++++++++++ Contents/Code/utils/jikan.py | 364 ++++++++++++++++++++++++ Contents/Code/utils/myanimelistagent.py | 99 +++++++ Contents/Code/utils/themoviedb.py | 108 +++++++ Contents/Code/utils/thetvdb.py | 156 ++++++++++ Contents/DefaultPrefs.json | 98 +++++-- VERSION | 4 + build.xml | 59 ++++ 14 files changed, 1134 insertions(+), 716 deletions(-) delete mode 100644 Contents/Code/myanimelist.py delete mode 100644 Contents/Code/theMovieDB.py delete mode 100644 Contents/Code/theTVDB.py delete mode 100644 Contents/Code/utils.py create mode 100644 Contents/Code/utils/__init__.py create mode 100644 Contents/Code/utils/common.py create mode 100644 Contents/Code/utils/jikan.py create mode 100644 Contents/Code/utils/myanimelistagent.py create mode 100644 Contents/Code/utils/themoviedb.py create mode 100644 Contents/Code/utils/thetvdb.py create mode 100644 VERSION create mode 100644 build.xml diff --git a/Contents/Code/__init__.py b/Contents/Code/__init__.py index 4fb36ed..c139ae4 100644 --- a/Contents/Code/__init__.py +++ b/Contents/Code/__init__.py @@ -1,190 +1,70 @@ -''' -Last update on 26.05.2018 - -Plex Media Server Metadata Agent for MyAnimeList.net - -This Agent will look up the title of the Anime and get the Metadata for the ID. -The Metadata will be cached in Plex for 1 Day - -@author: Fribb http://coding.fribbtastic.net/ -''' -import re -from datetime import datetime -from myanimelist import MyAnimeListUtils -from theTVDB import TheTVDBUtils -from theMovieDB import TheMovieDbUtils -from utils import Utils - -'''The Constants''' -AGENT_NAME = "MyAnimeList.net Agent" -AGENT_VERSION = "v6.0.5" -AGENT_LANGUAGES = [Locale.Language.English] -AGENT_PRIMARY_PROVIDER = True -AGENT_ACCEPTS_FROM = [ 'com.plexapp.agents.localmedia', 'com.plexapp.agents.opensubtitles', 'com.plexapp.agents.subzero' ] -AGENT_CACHE_TIME = CACHE_1HOUR * 24 +from utils import * -AGENT_MAPPING_URL = "https://atarashii.fribbtastic.net/mapping/animeMapping_full.json" -AGENT_MAPPING_CACHE_TIME = CACHE_1HOUR * 24 -AGENT_UTILS = None -AGENT_MYANIMELIST = None -AGENT_THETVDB = None -AGENT_THEMOVIEDB = None +global COMMON_UTILS +COMMON_UTILS = CommonUtils() def Start(): - Log.Info("[" + AGENT_NAME + "] " + "Starting MyAnimeList.net Metadata Agent " + AGENT_VERSION) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Starting MyAnimeList.net Metadata Agent v" + COMMON_UTILS.getVersion()) - # Initialize Utils - global AGENT_UTILS - AGENT_UTILS = Utils() - global AGENT_MYANIMELIST - AGENT_MYANIMELIST = MyAnimeListUtils() - global AGENT_THETVDB - AGENT_THETVDB = TheTVDBUtils() - global AGENT_THEMOVIEDB - AGENT_THEMOVIEDB = TheMovieDbUtils() + return - def ValidatePrefs(): - Log.Info("[" + AGENT_NAME + "] " + "Validating Preferences") + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Validating Preferences") - Log.Debug("[" + AGENT_NAME + "] " + "Fetch TheMovieDB images: " + str(Prefs["getTheMovieDbImages"])) - Log.Debug("[" + AGENT_NAME + "] " + "Fetch TheTVDB images: " + str(Prefs["getTheTVDBImages"])) - Log.Debug("[" + AGENT_NAME + "] " + "TheMovieDB Background Image size: " + Prefs["theMovieDbBackgroundSize"]) - Log.Debug("[" + AGENT_NAME + "] " + "TheMovieDB Poster Image size: " + Prefs["theMovieDbPosterSize"]) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Preferred Title Language: " + str(Prefs["preferredTitle"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Preferred Staff Image: " + str(Prefs["actorImage"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Preferred Staff Language: " + str(Prefs["actorLanguage"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Preferred Image Source for Shows: " + str(Prefs["tvshowImageSource"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "Preferred Image Source for Movies: " + str(Prefs["movieImageSource"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "TheMovieDB API Key: " + "-redacted-") #str(Prefs["tmdbAPIKey"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "TheMovieDB Poster Size: " + str(Prefs["tmdbPosterSize"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "TheMovieDB Background Size: " + str(Prefs["tmdbBackgroundSize"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "TheTVDB API Key: " + "-redacted-") #str(Prefs["tvdbAPIKey"])) + Log.Info("[" + COMMON_UTILS.getAgentName() + "] " + "TheTVDB API PIN: " + "-redacted-") #str(Prefs["tvdbAPIPIN"])) - Log.Info("[" + AGENT_NAME + "] " + "Validation Complete") - -''' -Class declaration for The Agent -''' -class MALAgent: - ''' - Method to search for an Anime TV-Show on the API - ''' - def searchAnime(self, results, media, lang, type): - Log.Info("[" + AGENT_NAME + "] " + "Searching for Anime") - - # check on mediaType if it is tv or movie - if type == "tv": - title = AGENT_UTILS.removeASCII(media.show) - elif type == "movie": - title = AGENT_UTILS.removeASCII(media.name) - else: - Log.Error("[" + AGENT_NAME + "] " + "No type defined, don't know which name to pick") - - AGENT_MYANIMELIST.search(title, results, lang) - - Log.Info("[" + AGENT_NAME + "] " + "Search Complete") - return - - ''' - Method to update the metadata information of an Anime TV-Show - ''' - def updateTvShow(self, metadata, media, lang): - Log.Info("[" + AGENT_NAME + "] " + "Updating TV-Show Anime with ID: " + metadata.id) - - AGENT_MYANIMELIST.getData(metadata, "tvshow", media) - - if Prefs["getTheTVDBImages"] == True: - Log.Debug("[" + AGENT_NAME + "] " + "Fetching TheTVDB Mapping") - mappingId = self.getMapping(metadata.id, "thetvdb") - - if mappingId is not None: - Log.Debug("[" + AGENT_NAME + "] " + "Fetching TheTVDB Information") - AGENT_THETVDB.getData(mappingId, metadata) - - Log.Info("[" + AGENT_NAME + "] " + "Update Complete") - return - - ''' - Method to update the metadata information of an Anime Movie - ''' - def updateMovie(self, metadata, media, lang): - Log.Info("[" + AGENT_NAME + "] " + "Updating Movie Anime with ID: " + metadata.id) - - AGENT_MYANIMELIST.getData(metadata, "movie", media) - - if Prefs["getTheMovieDbImages"] == True: - Log.Debug("[" + AGENT_NAME + "] " + "Fetching TheMovieDB Mapping") - mappingId = self.getMapping(metadata.id, "themoviedb") - - if mappingId is not None: - Log.Debug("[" + AGENT_NAME + "] " + "Fetching TheMovieDB Information") - AGENT_THEMOVIEDB.getData(mappingId, metadata) - - Log.Info("[" + AGENT_NAME + "] " + "Update Complete") - return - - ''' - Method to get the Mapping for a given ID - ''' - def getMapping(self, id, key): - - mappingFull = None - - try: - Log.Info("[" + AGENT_NAME + "] [Utils] " + "Fetching URL " + str(AGENT_MAPPING_URL)) - mappingFull = JSON.ObjectFromString(HTTP.Request(AGENT_MAPPING_URL, sleep=2.0, cacheTime=AGENT_MAPPING_CACHE_TIME).content) - except Exception as e: - Log.Info("[" + AGENT_NAME + "] " + "Mapping could not be requested " + str(e)) - - if mappingFull is None: - Log.Error("[" + AGENT_NAME + "] " + "Mapping could not be loaded") - return None - else: - Log.Info("[" + AGENT_NAME + "] " + "Searching for mapping for ID " + id) - - mappingString = str(key) + "_id" - mappingId = None - - for mapping in mappingFull: - if "mal_id" in mapping: - malId = AGENT_UTILS.getJSONValue("mal_id", mapping) - - if str(malId) == id: - mappingId = AGENT_UTILS.getJSONValue(mappingString, mapping) - - if mappingId == -1: - Log.Info("[" + AGENT_NAME + "] " + "Mapping entry was available but ID is not a valid TheTVDB or TheMovieDB ID") - return None - else: - Log.Info("[" + AGENT_NAME + "] " + "Mapping entry for ID found: " + str(key) + " = " + str(mappingId)) - return mappingId # don't need to search further if I already got the ID - return mappingId + return ''' Class declaration for The TV-Show Agent ''' -class MyAnimeList_TV(Agent.TV_Shows, MALAgent): +class MyAnimeList_TV(Agent.TV_Shows, MyAnimeListAgent): # initialize configuration - name = AGENT_NAME - languages = AGENT_LANGUAGES - primary_provider = AGENT_PRIMARY_PROVIDER - accepts_from = AGENT_ACCEPTS_FROM + name = COMMON_UTILS.getAgentName() + languages = COMMON_UTILS.getLanguages() + primary_provider = COMMON_UTILS.getPrimaryProvider() + accepts_from = COMMON_UTILS.getAcceptsFrom() + + MYANIMELIST = MyAnimeListAgent() + + TYPE = "show" def search(self, results, media, lang, manual): - self.searchAnime(results, media, lang, "tv") + self.MYANIMELIST.search(results, media, lang, manual, self.TYPE) return def update(self, metadata, media, lang, force): - self.updateTvShow(metadata, media, lang) + self.MYANIMELIST.update(metadata, media, lang, force, self.TYPE) return ''' Class declaration for The Movie Agent ''' -class MyAnimeList_Movie(Agent.Movies, MALAgent): +class MyAnimeList_Movie(Agent.Movies, MyAnimeListAgent): # initialize configuration - name = AGENT_NAME - languages = AGENT_LANGUAGES - primary_provider = AGENT_PRIMARY_PROVIDER - accepts_from = AGENT_ACCEPTS_FROM + name = COMMON_UTILS.getAgentName() + languages = COMMON_UTILS.getLanguages() + primary_provider = COMMON_UTILS.getPrimaryProvider() + accepts_from = COMMON_UTILS.getAcceptsFrom() + + MYANIMELIST = MyAnimeListAgent() + + TYPE = "movie" def search(self, results, media, lang, manual): - self.searchAnime(results, media, lang, "movie") + self.MYANIMELIST.search(results, media, lang, manual, self.TYPE) return def update(self, metadata, media, lang, force): - self.updateMovie(metadata, media, lang) + self.MYANIMELIST.update(metadata, media, lang, force, self.TYPE) return \ No newline at end of file diff --git a/Contents/Code/myanimelist.py b/Contents/Code/myanimelist.py deleted file mode 100644 index 1bc87fd..0000000 --- a/Contents/Code/myanimelist.py +++ /dev/null @@ -1,278 +0,0 @@ -''' -Last update on 26.05.2018 - -Plex Media Server Metadata Agent for MyAnimeList.net - -TODO: Description - -@author: Fribb http://coding.fribbtastic.net/ -''' -import math -import re, ssl, urllib2 - -from utils import Utils - -'''Constants''' -AGENT_NAME = "MyAnimeList.net" - -MYANIMELIST_URL_MAIN = "https://atarashii.fribbtastic.net" -MYANIMELIST_URL_SEARCH = "/web/2.1/anime/search?q={title}" -MYANIMELIST_URL_DETAILS = "/web/2.1/anime/{id}" -MYANIMELIST_URL_EPISODES = "/web/2.1/anime/episodes/{id}?page={page}" -MYANIMELIST_CACHE_TIME = CACHE_1HOUR * 24 * 7 - -class MyAnimeListUtils(): - - ''' - Search the Atarashii API for the name of the Anime - ''' - def search(self, name, results, lang): - manualIdMatch = re.match(r'^myanimelist-id:([0-9]+)$', str(name)) - - if manualIdMatch: - manualId = manualIdMatch.group(1) - - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Searching for Anime by ID: " + str(manualId)) - - searchUrl = MYANIMELIST_URL_MAIN + MYANIMELIST_URL_DETAILS.format(id=str(manualId)) - wrapSearchResultsInArray = True - forcePerfectAnimeMatchScore = True - else: - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Searching for Anime: " + str(name)) - - searchUrl = MYANIMELIST_URL_MAIN + MYANIMELIST_URL_SEARCH.format(title=String.Quote(name, usePlus=True)) - wrapSearchResultsInArray = False - forcePerfectAnimeMatchScore = False - - utils = Utils() - - try: - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Fetching URL " + str(searchUrl)) - searchResults = JSON.ObjectFromString(HTTP.Request(searchUrl, sleep=2.0, cacheTime=MYANIMELIST_CACHE_TIME).content) - except Exception as e: - Log.Info("[" + AGENT_NAME + "] " + "search results could not be requested " + str(e)) - return - - if wrapSearchResultsInArray: - searchResults = [searchResults] - - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + str(len(searchResults)) + " Results found") - for series in searchResults: - # get the ID if it is available - apiAnimeId = str(utils.getJSONValue("id", series)) - - # get the title if it is available - apiAnimeTitle = str(utils.getJSONValue("title", series)) - - # get the year if it is available - apiAnimeYear = str(utils.getJSONValue("start_date", series)).split("-")[0] - - # calculate the match score - if forcePerfectAnimeMatchScore: - animeMatchScore = 100 - elif len(apiAnimeTitle) > 0: - animeMatchScore = utils.getMatchScore(apiAnimeTitle, name) - - # append results to search results - Log.Debug("[" + AGENT_NAME + "] " + "Anime Found - ID=" + str(apiAnimeId) + " Title=" + str(apiAnimeTitle) + " Year=" + str(apiAnimeYear) + " MatchScore=" + str(animeMatchScore)) - results.Append(MetadataSearchResult(id=apiAnimeId, name=apiAnimeTitle, year=apiAnimeYear, score=animeMatchScore, lang=lang)) - - return - - ''' - Method to request the details of an AnimeID and add the information to the metadata - ''' - def getData(self, metadata, type, media): - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Requesting Information from MyAnimeList.net") - - utils = Utils() - - detailUrl = MYANIMELIST_URL_MAIN + MYANIMELIST_URL_DETAILS.format(id=metadata.id) - - try: - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Fetching URL " + str(detailUrl)) - detailResult = JSON.ObjectFromString(HTTP.Request(detailUrl, sleep=2.0, cacheTime=MYANIMELIST_CACHE_TIME).content) - except Exception as e: - Log.Error("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "No Detail Information were available " + str(e)) - return - - '''Parse the main Elements of the response''' - apiAnimeId = None # the ID of the Anime (plex metadata.id, myanimelist id) - apiAnimeTitle = None # the Title of the Anime (plex metadata.title, myanimelist title) - apiAnimeSummary = None # the Summary of the Anime (plex metadata.summary, myanimelist synopsis) - apiAnimeRating = None # the Rating of the Anime (plex metadata.rating, myanimelist members_score) - apiAnimeAvailableAt = None # the Date of the Anime first aired (plex metadata.originally_available_at, myanimelist start_date) - apiAnimeContentRating = None # the Content rating of the Anime (plex metadata.content_rating, myanimelist classification) - apiAnimeCovers = None # the Covers of the Anime (plex metadata.posters, myanimelist image_url) - apiAnimeDuration = None # the Duration of an Anime Episode (plex metadata.duration, myanimelist duration) - apiAnimeGenres = None # the Genres of the Anime (plex metadata.genres, myanimelist genres) - apiAnimeProducers = None # the Producers of the Anime (plex metadata.studio, myanimelist producers) ### TODO: Switch to Studios when they are available in the API (or add Producers to metadata when this is possible in Plex) - - if detailResult is not None: - - # get the ID if it is available - apiAnimeId = utils.getJSONValue("id", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "ID: " + str(apiAnimeId)) - if apiAnimeId is not None: - metadata.id = str(apiAnimeId) - - # get the Title if it is available - apiAnimeTitle = utils.getJSONValue("title", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Title: " + str(apiAnimeTitle)) - if apiAnimeTitle is not None: - metadata.title = str(apiAnimeTitle) - - # get the Summary if it is available - apiAnimeSummary = utils.getJSONValue("synopsis", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Summary: " + str(apiAnimeSummary)) - if apiAnimeSummary is not None: - metadata.summary = str(utils.removeTags(apiAnimeSummary)) - - # get the Rating if it is available - apiAnimeRating = utils.getJSONValue("members_score", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Rating: " + str(apiAnimeRating)) - if apiAnimeRating is not None: - metadata.rating = float(apiAnimeRating) - - # get the first aired Date if it is available - tmpYear = utils.getJSONValue("start_date", detailResult) - if(tmpYear is not None): - try: - apiAnimeAvailableAt = datetime.strptime(str(tmpYear), "%Y-%m-%d") - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Year: " + str(apiAnimeAvailableAt)) - if apiAnimeAvailableAt is not None: - metadata.originally_available_at = apiAnimeAvailableAt - except Exception as e: - Log.Error("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "There was an Error while adding the Aired Date " + str(e)) - - # get the content rating if it is available - apiAnimeContentRating = utils.getJSONValue("classification", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Content Rating: " + str(apiAnimeContentRating)) - if apiAnimeContentRating is not None: - metadata.content_rating = str(apiAnimeContentRating) - - # get the covers if they are available - apiAnimeCovers = utils.getJSONValue("image_url", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Cover: " + str(apiAnimeCovers)) - if apiAnimeCovers is not None: - if not apiAnimeCovers: - Log.Warn("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Image was empty") - else: - if metadata.posters[str(apiAnimeCovers)] is None: - - #metadata.posters[str(apiAnimeCovers)] = Proxy.Media(HTTP.Request(str(apiAnimeCovers), sleep=2.0).content) - - request = urllib2.Request(str(apiAnimeCovers)) - response = urllib2.urlopen(request, context=ssl.SSLContext(ssl.PROTOCOL_SSLv23)) - content = response.read() - - metadata.posters[str(apiAnimeCovers)] = Proxy.Media(content) - else: - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Image is already present") - - # get the duration if it is available - tmpDuration = utils.getJSONValue("duration", detailResult) - if(tmpDuration is not None): - apiAnimeDuration = tmpDuration * 60000 - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Duration: " + str(apiAnimeDuration)) - if apiAnimeDuration is not None: - metadata.duration = int(apiAnimeDuration) - - # get the genres if they are available - apiAnimeGenres = utils.getJSONValue("genres", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Genres: " + str(apiAnimeGenres)) - if apiAnimeGenres is not None and len(apiAnimeGenres) > 0: - for genre in apiAnimeGenres: - metadata.genres.add(str(genre)) - - # get the producers if they are available - # TODO: plex only has Studios currently and the Atarashii API does not provide the Studios from MyAnimeList (yet) - # When either of those are available this needs to be fixed - tmpProducers = utils.getJSONValue("producers", detailResult) - if(tmpProducers is not None): - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Producers: " + str(tmpProducers)) - if tmpProducers is not None and len(tmpProducers) > 0: - apiAnimeProducers = "" - for idx, producer in enumerate(tmpProducers): - apiAnimeProducers += str(producer) - if idx < len(tmpProducers) - 1: - apiAnimeProducers += ", " - - metadata.studio = str(apiAnimeProducers) - - ''' - Add specific data for TV-Shows - ''' - if type == "tvshow": - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Adding TV-Show specific data") - - apiAnimeEpisodeCount = None - pages = None - - # get the episode count if it is available - apiAnimeEpisodeCount = utils.getJSONValue("episodes", detailResult) - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Episodes: " + str(apiAnimeEpisodeCount)) - - if apiAnimeEpisodeCount is not None and apiAnimeEpisodeCount is not 0: - metadata.seasons[1].episode_count = int(apiAnimeEpisodeCount) - else: - metadata.seasons[1].episode_count = int(len(media.seasons[1].episodes)) - - pages = int(math.ceil(float(metadata.seasons[1].episode_count) / 100)) - - # fetch the episodes in 100 chunks - if pages is not None: - - for page in range(1, pages + 1): - episodesUrl = MYANIMELIST_URL_MAIN + MYANIMELIST_URL_EPISODES.format(id=metadata.id,page=page) - - try: - Log.Info("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Fetching URL " + str(episodesUrl)) - episodeResult = JSON.ObjectFromString(HTTP.Request(episodesUrl, sleep=2.0, cacheTime=MYANIMELIST_CACHE_TIME).content) - except Exception as e: - Log.Info("[" + AGENT_NAME + "] " + "episode results could not be requested " + str(e)) - return - - if "error" in episodeResult: - Log.Warn("[" + AGENT_NAME + "] " + "Episode Information are not available (" + str(episodeResult["error"]) + ") (might want to add them to MyAnimeList.net)!") - break - - for episode in episodeResult: - apiEpisodeNumber = None # the Number of the episode - apiEpisodeTitle = None # the title of the Episode - apiEpisodeAirDate = None # the air date of the Episode - - # get the episode Number - apiEpisodeNumber = utils.getJSONValue("number", episode) - - # get the episode title - apiEpisodeTitle = utils.getJSONValue("title", episode) - - # get the episode air date - apiEpisodeAirDate = utils.getJSONValue("air_date", episode) - - if apiEpisodeNumber is not None: - plexEpisode = metadata.seasons[1].episodes[int(apiEpisodeNumber)] - - # add the Episode Title if it is available, if not use a default title - if apiEpisodeTitle is not None: - plexEpisode.title = str(apiEpisodeTitle) - else: - plexEpisode.title = "Episode: #" + str(apiEpisodeNumber) - - # add the episode air date if it is available, if not use the current date - if apiEpisodeAirDate is not None: - plexEpisode.originally_available_at = datetime.strptime(str(apiEpisodeAirDate), "%Y-%m-%d") - else: - plexEpisode.originally_available_at = datetime.now() - - Log.Debug("Episode " + str(apiEpisodeNumber) + ": " + str(apiEpisodeTitle) + " - " + str(apiEpisodeAirDate)) - - ''' - Add specific data for Movies - ''' - if type == "movie": - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "Adding Movie specific data") - Log.Debug("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "nothing specific to add") - - return \ No newline at end of file diff --git a/Contents/Code/theMovieDB.py b/Contents/Code/theMovieDB.py deleted file mode 100644 index 8020aa7..0000000 --- a/Contents/Code/theMovieDB.py +++ /dev/null @@ -1,102 +0,0 @@ -''' -Last update on 26.05.2018 - -Plex Media Server Metadata Agent for MyAnimeList.net - -TODO: Description - -@author: Fribb http://coding.fribbtastic.net/ -''' - -from utils import Utils - -'''Constants''' -AGENT_NAME = "MyAnimeList.net" - -THEMOVIEDB_URL_MAIN = "https://api.themoviedb.org" -THEMOVIEDB_URL_CONFIGURATION = "/3/configuration?api_key={api_key}" -THEMOVIEDB_URL_MOVIE_IMAGES = "/3/movie/{id}/images?api_key={api_key}" -THEMOVIEDB_API_KEY = "f42adc8664ab008c7ea99b720c576213" -THEMOVIEDB_CACHE_TIME = CACHE_1HOUR * 24 * 7 - -class TheMovieDbUtils(): - - ''' - Method to request the image information from TheMovieDB.org - ''' - def getData(self, id, metadata): - Log.Info("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "Requesting Information from TheMovieDB.org") - - utils = Utils() - - imageBase = None - - ### Request the TheMovieDB configuration for the image base URL - if imageBase is None: - theMovieDBConfigUrl = THEMOVIEDB_URL_MAIN + THEMOVIEDB_URL_CONFIGURATION.format(api_key=THEMOVIEDB_API_KEY) - - try: - Log.Info("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "Fetching URL " + str(theMovieDBConfigUrl)) - configResult = JSON.ObjectFromString(HTTP.Request(theMovieDBConfigUrl, sleep=2.0, cacheTime=THEMOVIEDB_CACHE_TIME).content) - except Exception as e: - Log.Error("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB configuration could not be requested " + str(e)) - return - - if configResult is not None: - if "images" in configResult: - if "base_url" in configResult["images"]: - imageBase = configResult["images"]["base_url"] - else: - Log.Error("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB Image config is not available") - return - else: - Log.Error("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB Image config is not available") - return - else: - Log.Error("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB Image config is not available") - return - - theMovieDBUrl = THEMOVIEDB_URL_MAIN + THEMOVIEDB_URL_MOVIE_IMAGES.format(id=id, api_key=THEMOVIEDB_API_KEY) - - ### Request the TheMovieDB information by ID - - try: - Log.Info("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "Fetching URL " + str(theMovieDBUrl)) - movieResult = JSON.ObjectFromString(HTTP.Request(theMovieDBUrl, sleep=2.0, cacheTime=THEMOVIEDB_CACHE_TIME).content) - except Exception as e: - Log.Error("[" + AGENT_NAME + "] [MyAnimeListUtils] " + "TheMovieDB image result could not be requested " + str(e)) - return - - if movieResult is not None: - - if "backdrops" in movieResult: - backdrops = movieResult["backdrops"] - for backdrop in backdrops: - url = imageBase + Prefs["theMovieDbBackgroundSize"] + "/" + backdrop["file_path"] - Log.Debug("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB Background Image Url=" + url) - - if metadata.art[str(url)] is None: - metadata.art[str(url)] = Proxy.Media(HTTP.Request(str(url), sleep=2.0).content) - else: - Log.Debug("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "Image is already present") - else: - Log.Warn("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "There were no backdrops") - - if "posters" in movieResult: - posters = movieResult["posters"] - for poster in posters: - url = imageBase + Prefs["theMovieDbBackgroundSize"] + "/" + poster["file_path"] - Log.Debug("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB Poster Image Url=" + url) - - if metadata.posters[str(url)] is None: - metadata.posters[str(url)] = Proxy.Media(HTTP.Request(str(url), sleep=2.0).content) - else: - Log.Debug("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "Image is already present") - else: - Log.Warn("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "There were no posters") - - else: - Log.Error("[" + AGENT_NAME + "] [TheMovieDbUtils] " + "TheMovieDB Image Information are not available") - return - - return \ No newline at end of file diff --git a/Contents/Code/theTVDB.py b/Contents/Code/theTVDB.py deleted file mode 100644 index 4602363..0000000 --- a/Contents/Code/theTVDB.py +++ /dev/null @@ -1,101 +0,0 @@ -''' -Last update on 26.05.2018 - -Plex Media Server Metadata Agent for MyAnimeList.net - -TODO: Description - -@author: Fribb http://coding.fribbtastic.net/ -''' - -from utils import Utils - -'''Constants''' -AGENT_NAME = "MyAnimeList.net" - -THETVDB_URL_MAIN = "https://www.thetvdb.com" -THETVDB_URL_API_MAIN = "https://api.thetvdb.com" -THETVDB_URL_LOGIN = "/login" -THETVDB_URL_SERIES_IMAGES = "/series/{id}/images" -THETVDB_URL_SERIES_IMAGE_QUERY = "/series/{id}/images/query?keyType={imageType}" -THETVDB_URL_BANNERS = "/banners/{image}" -THETVDB_API_KEY = "CE86D5F59D2835C2" -THETVDB_CACHE_TIME = CACHE_1HOUR * 24 * 7 - -class TheTVDBUtils(): - - ''' - Mapping to request the image information from TheTVDB.com - ''' - def getData(self, id, metadata): - Log.Info("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Requesting Information from TheTVDB.com") - - utils = Utils() - token = None - - loginUrl = THETVDB_URL_API_MAIN + THETVDB_URL_LOGIN - - try: - loginResponse = JSON.ObjectFromString(HTTP.Request(loginUrl, data=JSON.StringFromObject(dict(apikey=THETVDB_API_KEY)), headers={'Content-type': 'application/json'}, sleep=2.0, cacheTime=THETVDB_CACHE_TIME).content) - except Exception as e: - Log.Error("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Could not retrieve authentication token from TheTVDB.com " + str(e)) - return - - if loginResponse is not None: - if "token" in loginResponse: - token = loginResponse["token"] - Log.Debug("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Token is: " + token) - else: - Log.Error("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Could not retrieve authentication token from TheTVDB.com") - return - - imageTypesUrl = THETVDB_URL_API_MAIN + THETVDB_URL_SERIES_IMAGES.format(id=id) - - imageTypesResult = None - - try: - imageTypesResult = JSON.ObjectFromString(HTTP.Request(imageTypesUrl, headers={'Authorization': 'Bearer %s' % token}, sleep=2.0, cacheTime=THETVDB_CACHE_TIME).content) - except Exception as e: - Log.Error("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Could not retrieve imageTypeResult " + str(e)) - - if imageTypesResult is not None: - - if "data" in imageTypesResult: - - for type in imageTypesResult["data"]: - imageTypeQueryUrl = THETVDB_URL_API_MAIN + THETVDB_URL_SERIES_IMAGE_QUERY.format(id=id, imageType=type) - - if type == "poster" or type == "fanart" or type == "series": - - imageData = JSON.ObjectFromString(HTTP.Request(imageTypeQueryUrl, headers={'Authorization': 'Bearer %s' % token}, cacheTime=THETVDB_CACHE_TIME, sleep=2.0).content) - - Log.Info("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Adding " + type + " Images to metadata") - for data in imageData["data"]: - keyType = data["keyType"] - fileName = THETVDB_URL_MAIN + THETVDB_URL_BANNERS.format(image=data["fileName"]) - - Log.Debug("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Image: " + keyType + " - " + fileName) - - - if keyType == "poster": - if metadata.posters[str(fileName)] is None: - metadata.posters[str(fileName)] = Proxy.Media(HTTP.Request(str(fileName), sleep=2.0).content) - else: - Log.Debug("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Image is already present") - - if keyType == "fanart": - if metadata.art[str(fileName)] is None: - metadata.art[str(fileName)] = Proxy.Media(HTTP.Request(str(fileName), sleep=2.0).content) - else: - Log.Debug("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Image is already present") - - if keyType == "series": - if metadata.banners[str(fileName)] is None: - metadata.banners[str(fileName)] = Proxy.Media(HTTP.Request(str(fileName), sleep=2.0).content) - else: - Log.Debug("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Image is already present") - - else: - Log.Error("[" + AGENT_NAME + "] [TheTVDBUtils] " + "Could not retrieve image information from TheTVDB.com") - - return \ No newline at end of file diff --git a/Contents/Code/utils.py b/Contents/Code/utils.py deleted file mode 100644 index 356cc1c..0000000 --- a/Contents/Code/utils.py +++ /dev/null @@ -1,55 +0,0 @@ -''' -Last update on 26.05.2018 - -Plex Media Server Metadata Agent for MyAnimeList.net - -TODO: Description - -@author: Fribb http://coding.fribbtastic.net/ -''' - -import re -import difflib - -'''Constants''' -AGENT_NAME = "MyAnimeList.net" - -UTILS_DEFAULT_HEADERS = {'User-agent': 'Plex/MyAnimeList-Agent'} -UTILS_DEFAULT_CACHE_TIME = CACHE_1HOUR * 24 - - -class Utils(): - - ''' - Method to remove all ASCII from the text - ''' - def removeASCII(self, text): - return re.sub(r'[^\x00-\x7F]+', ' ', text) - - ''' - Method to remove all HTML tags - ''' - def removeTags(self, text): - cleanr = re.compile('<.*?>') - cleantext = re.sub(cleanr, '', text) - return cleantext - - ''' - Method to get the JSON value of a key - ''' - def getJSONValue(self, key, json): - value = None - - if key in json: - value = json[key] - - return value - - ''' - Method to calculate the Match Score of the titles - ''' - def getMatchScore(self, title1, title2): - #result = int(100 - abs(String.LevenshteinDistance(title1, title2))) - result = int(difflib.SequenceMatcher(None, title1, title2).ratio() * 100) - - return result \ No newline at end of file diff --git a/Contents/Code/utils/__init__.py b/Contents/Code/utils/__init__.py new file mode 100644 index 0000000..19fee8a --- /dev/null +++ b/Contents/Code/utils/__init__.py @@ -0,0 +1,5 @@ +from common import CommonUtils +from myanimelistagent import MyAnimeListAgent +from jikan import JikanApiUtils +from thetvdb import TheTvDbUtils +from themoviedb import TheMovieDbUtils \ No newline at end of file diff --git a/Contents/Code/utils/common.py b/Contents/Code/utils/common.py new file mode 100644 index 0000000..45b6a8b --- /dev/null +++ b/Contents/Code/utils/common.py @@ -0,0 +1,223 @@ +import urllib +import os +import re +import sys +import urllib2 +import ssl +import difflib + +class CommonUtils: + ''' the path to the VERSION file ''' + VERSIONFILEPATH = os.path.join(Core.bundle_path, "VERSION") + + ''' the name of the Agent ''' + AGENT_NAME = "MyAnimeList.net" + ''' the library languages ''' + AGENT_LANGUAGES = [Locale.Language.English] + ''' is the agent a primary provider of the metadata ''' + AGENT_PRIMARY_PROVIDER = True + ''' allows other agents to provide metadata ''' + AGENT_ACCEPTS_FROM = [ 'com.plexapp.agents.localmedia', 'com.plexapp.agents.opensubtitles', 'com.plexapp.agents.subzero', 'com.plexapp.agents.xbmcnfo', 'com.plexapp.agents.xbmcnfotv', 'com.plexapp.agents.hama' ] + ''' caching time for requests ''' + AGENT_CACHE_TIME = CACHE_1HOUR * 24 + ''' mapping file ''' + AGENT_MAPPING_URL = "https://fribbtastic.net/mapping/animeMapping_full.json" + ''' headers ''' + AGENT_HEADERS = {'Content-type': 'application/json'} + + ''' + return the name of the Agent + ''' + def getAgentName(self): + return self.AGENT_NAME + + ''' + return the supported languages + ''' + def getLanguages(self): + return self.AGENT_LANGUAGES + + ''' + return the Primary Provider variable + ''' + def getPrimaryProvider(self): + return self.AGENT_PRIMARY_PROVIDER + + ''' + return the agents that are accepted to provide additional metadata + ''' + def getAcceptsFrom(self): + return self.AGENT_ACCEPTS_FROM + + ''' + return the the time a request should be cached (default 24 hours) + ''' + def getCacheTime(self): + return self.AGENT_CACHE_TIME + + ''' + return the url for the mapping file + ''' + def getMappingUrl(self): + return self.AGENT_MAPPING_URL + + ''' + load the Version file and add the keys to a dictionary + ''' + def loadVersionFile(self): + separator = "=" + keys = {} + + data = Core.storage.load(self.VERSIONFILEPATH) + + for line in data.splitlines(): + if separator in line: + name, value = line.split(separator, 1) + keys[name.strip()] = value.strip() + + return keys + + ''' + get the current Version + ''' + def getVersion(self): + keys = self.loadVersionFile() + version = keys["major.number"] + "." + keys["minor.number"] + "." + keys["build.number"] + + return version + + ''' + remove all ASCII characters from the name + ''' + def removeAscii(self, name): + return re.sub(r'[^\x00-\x7F]+', ' ', name) + + ''' + get response from a given URL with or without POST data + ''' + def getResponse(self, url, data=None, headers={}): + Log.Debug("[" + self.AGENT_NAME + "] " + "Requesting response from '" + url + "'") + + header = self.AGENT_HEADERS.copy() + + for h in headers: + header[h] = headers[h] + + try: + request = urllib2.Request(url, data, header) + response = urllib2.urlopen(request, context=ssl.SSLContext(ssl.PROTOCOL_SSLv23), timeout=60) + + return response.read() + except Exception as e: + Log.Error("[" + self.AGENT_NAME + "] " + "Error getting response '" + str(e) + "'") + return None + + ''' + return the JSON value for a key + ''' + def getJsonValue(self, key, json): + value = None + + if key in json: + value = json[key] + + return value + + ''' + get an array from a JSON Array that only contains the individual keys as elements + ''' + def getArrayFromJsonValue(self, key, json): + elemArr = [] + + for elem in json: + elemName = self.getJsonValue(key, elem) + elemArr.append(elemName) + + return elemArr + + ''' + calculate the match score between the two titles + ''' + def calcMatchScore(self, title1, title2): + #result = int(100 - abs(String.LevenshteinDistance(title1, title2))) + result = int(difflib.SequenceMatcher(None, title1, title2).ratio() * 100) + + return result + + ''' + parse the Date from a JSON value + ''' + def parseDateFromJson(self, key, json): + date = None + value = self.getJsonValue(key, json) + + if value is not None: + date = Datetime.ParseDate(value) + + return date + + ''' + get only the year from the datetime + ''' + def getYear(self, key, json): + date = self.parseDateFromJson(key, json) + if date is not None: + return date.year + else: + return None + + ''' + get the date from the datetime + ''' + def getDate(self, key, json): + date = self.parseDateFromJson(key, json) + + return date.date() + + ''' + get the current Date + ''' + def getNowDate(self): + return Datetime.Now() + + ''' + get the regular expression match from a string + ''' + def getRegExMatch(self, pattern, string, group): + match = re.match(pattern, string) + + if match is not None: + return match.group(group) + else: + return None + + ''' + get the mapping between MyAnimeList and TheTVDB/TheMovieDB + ''' + def getMapping(self, id, key): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting Mapping file") + + mappingFull = JSON.ObjectFromString(self.getResponse(self.getMappingUrl())) + + if mappingFull is not None: + mappingkey = str(key) + "_id" + mappingId = None + + for mapping in mappingFull: + if "mal_id" in mapping: + malId = self.getJsonValue("mal_id", mapping) + + if str(malId) == id: + mappingId = self.getJsonValue(mappingkey, mapping) + + if mappingId == -1 or mappingId is None: + Log.Info("[" + self.AGENT_NAME + "] " + "Mapping was available for mal_id=" + str(malId) + " but ID for '" + str(key) + "' was not valid or did not exist") + return None + else: + Log.Info("[" + self.AGENT_NAME + "] " + "Mapping found: " + str(key) + " = " + str(mappingId)) + return mappingId + + return mappingId + else: + Log.Error("[" + self.AGENT_NAME + "] " + "Mapping file could not be requested") + return None \ No newline at end of file diff --git a/Contents/Code/utils/jikan.py b/Contents/Code/utils/jikan.py new file mode 100644 index 0000000..4b57aae --- /dev/null +++ b/Contents/Code/utils/jikan.py @@ -0,0 +1,364 @@ +from common import CommonUtils +import re + +class JikanApiUtils: + + API_MAIN = "https://jikan.fribbtastic.net/v3" + API_DETAILS = "/anime/{id}" + API_SEARCH = "/search/anime?q={title}" + API_EPISODES = API_DETAILS + "/episodes/{page}" + API_STAFF = API_DETAILS + "/characters_staff" + API_PICTURES = API_DETAILS + "/pictures" + API_PERSON = "/person/{id}/pictures" + API_CHARACTER = "/character/{id}/pictures" + + COMMON_UTILS = None + AGENT_NAME = None + + def __init__(self): + self.COMMON_UTILS = CommonUtils() + self.AGENT_NAME = self.COMMON_UTILS.getAgentName() + return + + ''' + search for the title on the Jikan API + ''' + def search(self, title, results, lang): + + # match the search title for a specific myanimelist-id + manualId = self.COMMON_UTILS.getRegExMatch("^myanimelist-id:([0-9]+)$", str(title), 1) + manualMatch = False + searchUrl = None + + # manual match with a myanimelist ID or general search for the title + if manualId: + + Log.Info("[" + self.AGENT_NAME + "] " + "Searching on Jikan for ID: '" + str(manualId) + "'") + + manualMatch = True + searchUrl = self.API_MAIN + self.API_DETAILS.format(id=str(manualId)) + else: + Log.Info("[" + self.AGENT_NAME + "] " + "Searching on Jikan for name: '" + str(title) + "'") + + searchUrl = self.API_MAIN + self.API_SEARCH.format(title=String.Quote(title, usePlus=True)) + + searchResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(searchUrl)) + + # manual match request the details page and have a different data Structure + if manualMatch: + Log.Debug("[" + self.AGENT_NAME + "] " + "Parsing match with myanimelist ID") + + apiMal_id = str(self.COMMON_UTILS.getJsonValue("mal_id", searchResult)) + apiTitle = str(self.COMMON_UTILS.getJsonValue("title", searchResult)) + apiAired = str(self.COMMON_UTILS.getYear("from", searchResult["aired"])) + matchScore = 100 + + Log.Debug("[" + self.AGENT_NAME + "] " + "ID=" + str(apiMal_id) + " Title='" + str(apiTitle) + "' Year=" + str(apiAired) + " MatchScore=" + str(matchScore)) + + results.Append(MetadataSearchResult(id=apiMal_id, name=apiTitle, year=apiAired, score=matchScore, lang=lang)) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Parsing search results") + resultsArray = searchResult["results"] + + Log.Info("[" + self.AGENT_NAME + "] " + str(len(resultsArray)) + " Results found") + + for show in resultsArray: + apiMal_id = str(self.COMMON_UTILS.getJsonValue("mal_id", show)) + apiTitle = str(self.COMMON_UTILS.getJsonValue("title", show)) + apiAired = str(self.COMMON_UTILS.getYear("start_date", show)) + matchScore = self.COMMON_UTILS.calcMatchScore(title, apiTitle) + + Log.Debug("[" + self.AGENT_NAME + "] " + "ID=" + str(apiMal_id) + " Title='" + str(apiTitle) + "' Year=" + str(apiAired) + " MatchScore=" + str(matchScore)) + + results.Append(MetadataSearchResult(id=apiMal_id, name=apiTitle, year=apiAired, score=matchScore, lang=lang)) + + return + + ''' + get the metadata details for the specific MyAnimeList ID from the Jikan API + ''' + def getDetails(self, metadata): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting detailed Information from Jikan") + + detailsUrl = self.API_MAIN + self.API_DETAILS.format(id=metadata.id) + detailResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(detailsUrl)) + + if detailResult is not None: + + # get the MyAnimeList ID from the JSON response and add it to the metadata + apiId = self.COMMON_UTILS.getJsonValue("mal_id", detailResult) + if apiId is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "ID: " + str(apiId)) + metadata.id = str(apiId) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "ID was not available ") + + # get the Title that the user desires from the JSON response and add it to the metadata, if that title isn't available then default to the main + preferredTitle = str(Prefs["preferredTitle"]) + titleLanguage = None + + if preferredTitle == "Japanese": + titleLanguage = "title_japanese" + elif preferredTitle == "English": + titleLanguage = "title_english" + else: + titleLanguage = "title" + + apiTitle = self.COMMON_UTILS.getJsonValue(titleLanguage, detailResult) + if apiTitle is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "Title (" + preferredTitle + "): " + str(apiTitle)) + metadata.title = str(apiTitle) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Title was not available ") + + # get the summary from the JSON response and add it to the metadata + apiSummary = self.COMMON_UTILS.getJsonValue("synopsis", detailResult) + if apiSummary is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "Summary: " + str(apiSummary)) + metadata.summary = str(apiSummary) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Summary was not available ") + + # get the rating from the JSON response and add it to the metadata + apiRating = self.COMMON_UTILS.getJsonValue("score", detailResult) + if apiRating is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "Rating: " + str(apiRating)) + metadata.rating = apiRating + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Rating was not available ") + + # get the year when it originally aired from the JSON response and add it to the metadata + apiYear = self.COMMON_UTILS.getDate("from", detailResult["aired"]) + if apiYear is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "Year: " + str(apiYear)) + metadata.originally_available_at = apiYear + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Year was not available ") + + # get the content rating from the JSON response and add it to the metadata + apiContentRating = self.COMMON_UTILS.getJsonValue("rating", detailResult) + if apiContentRating is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "Content Rating: " + str(apiContentRating)) + metadata.content_rating = str(apiContentRating) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Content Rating was not available ") + + # get the main poster from the JSON response and add it to the metadata + apiMainPoster = self.COMMON_UTILS.getJsonValue("image_url", detailResult) + if apiMainPoster is not None: + Log.Debug("[" + self.AGENT_NAME + "] " + "Main Poster: " + str(apiMainPoster)) + if metadata.posters[str(apiMainPoster)] is None: + imageContent = self.COMMON_UTILS.getResponse(str(apiMainPoster)) + + metadata.posters[str(apiMainPoster)] = Proxy.Media(imageContent) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Image is already present (" + str(apiMainPoster) + ")") + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Main Poster was not available ") + + # get the duration from the JSON response, parse it, get milliseconds and add it to the metadata + apiDuration = self.COMMON_UTILS.getJsonValue("duration", detailResult) + if apiDuration is not None: + duration = int(self.COMMON_UTILS.getRegExMatch("^(\d*)", str(apiDuration), 1)) * 60000 + Log.Debug("[" + self.AGENT_NAME + "] " + "Duration: " + str(duration)) + metadata.duration = int(duration) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Duration was not available ") + + # get the genres from the JSON response and add it to the metadata + apiGenres = self.COMMON_UTILS.getJsonValue("genres", detailResult) + if apiGenres is not None: + genresArray = self.COMMON_UTILS.getArrayFromJsonValue("name", apiGenres) + Log.Debug("[" + self.AGENT_NAME + "] " + "Genres: " + str(genresArray)) + for genre in genresArray: + metadata.genres.add(str(genre)) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Genres were not available ") + + # get the producers from the JSON response and add it to the metadata + # Note: producers are only set on an individual episodes or a Movie, not on a show or season + #apiProducers= self.COMMON_UTILS.getJsonValue("producers", detailResult) + #if apiProducers is not None: + # producersArray = self.COMMON_UTILS.getArrayFromJsonValue("name", apiProducers) + # Log.Debug("[" + self.AGENT_NAME + "] " + "Producers: " + str(producersArray)) + #else: + # Log.Warn("[" + self.AGENT_NAME + "] " + "Producers were not available ") + + # get the studios from the JSON response and add it to the details dictionary + apiStudios = self.COMMON_UTILS.getJsonValue("studios", detailResult) + if apiStudios is not None: + studiosArray = self.COMMON_UTILS.getArrayFromJsonValue("name", apiStudios) + Log.Debug("[" + self.AGENT_NAME + "] " + "Studios: " + str(studiosArray)) + + metadata.studio = ', '.join(studiosArray) + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Studios were not available ") + + return + + ''' + get the episodes for a specific MyAnimeList ID + ''' + def getEpisodes(self, metadata): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting Episodes from Jikan") + + firstPage = 1 + + episodesUrl = self.API_MAIN + self.API_EPISODES.format(id=metadata.id,page=firstPage) + episodesResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(episodesUrl)) + + if episodesResult is not None: + maxPages = self.COMMON_UTILS.getJsonValue("episodes_last_page", episodesResult) + + # parse the first page and add them to the metadata + self.parseEpisodePage(metadata, firstPage, maxPages, episodesResult) + + # if there are more pages, parse them too and add them to the metadata + for currentPage in range(firstPage + 1, maxPages + 1): + + nextPageUrl = self.API_MAIN + self.API_EPISODES.format(id=metadata.id,page=currentPage) + nextPageResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(nextPageUrl)) + + if nextPageResult is not None: + self.parseEpisodePage(metadata, currentPage, maxPages, nextPageResult) + + else: + Log.Warn("[" + self.AGENT_NAME + "] " + "Episodes were not available") + + return + + ''' + parse the specific page of episodes and add the information to the metadata + ''' + def parseEpisodePage(self, metadata, current, max, page): + Log.Info("[" + self.AGENT_NAME + "] " + "Parsing Episodes page " + str(current) + "/" + str(max)) + + episodes = self.COMMON_UTILS.getJsonValue("episodes", page) + + for episode in episodes: + # get the episode number and title of the episode + # both the number and title are required on MyAnimeList and define that an episode even exist. + number = self.COMMON_UTILS.getJsonValue("episode_id", episode) + title = self.COMMON_UTILS.getJsonValue("title", episode) + + # get the aired date of the episode + # unlike the number and title, the aired date can be unavailable (null on the jikan API) + # plex needs a valid date so that the episode can be considered for "next episode" + try: + aired = self.COMMON_UTILS.getDate("aired", episode) + except: + aired = self.COMMON_UTILS.getNowDate() + + Log.Debug("[" + self.AGENT_NAME + "] Episode " + str(number) + ": " + str(title) + " - " + str(aired)) + + plexEpisode = metadata.seasons[1].episodes[int(number)] + plexEpisode.title = str(title) + plexEpisode.originally_available_at = aired + + return + + ''' + get the pictures of the anime + Note: Pictures on MyAnimeList can contain Pictures that could be either a Poster or a Background image + ''' + def getPictures(self, metadata): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting Pictures from Jikan") + + picturesUrl = self.API_MAIN + self.API_PICTURES.format(id=metadata.id) + picturesResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(picturesUrl)) + + if picturesResult is not None: + + picArr = self.COMMON_UTILS.getJsonValue("pictures", picturesResult) + pictures = self.COMMON_UTILS.getArrayFromJsonValue("large", picArr) + + for picture in pictures: + Log.Debug("[" + self.AGENT_NAME + "] " + "Poster: " + str(picture)) + + if metadata.posters[str(picture)] is None: + imageContent = self.COMMON_UTILS.getResponse(str(picture)) + + metadata.posters[str(picture)] = Proxy.Media(imageContent) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Image is already present (" + str(picture) + ")") + + return + + ''' + get the Character of the anime + Note: a single Character can have multiple Voice Actors + since there is not necessarily a distinction between those Voice Actors in terms of a different Character Picture + the method will return the first VA for that selected preferred language + ''' + def getCharacters(self, metadata): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting Staff information from Jikan") + + preferredVaLanguage = str(Prefs["actorLanguage"]) + preferredCharacterImage = str(Prefs["actorImage"]) + + staffUrl = self.API_MAIN + self.API_STAFF.format(id=metadata.id) + staffResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(staffUrl)) + + metadata.roles.clear() + + if staffResult is not None: + + charactersArr = self.COMMON_UTILS.getJsonValue("characters", staffResult) + + for character in charactersArr: + #Log.Debug("[" + self.AGENT_NAME + "] " + "Character: " + str(character)) + + charId = self.COMMON_UTILS.getJsonValue("mal_id", character) + charName = self.COMMON_UTILS.getJsonValue("name", character) + charImage = self.COMMON_UTILS.getJsonValue("image_url", character) + vaId = None + vaName = None + vaLanguage = None + vaImage = None + + voiceActors = self.COMMON_UTILS.getJsonValue("voice_actors", character) + + for voiceActor in voiceActors: + vaLang = self.COMMON_UTILS.getJsonValue("language", voiceActor) + + if vaLang == preferredVaLanguage: + vaId = self.COMMON_UTILS.getJsonValue("mal_id", voiceActor) + vaName = self.COMMON_UTILS.getJsonValue("name", voiceActor) + if preferredCharacterImage == "Voice Actor": + vaImage = self.getPersonImage(vaId) + vaLanguage = vaLang + break + + Log.Debug("[" + self.AGENT_NAME + "] " + + "Character: #" + str(charId) + " - " + + str(charName) + " - " + + str(charImage) + " - " + + str(vaId) + " - " + + str(vaName) + " - " + + str(vaLanguage) + " - " + + str(vaImage)) + + newRole = metadata.roles.new() + newRole.name = vaName + newRole.role = charName + if preferredCharacterImage == "Voice Actor": + newRole.photo = vaImage + else: + newRole.photo = charImage + + return + + ''' + get the image for a person + ''' + def getPersonImage(self, id): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting Person image from Jikan") + + personUrl = self.API_MAIN + self.API_PERSON.format(id=id) + personResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(personUrl)) + + if personResult is not None: + picArr = self.COMMON_UTILS.getJsonValue("pictures", personResult) + pictures = self.COMMON_UTILS.getArrayFromJsonValue("large", picArr) + + return pictures[0] \ No newline at end of file diff --git a/Contents/Code/utils/myanimelistagent.py b/Contents/Code/utils/myanimelistagent.py new file mode 100644 index 0000000..9491915 --- /dev/null +++ b/Contents/Code/utils/myanimelistagent.py @@ -0,0 +1,99 @@ +from common import CommonUtils +from jikan import JikanApiUtils +from thetvdb import TheTvDbUtils +from themoviedb import TheMovieDbUtils + +class MyAnimeListAgent: + + COMMON_UTILS = None + JIKAN_UTILS = None + AGENT_NAME = None + TVDB_UTILS = None + TMDB_UTILS = None + + ''' + [TODO] Implement metadata update feature from TheMovieDB + ''' + + ''' + Initialize the Utils + ''' + def __init__(self): + self.COMMON_UTILS = CommonUtils() + self.JIKAN_UTILS = JikanApiUtils() + self.AGENT_NAME = self.COMMON_UTILS.getAgentName() + self.TVDB_UTILS = TheTvDbUtils() + self.TMDB_UTILS = TheMovieDbUtils() + return + + ''' + Search for an Anime on the Jikan API and add the search results to the MetadataSearchResults + ''' + def search(self, results, media, lang, manual, type): + Log.Info("[" + self.AGENT_NAME + "] " + "Searching for Anime") + + # check the media type because the title is not in the same location + if type == "show": + title = self.COMMON_UTILS.removeAscii(media.show) + elif type == "movie": + title = self.COMMON_UTILS.removeAscii(media.name) + else: + Log.Error("[" + self.AGENT_NAME + "] " + "No type defined, don't know which name to pick") + + self.JIKAN_UTILS.search(title, results, lang) + + Log.Info("[" + self.AGENT_NAME + "] " + "Search finished") + return + + ''' + Update the metadata for a specific anime with metadata from Jikan, TheTVDB/TheMovieDB + ''' + def update(self, metadata, media, lang, force, type): + Log.Info("[" + self.AGENT_NAME + "] " + "Updating Anime " + type + " Metadata") + + # get the details of the Anime and add it to the metadata + self.JIKAN_UTILS.getDetails(metadata) + + # when it is a tvshow, get the episodes and add them to the metadata + if type == "show": + self.JIKAN_UTILS.getEpisodes(metadata) + + # get the Images of the Anime and add them to the metadata + self.JIKAN_UTILS.getPictures(metadata) + + # get the Staff of the Anime and add them to the metadata + self.JIKAN_UTILS.getCharacters(metadata) + + # request additional images from TheTVDB or TheMovieDB + movieImageSource = str(Prefs["movieImageSource"]) + tvshowImageSource = str(Prefs["tvshowImageSource"]) + + if type == "show": + tvdbEndpoint = "series" + tmdbEndpoint = "tv" + + if tvshowImageSource == "TheTVDB": + mappingId = self.COMMON_UTILS.getMapping(metadata.id, "thetvdb") + if mappingId is not None: + self.TVDB_UTILS.requestImages(metadata, mappingId, tvdbEndpoint) + elif tvshowImageSource == "TheMovieDB": + mappingId = self.COMMON_UTILS.getMapping(metadata.id, "themoviedb") + if mappingId is not None: + self.TMDB_UTILS.requestImages(metadata, mappingId, tmdbEndpoint) + + if type == "movie": + tvdbEndpoint = "movies" + tmdbEndpoint = "movie" + + if movieImageSource == "TheTVDB": + mappingId = self.COMMON_UTILS.getMapping(metadata.id, "thetvdb") + if mappingId is not None: + self.TVDB_UTILS.requestImages(metadata, mappingId, tvdbEndpoint) + elif movieImageSource == "TheMovieDB": + mappingId = self.COMMON_UTILS.getMapping(metadata.id, "themoviedb") + if mappingId is not None: + self.TMDB_UTILS.requestImages(metadata, mappingId, tmdbEndpoint) + + Log.Info("[" + self.AGENT_NAME + "] " + "Update finished") + return + \ No newline at end of file diff --git a/Contents/Code/utils/themoviedb.py b/Contents/Code/utils/themoviedb.py new file mode 100644 index 0000000..647074b --- /dev/null +++ b/Contents/Code/utils/themoviedb.py @@ -0,0 +1,108 @@ +from common import CommonUtils + +class TheMovieDbUtils(): + + API_MAIN = "https://api.themoviedb.org" + API_CONFIG = "/3/configuration?api_key={apiKey}" + API_IMAGES = "/3/{endpoint}/{id}/images?api_key={apiKey}" + + COMMON_UTILS = None + AGENT_NAME = None + + ''' + Initialize the Utils + ''' + def __init__(self): + self.COMMON_UTILS = CommonUtils() + self.AGENT_NAME = self.COMMON_UTILS.getAgentName() + return + + ''' + get the images from TheMovieDB API + ''' + def requestImages(self, metadata, id, endpoint): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting more Data from TheMovieDB") + + apiKey = Prefs["tmdbAPIKey"] + + if apiKey is None: + Log.Error("[" + self.AGENT_NAME + "] " + "TheMovieDB API Key was not available!") + return + + imageBaseUrl = self.getBaseUrl(apiKey) + + if imageBaseUrl is not None: + images = self.getImages(endpoint, id, apiKey) + if images is not None: + self.addImages(images, imageBaseUrl, metadata) + else: + Log.Error("[" + self.AGENT_NAME + "] " + "Error retrieving image base URL") + return None + + return + + ''' + get the Base URL for the Images + ''' + def getBaseUrl(self, apiKey): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting the Image Base URL") + + configUrl = self.API_MAIN + self.API_CONFIG.format(apiKey=apiKey) + configResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(url=configUrl)) + + if configResult is not None: + images = self.COMMON_UTILS.getJsonValue("images", configResult) + + if images is not None: + return self.COMMON_UTILS.getJsonValue("secure_base_url", images) + + return None + + ''' + get the images for a specific TheMovieDB ID + ''' + def getImages(self, endpoint, id, apiKey): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting Images for ID: " + str(id)) + + imagesUrl = self.API_MAIN + self.API_IMAGES.format(endpoint=endpoint, id=id, apiKey=apiKey) + imagesResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(url=imagesUrl)) + + if imagesResult is not None: + return imagesResult + + return None + + ''' + add the images to the metadata + ''' + def addImages(self, images, baseUrl, metadata): + + posterSize = Prefs["tmdbPosterSize"] + backgroundSize = Prefs["tmdbBackgroundSize"] + backdrops = self.COMMON_UTILS.getJsonValue("backdrops", images) + posters = self.COMMON_UTILS.getJsonValue("posters", images) + + # add all backdrops to the metadata (if they don't exist yet) + for backdrop in backdrops: + path = self.COMMON_UTILS.getJsonValue("file_path", backdrop) + + backdropUrl = baseUrl + backgroundSize + "/" + path + + if metadata.art[str(backdropUrl)] is None: + metadata.art[str(backdropUrl)] = Proxy.Media(HTTP.Request(str(backdropUrl), sleep=2.0).content) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Background is already present (" + str(backdropUrl) + ")") + + # add all poster to the metadata (if they don't exist yet) + for poster in posters: + path = self.COMMON_UTILS.getJsonValue("file_path", poster) + + posterUrl = baseUrl + posterSize + "/" + path + + if metadata.posters[str(posterUrl)] is None: + metadata.posters[str(posterUrl)] = Proxy.Media(HTTP.Request(str(posterUrl), sleep=2.0).content) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Poster is already present (" + str(posterUrl) + ")") + + + return \ No newline at end of file diff --git a/Contents/Code/utils/thetvdb.py b/Contents/Code/utils/thetvdb.py new file mode 100644 index 0000000..475d3fa --- /dev/null +++ b/Contents/Code/utils/thetvdb.py @@ -0,0 +1,156 @@ +from common import CommonUtils + +class TheTvDbUtils(): + + API_MAIN = "https://api4.thetvdb.com/v4" + API_LOGIN = "/login" + API_ARTWORKS_TYPE = "/artwork/types" + API_ARTWORKS = "/{endpoint}/{id}/extended" + + COMMON_UTILS = None + AGENT_NAME = None + + ''' + Initialize the Utils + ''' + def __init__(self): + self.COMMON_UTILS = CommonUtils() + self.AGENT_NAME = self.COMMON_UTILS.getAgentName() + return + + ''' + get the images from TheTVDB API + ''' + def requestImages(self, metadata, id, endpoint): + Log.Info("[" + self.AGENT_NAME + "] " + "Requesting more Data from TheTVDB") + + token = self.authenticate() + if token is not None: + artworkTypes = self.getArtworkTypes(token) + + if artworkTypes is not None: + artworks = self.getArtworks(id, token, endpoint) + + # iterate over every artwork of the show + for artwork in artworks: + artworkUrl = self.COMMON_UTILS.getJsonValue("image", artwork) + artworkType = self.COMMON_UTILS.getJsonValue("type", artwork) + + # iterate over each artwork type + for type in artworkTypes: + typeId = self.COMMON_UTILS.getJsonValue("id", type) + typeName = self.COMMON_UTILS.getJsonValue("name", type) + + # if the type of the artwork of the show equals the id of the type then we have found the correct type + if artworkType == typeId: + + self.addImage(typeName, artworkUrl, metadata) + return None + + ''' + check the response from TheTVDB for the status and return the data value + ''' + def getData(self, response): + + if response is not None: + status = self.COMMON_UTILS.getJsonValue("status", response) + + if status == "success": + Log.Info("[" + self.AGENT_NAME + "] " + "Status: " + str(status)) + + data = self.COMMON_UTILS.getJsonValue("data", response) + + return data + + else: + message = self.COMMON_UTILS.getJsonValue("message", loginResult) + Log.Error("[" + self.AGENT_NAME + "] " + "Status: '" + str(status) + "' Reason: " + str(message)) + + return None + + ''' + authenticate against the TheTVDB API to get a token + ''' + def authenticate(self): + Log.Info("[" + self.AGENT_NAME + "] " + "authenticating") + apiKey = str(Prefs["tvdbAPIKey"]) + apiPin = str(Prefs["tvdbAPIPIN"]) + token = None + + if len(apiKey) > 0 and len(apiPin) > 0: + data = JSON.StringFromObject(dict(apikey=apiKey, pin=apiPin)) + + loginUrl = self.API_MAIN + self.API_LOGIN + loginResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(url=loginUrl, data=data)) + + data = self.getData(loginResult) + + if data is not None: + return self.COMMON_UTILS.getJsonValue("token", data) + else: + Log.Error("[" + self.AGENT_NAME + "] " + "Could not Authenticate") + else: + Log.Error("[" + self.AGENT_NAME + "] " + "TheTVDB API Key and/or PIN were not available!") + + return None + + ''' + get the Artwork Types from TheTVDBD + ''' + def getArtworkTypes(self, token): + Log.Info("[" + self.AGENT_NAME + "] " + "Retrieving Artwork Types") + + artworkTypeUrl = self.API_MAIN + self.API_ARTWORKS_TYPE + artworkTypeResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(url=artworkTypeUrl, headers={'Authorization': 'Bearer %s' % token})) + + data = self.getData(artworkTypeResult) + + if data is not None: + return data + else: + Log.Error("[" + self.AGENT_NAME + "] " + "Could not retrieve Artwork types") + + return None + + ''' + get the Artworks for a specific Id from TheTVDB + ''' + def getArtworks(self, id, token, endpoint): + Log.Info("[" + self.AGENT_NAME + "] " + "Retrieving Artworks for ID: " + str(id)) + + artworkUrl = self.API_MAIN + self.API_ARTWORKS.format(id=id, endpoint=endpoint) + artworkResult = JSON.ObjectFromString(self.COMMON_UTILS.getResponse(url=artworkUrl, headers={'Authorization': 'Bearer %s' % token})) + + data = self.getData(artworkResult) + + if data is not None: + return self.COMMON_UTILS.getJsonValue("artworks", data) + else: + Log.Error("[" + self.AGENT_NAME + "] " + "Could not retrieve Artworks") + + return None + + ''' + add the image to the metadata + ''' + def addImage(self, name, url, metadata): + + if name == "Banner": + if metadata.banners[str(url)] is None: + metadata.banners[str(url)] = Proxy.Media(HTTP.Request(str(url), sleep=2.0).content) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Banner is already present (" + str(url) + ")") + + if name == "Poster": + if metadata.posters[str(url)] is None: + metadata.posters[str(url)] = Proxy.Media(HTTP.Request(str(url), sleep=2.0).content) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Poster is already present (" + str(url) + ")") + + if name == "Background": + if metadata.art[str(url)] is None: + metadata.art[str(url)] = Proxy.Media(HTTP.Request(str(url), sleep=2.0).content) + else: + Log.Debug("[" + self.AGENT_NAME + "] " + "Background is already present (" + str(url) + ")") + + return \ No newline at end of file diff --git a/Contents/DefaultPrefs.json b/Contents/DefaultPrefs.json index da476a0..0947625 100644 --- a/Contents/DefaultPrefs.json +++ b/Contents/DefaultPrefs.json @@ -1,41 +1,97 @@ [ { - "id": "getTheMovieDbImages", - "label": "Fetch Images for Movies from TheMovieDB: ", - "type": "bool", - "default": "true" + "id": "preferredTitle", + "label": "Preferred Title Language", + "type": "enum", + "values": [ + "main", + "Japanese", + "English" + ], + "default": "main" }, { - "id": "getTheTVDBImages", - "label": "Fetch Images for TV-Shows from TheTVDB: ", - "type": "bool", - "default": "true" + "id": "actorImage", + "label": "Preferred Staff Image", + "type": "enum", + "values": [ + "Character", + "Voice Actor" + ], + "default": "Character" }, { - "id": "theMovieDbBackgroundSize", - "label": "Download Background images from TheMovieDB with Size (w=width, h=height): ", + "id": "actorLanguage", + "label": "Preferred Staff Language", "type": "enum", "values": [ - "w300", + "Japanese", + "English" + ], + "default": "Japanese" + }, + { + "id": "tvshowImageSource", + "label": "Source for additional images in Shows", + "type": "enum", + "values": [ + "disabled", + "TheTVDB", + "TheMovieDB" + ], + "default": "disabled" + }, + { + "id": "movieImageSource", + "label": "Source for additional images for Movies", + "type": "enum", + "values": [ + "disabled", + "TheTVDB", + "TheMovieDB" + ], + "default": "disabled" + }, + { + "id": "tmdbAPIKey", + "label": "TheMovieDB - API Key", + "type": "text" + }, + { + "id": "tmdbPosterSize", + "label": "TheMovieDB - Poster image size (w=width, h=height)", + "type": "enum", + "values": [ + "original", "w780", - "w1280", - "original" + "w500", + "w342", + "w185", + "w154", + "w92" ], "default": "original" }, { - "id": "theMovieDbPosterSize", - "label": "Download Poster images from TheMovieDB with Size (w=width, h=height): ", + "id": "tmdbBackgroundSize", + "label": "TheMovieDB - Background image size (w=width, h=height)", "type": "enum", "values": [ - "w92", - "w154", - "w185", - "w342", - "w500", + "original", + "w1280", "w780", - "original" + "w300" ], "default": "original" + }, + { + "id": "tvdbAPIKey", + "label": "TheTVDB - API Key", + "type": "text" + }, + { + "id": "tvdbAPIPIN", + "label": "TheTVDB - API PIN", + "type": "text" } ] \ No newline at end of file diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..1209f69 --- /dev/null +++ b/VERSION @@ -0,0 +1,4 @@ +#Fri, 09 Apr 2021 18:50:16 +0200 +major.number=7 +minor.number=0 +build.number=0 diff --git a/build.xml b/build.xml new file mode 100644 index 0000000..3aa5ace --- /dev/null +++ b/build.xml @@ -0,0 +1,59 @@ + + + This is the Task to package only the necessary Agent resources into a zip file + and to increase revision, minor and major version and append it to the zip filename + + + + + + + + + Version: ${major.number}.${minor.number}.${build.number} + SHA1: ${sha1.number} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file