Skip to content

Commit

Permalink
Merge pull request #240 from meisnate12/develop
Browse files Browse the repository at this point in the history
v1.9.0
  • Loading branch information
meisnate12 authored May 16, 2021
2 parents 3550d17 + 9668c2f commit be23b4a
Show file tree
Hide file tree
Showing 23 changed files with 2,631 additions and 2,325 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Plex Meta Manager
#### Version 1.8.0
#### Version 1.9.0

The original concept for Plex Meta Manager is [Plex Auto Collections](https://github.com/mza921/Plex-Auto-Collections), but this is rewritten from the ground up to be able to include a scheduler, metadata edits, multiple libraries, and logging. Plex Meta Manager is a Python 3 script that can be continuously run using YAML configuration files to update on a schedule the metadata of the movies, shows, and collections in your libraries as well as automatically build collections based on various methods all detailed in the wiki. Some collection examples that the script can automatically build and update daily include Plex Based Searches like actor, genre, or studio collections or Collections based on TMDb, IMDb, Trakt, TVDb, AniDB, or MyAnimeList lists and various other services.

Expand Down
13 changes: 13 additions & 0 deletions config/config.yml.template
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,21 @@

libraries: # Library mappings must have a colon (:) placed after them
Movies:
metadata_path:
- file: config/Movies.yml # You have to create this file the other are online
- git: meisnate12/MovieCharts
- git: meisnate12/Studios
- git: meisnate12/IMDBGenres
- git: meisnate12/People
TV Shows:
metadata_path:
- file: config/TV Shows.yml # You have to create this file the other are online
- git: meisnate12/ShowCharts
- git: meisnate12/Networks
Anime:
metadata_path:
- file: config/Anime.yml # You have to create this file the other are online
- git: meisnate12/AnimeCharts
settings: # Can be individually specified per library as well
cache: true
cache_expiration: 60
Expand Down
46 changes: 20 additions & 26 deletions modules/anidb.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,56 +17,50 @@ def __init__(self, config):
"relation": "/relation/graph"
}

def get_AniDB_IDs(self):
return html.fromstring(requests.get("https://raw.githubusercontent.com/Anime-Lists/anime-lists/master/anime-list-master.xml").content)

@retry(stop_max_attempt_number=6, wait_fixed=10000)
def send_request(self, url, language):
def _request(self, url, language):
return html.fromstring(requests.get(url, headers={"Accept-Language": language, "User-Agent": "Mozilla/5.0 x64"}).content)

def get_popular(self, language):
response = self.send_request(self.urls["popular"], language)
def _popular(self, language):
response = self._request(self.urls["popular"], language)
return util.get_int_list(response.xpath("//td[@class='name anime']/a/@href"), "AniDB ID")

def validate_anidb_id(self, anidb_id, language):
response = self.send_request(f"{self.urls['anime']}/{anidb_id}", language)
def _relations(self, anidb_id, language):
response = self._request(f"{self.urls['anime']}/{anidb_id}{self.urls['relation']}", language)
return util.get_int_list(response.xpath("//area/@href"), "AniDB ID")

def _validate(self, anidb_id, language):
response = self._request(f"{self.urls['anime']}/{anidb_id}", language)
ids = response.xpath(f"//*[text()='a{anidb_id}']/text()")
if len(ids) > 0:
return util.regex_first_int(ids[0], "AniDB ID")
raise Failed(f"AniDB Error: AniDB ID: {anidb_id} not found")

def get_anidb_relations(self, anidb_id, language):
response = self.send_request(f"{self.urls['anime']}/{anidb_id}{self.urls['relation']}", language)
return util.get_int_list(response.xpath("//area/@href"), "AniDB ID")

def validate_anidb_list(self, anidb_list, language):
anidb_values = []
for anidb_id in anidb_list:
try:
anidb_values.append(self.validate_anidb_id(anidb_id, language))
anidb_values.append(self._validate(anidb_id, language))
except Failed as e:
logger.error(e)
if len(anidb_values) > 0:
return anidb_values
raise Failed(f"AniDB Error: No valid AniDB IDs in {anidb_list}")

def get_items(self, method, data, language, status_message=True):
def get_items(self, method, data, language):
pretty = util.pretty_names[method] if method in util.pretty_names else method
if status_message:
logger.debug(f"Data: {data}")
logger.debug(f"Data: {data}")
anidb_ids = []
if method == "anidb_popular":
if status_message:
logger.info(f"Processing {pretty}: {data} Anime")
anidb_ids.extend(self.get_popular(language)[:data])
logger.info(f"Processing {pretty}: {data} Anime")
anidb_ids.extend(self._popular(language)[:data])
else:
if status_message: logger.info(f"Processing {pretty}: {data}")
logger.info(f"Processing {pretty}: {data}")
if method == "anidb_id": anidb_ids.append(data)
elif method == "anidb_relation": anidb_ids.extend(self.get_anidb_relations(data, language))
elif method == "anidb_relation": anidb_ids.extend(self._relations(data, language))
else: raise Failed(f"AniDB Error: Method {method} not supported")
movie_ids, show_ids = self.config.Arms.anidb_to_ids(anidb_ids, language)
if status_message:
logger.debug(f"AniDB IDs Found: {anidb_ids}")
logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {show_ids}")
movie_ids, show_ids = self.config.Convert.anidb_to_ids(anidb_ids)
logger.debug(f"AniDB IDs Found: {anidb_ids}")
logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {show_ids}")
return movie_ids, show_ids
105 changes: 46 additions & 59 deletions modules/anilist.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,26 +19,21 @@
"score": "Average Score",
"popular": "Popularity"
}
tag_query = "query{MediaTagCollection {name}}"
genre_query = "query{GenreCollection}"

class AniListAPI:
def __init__(self, config):
self.config = config
self.url = "https://graphql.anilist.co"
self.tags = {}
self.genres = {}

for tag in self.send_request("query{MediaTagCollection {name}}", {})["data"]["MediaTagCollection"]:
self.tags[tag["name"].lower()] = tag["name"]
for genre in self.send_request("query{GenreCollection}", {})["data"]["GenreCollection"]:
self.genres[genre.lower()] = genre

@retry(stop_max_attempt_number=6, wait_fixed=10000)
def post(self, query, variables):
return requests.post(self.url, json={"query": query, "variables": variables})
self.tags = {t["name"].lower(): t["name"] for t in self._request(tag_query, {})["data"]["MediaTagCollection"]}
self.genres = {g.lower(): g for g in self._request(genre_query, {})["data"]["GenreCollection"]}

@retry(stop_max_attempt_number=2, retry_on_exception=util.retry_if_not_failed)
def send_request(self, query, variables):
response = self.post(query, variables)
def _request(self, query, variables):
response = requests.post(self.url, json={"query": query, "variables": variables})
json_obj = response.json()
if "errors" in json_obj:
if json_obj['errors'][0]['message'] == "Too Many Requests.":
Expand All @@ -51,14 +46,14 @@ def send_request(self, query, variables):
time.sleep(0.4)
return json_obj

def anilist_id(self, anilist_id):
def _validate(self, anilist_id):
query = "query ($id: Int) {Media(id: $id) {id title{romaji english}}}"
media = self.send_request(query, {"id": anilist_id})["data"]["Media"]
media = self._request(query, {"id": anilist_id})["data"]["Media"]
if media["id"]:
return media["id"], media["title"]["english" if media["title"]["english"] else "romaji"]
raise Failed(f"AniList Error: No AniList ID found for {anilist_id}")

def get_pagenation(self, query, limit=0, variables=None):
def _pagenation(self, query, limit=0, variables=None):
anilist_ids = []
count = 0
page_num = 0
Expand All @@ -68,7 +63,7 @@ def get_pagenation(self, query, limit=0, variables=None):
while next_page:
page_num += 1
variables["page"] = page_num
json_obj = self.send_request(query, variables)
json_obj = self._request(query, variables)
next_page = json_obj["data"]["Page"]["pageInfo"]["hasNextPage"]
for media in json_obj["data"]["Page"]["media"]:
if media["id"]:
Expand All @@ -80,7 +75,7 @@ def get_pagenation(self, query, limit=0, variables=None):
break
return anilist_ids

def top_rated(self, limit):
def _top_rated(self, limit):
query = """
query ($page: Int) {
Page(page: $page) {
Expand All @@ -89,9 +84,9 @@ def top_rated(self, limit):
}
}
"""
return self.get_pagenation(query, limit=limit)
return self._pagenation(query, limit=limit)

def popular(self, limit):
def _popular(self, limit):
query = """
query ($page: Int) {
Page(page: $page) {
Expand All @@ -100,9 +95,9 @@ def popular(self, limit):
}
}
"""
return self.get_pagenation(query, limit=limit)
return self._pagenation(query, limit=limit)

def season(self, season, year, sort, limit):
def _season(self, season, year, sort, limit):
query = """
query ($page: Int, $season: MediaSeason, $year: Int, $sort: [MediaSort]) {
Page(page: $page){
Expand All @@ -112,9 +107,9 @@ def season(self, season, year, sort, limit):
}
"""
variables = {"season": season.upper(), "year": year, "sort": "SCORE_DESC" if sort == "score" else "POPULARITY_DESC"}
return self.get_pagenation(query, limit=limit, variables=variables)
return self._pagenation(query, limit=limit, variables=variables)

def genre(self, genre, sort, limit):
def _genre(self, genre, sort, limit):
query = """
query ($page: Int, $genre: String, $sort: [MediaSort]) {
Page(page: $page){
Expand All @@ -124,9 +119,9 @@ def genre(self, genre, sort, limit):
}
"""
variables = {"genre": genre, "sort": "SCORE_DESC" if sort == "score" else "POPULARITY_DESC"}
return self.get_pagenation(query, limit=limit, variables=variables)
return self._pagenation(query, limit=limit, variables=variables)

def tag(self, tag, sort, limit):
def _tag(self, tag, sort, limit):
query = """
query ($page: Int, $tag: String, $sort: [MediaSort]) {
Page(page: $page){
Expand All @@ -136,9 +131,9 @@ def tag(self, tag, sort, limit):
}
"""
variables = {"tag": tag, "sort": "SCORE_DESC" if sort == "score" else "POPULARITY_DESC"}
return self.get_pagenation(query, limit=limit, variables=variables)
return self._pagenation(query, limit=limit, variables=variables)

def studio(self, studio_id):
def _studio(self, studio_id):
query = """
query ($page: Int, $id: Int) {
Studio(id: $id) {
Expand All @@ -156,7 +151,7 @@ def studio(self, studio_id):
name = None
while next_page:
page_num += 1
json_obj = self.send_request(query, {"id": studio_id, "page": page_num})
json_obj = self._request(query, {"id": studio_id, "page": page_num})
if not name:
name = json_obj["data"]["Studio"]["name"]
next_page = json_obj["data"]["Studio"]["media"]["pageInfo"]["hasNextPage"]
Expand All @@ -165,7 +160,7 @@ def studio(self, studio_id):
anilist_ids.append(media["id"])
return anilist_ids, name

def relations(self, anilist_id, ignore_ids=None):
def _relations(self, anilist_id, ignore_ids=None):
query = """
query ($id: Int) {
Media(id: $id) {
Expand All @@ -182,9 +177,9 @@ def relations(self, anilist_id, ignore_ids=None):
name = ""
if not ignore_ids:
ignore_ids = [anilist_id]
anilist_id, name = self.anilist_id(anilist_id)
anilist_id, name = self._validate(anilist_id)
anilist_ids.append(anilist_id)
json_obj = self.send_request(query, {"id": anilist_id})
json_obj = self._request(query, {"id": anilist_id})
edges = [media["node"]["id"] for media in json_obj["data"]["Media"]["relations"]["edges"]
if media["relationType"] not in ["CHARACTER", "OTHER"] and media["node"]["type"] == "ANIME"]
for media in json_obj["data"]["Media"]["relations"]["nodes"]:
Expand All @@ -194,7 +189,7 @@ def relations(self, anilist_id, ignore_ids=None):
anilist_ids.append(media["id"])

for next_id in new_anilist_ids:
new_relation_ids, ignore_ids, _ = self.relations(next_id, ignore_ids=ignore_ids)
new_relation_ids, ignore_ids, _ = self._relations(next_id, ignore_ids=ignore_ids)
anilist_ids.extend(new_relation_ids)

return anilist_ids, ignore_ids, name
Expand All @@ -215,48 +210,40 @@ def validate_anilist_ids(self, anilist_ids, studio=False):
if studio: query = "query ($id: Int) {Studio(id: $id) {name}}"
else: query = "query ($id: Int) {Media(id: $id) {id}}"
try:
self.send_request(query, {"id": anilist_id})
self._request(query, {"id": anilist_id})
anilist_values.append(anilist_id)
except Failed as e: logger.error(e)
if len(anilist_values) > 0:
return anilist_values
raise Failed(f"AniList Error: No valid AniList IDs in {anilist_ids}")

def get_items(self, method, data, language, status_message=True):
if status_message:
logger.debug(f"Data: {data}")
def get_items(self, method, data):
logger.debug(f"Data: {data}")
pretty = util.pretty_names[method] if method in util.pretty_names else method
if method == "anilist_id":
anilist_id, name = self.anilist_id(data)
anilist_id, name = self._validate(data)
anilist_ids = [anilist_id]
if status_message:
logger.info(f"Processing {pretty}: ({data}) {name}")
logger.info(f"Processing {pretty}: ({data}) {name}")
elif method in ["anilist_popular", "anilist_top_rated"]:
anilist_ids = self.popular(data) if method == "anilist_popular" else self.top_rated(data)
if status_message:
logger.info(f"Processing {pretty}: {data} Anime")
anilist_ids = self._popular(data) if method == "anilist_popular" else self._top_rated(data)
logger.info(f"Processing {pretty}: {data} Anime")
elif method == "anilist_season":
anilist_ids = self.season(data["season"], data["year"], data["sort_by"], data["limit"])
if status_message:
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}")
anilist_ids = self._season(data["season"], data["year"], data["sort_by"], data["limit"])
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {pretty_names[data['sort_by']]}")
elif method == "anilist_genre":
anilist_ids = self.genre(data["genre"], data["sort_by"], data["limit"])
if status_message:
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {pretty_names[data['sort_by']]}")
anilist_ids = self._genre(data["genre"], data["sort_by"], data["limit"])
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Genre: {data['genre']} sorted by {pretty_names[data['sort_by']]}")
elif method == "anilist_tag":
anilist_ids = self.tag(data["tag"], data["sort_by"], data["limit"])
if status_message:
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {pretty_names[data['sort_by']]}")
anilist_ids = self._tag(data["tag"], data["sort_by"], data["limit"])
logger.info(f"Processing {pretty}: {data['limit'] if data['limit'] > 0 else 'All'} Anime from the Tag: {data['tag']} sorted by {pretty_names[data['sort_by']]}")
elif method in ["anilist_studio", "anilist_relations"]:
if method == "anilist_studio": anilist_ids, name = self.studio(data)
else: anilist_ids, _, name = self.relations(data)
if status_message:
logger.info(f"Processing {pretty}: ({data}) {name} ({len(anilist_ids)} Anime)")
if method == "anilist_studio": anilist_ids, name = self._studio(data)
else: anilist_ids, _, name = self._relations(data)
logger.info(f"Processing {pretty}: ({data}) {name} ({len(anilist_ids)} Anime)")
else:
raise Failed(f"AniList Error: Method {method} not supported")
movie_ids, show_ids = self.config.Arms.anilist_to_ids(anilist_ids, language)
if status_message:
logger.debug(f"AniList IDs Found: {anilist_ids}")
logger.debug(f"Shows Found: {show_ids}")
logger.debug(f"Movies Found: {movie_ids}")
movie_ids, show_ids = self.config.Convert.anilist_to_ids(anilist_ids)
logger.debug(f"AniList IDs Found: {anilist_ids}")
logger.debug(f"Shows Found: {show_ids}")
logger.debug(f"Movies Found: {movie_ids}")
return movie_ids, show_ids
Loading

0 comments on commit be23b4a

Please sign in to comment.