Merge pull request #103 from meisnate12/develop

v1.5.0
pull/144/head v1.5.0
meisnate12 4 years ago committed by GitHub
commit 95c711b325
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -1,5 +1,5 @@
# Plex Meta Manager
#### Version 1.4.0
#### Version 1.5.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.

@ -1,5 +1,3 @@
## This file is a template remove the .template to use the file
templates:
Chart Alpha:
sort_title: ++++_<<num>><<collection_name>>
@ -16,14 +14,12 @@ templates:
summary: Rotten Tomatoes Best Movies of <<year>>
collection_order: release
Studio:
optional:
- company
tmdb_company: <<company>>
sort_title: +++_<<collection_name>>
sync_mode: sync
collection_order: alpha
Studio Alpha:
sort_title: +++_<<collection_name>>
sync_mode: sync
collection_order: alpha
IMDb Genre:
default:
title: feature
@ -88,17 +84,13 @@ templates:
sync_mode: sync
collection_order: release
Collection:
tmdb_collection_details: <<collection>>
sync_mode: sync
collection_order: release
Collection Movie:
optional:
- collection
- movie
tmdb_collection_details: <<collection>>
tmdb_movie: <<movie>>
sync_mode: sync
collection_order: release
Other Collection:
sync_mode: sync
collection_order: release
collections:
@ -187,11 +179,11 @@ collections:
template: {name: Studio, company: 25120}
summary: The Warner Animation Group (WAG) is an American animation studio that is the feature animation division of Warner Bros. Entertainment. Established on January 7, 2013, the studio is the successor to the dissolved 2D traditional hand-drawn animation studio Warner Bros. Feature Animation, which shut down in 2003 and the dissolved family film division Warner Bros. Family Entertainment, which shut down in 2009. The entity is also a sister animation studio of the regular animation studio Warner Bros. Animation
Walt Disney Animation Studios:
template: {name: Studio Alpha}
template: {name: Studio}
imdb_list: https://www.imdb.com/list/ls059383351/
summary: Walt Disney Animation Studios (WDAS), sometimes shortened to Disney Animation, is an American animation studio that creates animated features and short films for The Walt Disney Company. Founded on October 16, 1923 by brothers Walt Disney and Roy O. Disney, it is one of the oldest-running animation studios in the world. It is currently organized as a division of Walt Disney Studios and is headquartered at the Roy E. Disney Animation Building at the Walt Disney Studios lot in Burbank, California.
Walt Disney Pictures:
template: {name: Studio Alpha}
template: {name: Studio}
imdb_list: https://www.imdb.com/list/ls077114097/
summary: Walt Disney Pictures is an American film production studio of The Walt Disney Studios, which is owned by The Walt Disney Company. The studio is the flagship producer of live-action feature films within the Walt Disney Studios unit, and is based at the Walt Disney Studios in Burbank, California. Animated films produced by Walt Disney Animation Studios and Pixar Animation Studios are also released under this brand. Walt Disney Studios Motion Pictures distributes and markets the films produced by Walt Disney Pictures.
@ -280,7 +272,6 @@ collections:
summary: Romantic Comedy is a genre that attempts to catch the viewers heart with the combination of love and humor. This sub-genre is light-hearted and usually places the two protagonists in humorus situation. Romantic-Comedy film revolves around a romantic ideal, such as true love. In the end, the ideal triumphs over the situation or obstacle, thus creating a happy ending.
filters:
genre: Comedy
test: true
Romantic Drama:
template: {name: IMDb Genre, genre: "romance,drama", limit: 200}
summary: Romantic Drama film is a genre that explores the complex side of love. The plot usually centers around an obstacle that is preventing love between two people. The obstacles in Romantic Drama film can range from a family's disapproval, to forbidden love, to one's own psychological restraints. Many Romantic Dramas end with the lovers separating because of the enormity of the obstacle, the realization of incompatibility, or simply because of fate.
@ -440,7 +431,7 @@ collections:
template: {name: Actor, person: 17051}
Jamie Foxx:
template: {name: Actor, person: 134}
Jason Bateman::
Jason Bateman:
template: {name: Actor, person: 23532}
Jason Statham:
template: {name: Actor, person: 976}
@ -633,7 +624,7 @@ collections:
An American Tail:
template: {name: Collection, collection: 8783}
Anaconda:
template: {name: Collection Movie, collection: 105995, movie: 336560}
template: {name: Collection, collection: 105995, movie: 336560}
Anchorman:
template: {name: Collection, collection: 93791}
Angels in the ...:
@ -672,7 +663,7 @@ collections:
Bambi:
template: {name: Collection, collection: 87250}
Barbershop:
template: {name: Collection Movie, collection: 176097, movie: 14177}
template: {name: Collection, collection: 176097, movie: 14177}
Batman:
template: {name: Collection, collection: 120794}
Batman (Adam West) Animation:
@ -710,7 +701,7 @@ collections:
Cars:
template: {name: Collection, collection: 87118}
Charlie Brown:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls054850259/
summary: Collection of Movies and TV Specials with the beloved Peanuts characters.
Charlie's Angels:
@ -734,13 +725,13 @@ collections:
Cloudy with a Chance of Meatballs:
template: {name: Collection, collection: 177467}
Cloverfield:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls021933730/
summary: Cloverfield is an American science fiction anthology film series and media franchise created and produced by J. J. Abrams consisting of three films, viral marketing websites linking the films together, and a tie-in manga to the first film titled Cloverfield/Kishin (2008), all set in a shared fictional universe referred to as the "Cloververse". The franchise as a whole deals with creatures from other dimensions attacking Earth throughout various decades, all as a repercussion of an experiment by an astronaut team aboard the Cloverfield Station in outer-space. Each film depicts the reality-altering effects of their study, which was meant to find a new energy source replacing the planet's depleted resources, only to open portals for assault from various beasts from deep space.
The Conjuring:
template: {name: Collection, collection: 313086}
Cornetto Trilogy:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls068623110/
summary: An anthology series of British comedic genre films directed by Edgar Wright, written by Wright and Simon Pegg, produced by Nira Park, and starring Pegg and Nick Frost. The trilogy consists of Shaun of the Dead (2004), Hot Fuzz (2007), and The World's End (2013).
Creed:
@ -758,7 +749,7 @@ collections:
DC Super Hero Girls:
template: {name: Collection, collection: "477208, 557495"}
Deadpool:
template: {name: Collection Movie, collection: 448150, movie: 567604}
template: {name: Collection, collection: 448150, movie: 567604}
Death Note:
template: {name: Collection, collection: 102019}
Death Race:
@ -808,7 +799,7 @@ collections:
Final Destination:
template: {name: Collection, collection: 8864}
Final Fantasy:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls022264056/
summary: A collection of films based off or closely associated with the Final Fantasy video games.
Finding Nemo:
@ -822,19 +813,19 @@ collections:
Friday:
template: {name: Collection, collection: 43563}
Friday the 13th:
template: {name: Collection Movie, collection: 9735, movie: "6466, 222724"}
template: {name: Collection, collection: 9735, movie: "6466, 222724"}
Frozen:
template: {name: Collection Movie, collection: 386382, movie: "326359, 460793"}
template: {name: Collection, collection: 386382, movie: "326359, 460793"}
G.I. Joe:
template: {name: Collection, collection: 135468}
Garfield:
template: {name: Collection, collection: "86115, 373918"}
George Carlin Stand Up:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls070221411/
summary: Collection of George Carlin's Stand Up Comedy HBO Specials
George Lopez Stand Up:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls086584751/
summary: Collection of George Lopez's Stand Up Comedy Specials
George of the Jungle:
@ -844,13 +835,13 @@ collections:
Ghost Rider:
template: {name: Collection, collection: 90306}
Ghostbusters:
template: {name: Collection Movie, collection: 2980, movie: 43074}
template: {name: Collection, collection: 2980, movie: 43074}
The Girl - Millennium:
template: {name: Collection, collection: 575987}
The Godfather:
template: {name: Collection, collection: 230}
Godzilla (Showa):
template: {name: Collection Movie, collection: 374509, movie: 18983}
template: {name: Collection, collection: 374509, movie: 18983}
sort_title: Godzilla 01 (Showa)
Godzilla (Heisei):
template: {name: Collection, collection: 374511, movie: 39256}
@ -877,12 +868,12 @@ collections:
Halloween:
template: {name: Collection, collection: "91361, 126209"}
Halo:
template: {name: Other Collection}
template: {name: Collection}
tmdb_list_details: 7070832
The Hangover:
template: {name: Collection, collection: 86119}
Hannibal Lecter:
template: {name: Collection Movie, collection: 9743, movie: 11454}
template: {name: Collection, collection: 9743, movie: 11454}
Happy Death Day:
template: {name: Collection, collection: 526380}
Happy Feet:
@ -892,10 +883,10 @@ collections:
Harry Potter:
template: {name: Collection, collection: 1241}
... Has Fallen:
template: {name: Collection, collection: 508783}
template: {name: Collection, collection: 386534}
name_mapping: Has Fallen
Hellboy:
template: {name: Collection, collection: 508783}
template: {name: Collection, collection: 17235}
Hellboy (Animated):
template: {name: Collection, collection: 123203}
High School Musical:
@ -927,7 +918,7 @@ collections:
The Huntsman:
template: {name: Collection, collection: 393379}
Ice Age:
template: {name: Collection Movie, collection: 8354, movie: "79218, 717095, 387893"}
template: {name: Collection, collection: 8354, movie: "79218, 717095, 387893"}
The Incredibles:
template: {name: Collection, collection: 468222}
Independence Day:
@ -935,7 +926,7 @@ collections:
Indiana Jones:
template: {name: Collection, collection: 84}
Ip Man:
template: {name: Collection Movie, collection: 70068, movie: "658009, 643413, 450001, 751391, 44249, 182127, 44865"}
template: {name: Collection, collection: 70068, movie: "658009, 643413, 450001, 751391, 44249, 182127, 44865"}
collection_order: alpha
Iron Man:
template: {name: Collection, collection: 131292}
@ -948,7 +939,7 @@ collections:
Jay and Silent Bob:
template: {name: Collection, collection: 726870}
Jeff Dunham Stand Up:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls086022668/
summary: Collection of Jeff Dunham's Stand Up Comedy Specials
John Wick:
@ -962,11 +953,11 @@ collections:
The Jungle Book:
template: {name: Collection, collection: 97459}
Jurassic Park:
template: {name: Collection Movie, collection: 328, movie: 630322}
template: {name: Collection, collection: 328, movie: 630322}
The Karate Kid:
template: {name: Collection Movie, collection: 8580, movie: 38575}
template: {name: Collection, collection: 8580, movie: 38575}
Kevin Hart Stand Up:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls049792208/
summary: Collection of Kevin Hart's Stand Up Comedy Specials
Kick-Ass:
@ -1012,13 +1003,13 @@ collections:
Mall Cop:
template: {name: Collection, collection: 328372}
The Man with No Name:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls023916334/
summary: An Italian film series consisting of three Spaghetti Western films directed by Sergio Leone. The films are titled A Fistful of Dollars (1964), For a Few Dollars More (1965) and The Good, the Bad and the Ugly (1966). The series has become known for establishing the Spaghetti Western genre, and inspiring the creation of many more Spaghetti Western films. The three films are consistently listed among the best rated Western films in history.
Marvel Rising:
template: {name: Collection, collection: 627234}
Marx Brothers:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls068486735/
summary: The Marx Brothers were an American family comedy act that was successful in vaudeville, on Broadway, and in motion pictures from 1905 to 1949. Five of the Marx Brothers' thirteen feature films were selected by the American Film Institute (AFI) as among the top 100 comedy films, with two of them, Duck Soup (1933) and A Night at the Opera (1935), in the top fifteen. They are widely considered by critics, scholars, and fans to be among the greatest and most influential comedians of the 20th century.
Mary Poppins:
@ -1045,13 +1036,13 @@ collections:
Monsters, Inc.:
template: {name: Collection, collection: 137696}
Monty Python:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls072012494/
summary: Monty Python is a British surreal comedy troupe who created sketch comedy television shows and movies
Mortal Kombat:
template: {name: Collection Movie, collection: 9818, movie: 664767}
template: {name: Collection, collection: 9818, movie: 664767}
Mothra:
template: {name: Collection Movie, collection: 171732, movie: 39410}
template: {name: Collection, collection: 171732, movie: 39410}
Mulan:
template: {name: Collection, collection: 87236}
The Mummy:
@ -1067,7 +1058,7 @@ collections:
Night at the Museum:
template: {name: Collection, collection: 85943}
A Nightmare on Elm Street:
template: {name: Collection Movie, collection: 8581, movie: "6466, 23437"}
template: {name: Collection, collection: 8581, movie: "6466, 23437"}
Now You See Me:
template: {name: Collection, collection: 382685}
Ocean's:
@ -1075,7 +1066,7 @@ collections:
Ong Bak:
template: {name: Collection, collection: 94589}
Oz:
template: {name: Collection Movie, collection: 627517, movie: "13155, 68728"}
template: {name: Collection, collection: 627517, movie: "13155, 68728"}
Pacific Rim:
template: {name: Collection, collection: 363369}
Paddington:
@ -1085,7 +1076,7 @@ collections:
Percy Jackson:
template: {name: Collection, collection: 179919}
Pet Sematary:
template: {name: Collection Movie, collection: 10789, movie: 157433}
template: {name: Collection, collection: 10789, movie: 157433}
Peter Pan:
template: {name: Collection, collection: 55422}
Pirates of the Caribbean:
@ -1099,13 +1090,13 @@ collections:
Pocahontas:
template: {name: Collection, collection: 136214}
Pokémon:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls062687939/
summary: Pokémon is a media franchise created by video game designer Satoshi Tajiri that centers on fictional creatures called Pokémon. As of 2020, there have been 23 animated films and one live action film. The first nineteen animated films are based on the anime television series of the same name, with the original film being remade into the 22nd. The 20th, 21st and 23rd animated films are set in an alternate continuity to the anime.
Police Story:
template: {name: Collection, collection: 269098}
Power Rangers:
template: {name: Collection Movie, collection: 708816, movie: "305470, 306264"}
template: {name: Collection, collection: 708816, movie: "305470, 306264"}
Predator:
template: {name: Collection, collection: 399}
The Princess Diaries:
@ -1119,7 +1110,7 @@ collections:
Red Cliff:
template: {name: Collection, collection: 96677}
Rent:
template: {name: Other Collection}
template: {name: Collection}
tmdb_list_details: 7072241
The Rescuers:
template: {name: Collection, collection: 57971}
@ -1197,14 +1188,14 @@ collections:
template: {name: Collection, collection: 10}
name_mapping: Star Wars Skywalker Saga
"Star Wars: Legends":
template: {name: Other Collection}
template: {name: Collection}
tmdb_movie: 348350, 330459
summary: "Star Wars Anthology Films and other Star Wars Movies"
name_mapping: Star Wars Legends
Step Up:
template: {name: Collection, collection: 86092}
Street Fighter:
template: {name: Collection Movie, collection: 190435, movie: "687354, 11667"}
template: {name: Collection, collection: 190435, movie: "687354, 11667"}
Stuart Little:
template: {name: Collection, collection: 99727}
Super Troopers:
@ -1220,7 +1211,7 @@ collections:
Ted:
template: {name: Collection, collection: 266672}
Teenage Mutant Ninja Turtles:
template: {name: Collection Movie, collection: "1582, 401562", movie: 1273}
template: {name: Collection, collection: "1582, 401562", movie: 1273}
Tekken:
template: {name: Collection, collection: 294172}
The Terminator:
@ -1230,7 +1221,7 @@ collections:
Thor:
template: {name: Collection, collection: 131296}
The Three Stooges:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls075972675/
tmdb_movie: 76489
summary: The Three Stooges were an American vaudeville and comedy team active from 1922 until 1970, best known for their 190 short subject films by Columbia Pictures that have been regularly airing on television since 1958.
@ -1239,7 +1230,7 @@ collections:
Tokyo Ghoul:
template: {name: Collection, collection: 551278}
Tom and Jerry:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls022966050/
summary: Tom and Jerry's animated feature-length films based on the series.
Tomb Raider:
@ -1247,7 +1238,7 @@ collections:
Tomie:
template: {name: Collection, collection: 139394}
Toy Story:
template: {name: Collection Movie, collection: 10194, movie: 130925}
template: {name: Collection, collection: 10194, movie: 130925}
Trainspotting:
template: {name: Collection, collection: 424202}
Transformers:
@ -1259,9 +1250,9 @@ collections:
Trolls:
template: {name: Collection, collection: 489724}
TRON:
template: {name: Collection Movie, collection: 63043, movie: 73362}
template: {name: Collection, collection: 63043, movie: 73362}
Unbreakable:
template: {name: Other Collection}
template: {name: Collection}
imdb_list: https://www.imdb.com/list/ls022101006/
summary: The Unbreakable trilogy, officially known as the Eastrail 177 Trilogy, is an American superhero thriller and psychological horror film series. The films were written, produced, and directed by M. Night Shyamalan. The trilogy consists of Unbreakable (2000), Split (2016), and Glass (2019).
Underworld:
@ -1279,7 +1270,7 @@ collections:
Wreck-It Ralph:
template: {name: Collection, collection: 404825}
X-Men:
template: {name: Collection Movie, collection: "748, 453993, 448150", movie: 567604}
template: {name: Collection, collection: "748, 453993, 448150", movie: 567604}
xXx:
template: {name: Collection, collection: 52785}
Zenon:
@ -1302,374 +1293,4 @@ collections:
- "~"
sort_title: ~_Collectionless
collection_order: alpha
######################################################
# Metadata Edits #
######################################################
metadata:
The Adventures of Ichabod and Mr. Toad:
content_rating: G
All Monsters Attack:
content_rating: G
originally_available: 1969-12-08
The Amazing Panda Adventure:
content_rating: G
American Pie:
content_rating: R
American Psycho:
content_rating: R
American Wedding:
content_rating: R
Angels in the Endzone:
content_rating: PG-13
Appleseed (1997):
title: Appleseed
year: 1997
content_rating: R
Appleseed (2004):
title: Appleseed
year: 2004
content_rating: R
"Appleseed XIII: Ouranos":
content_rating: PG-13
"Appleseed XIII: Tartaros":
content_rating: PG-13
"Arthur & Merlin: Knights of Camelot":
content_rating: R
"Assassin's Creed: Lineage":
content_rating: R
The Assistant:
content_rating: R
Attack on Titan:
content_rating: R
"Attack on Titan II: End of the World":
content_rating: R
August Rush:
content_rating: PG
"Aura: Koga Maryuin's Last War":
content_rating: R
Bad Education:
content_rating: R
Bad Grandpa:
alt_title: "Jackass Presents: Bad Grandpa"
Batman & Bill:
content_rating: PG-13
"Batman & Mr. Freeze: SubZero":
content_rating: PG
Battle at Big Rock:
content_rating: PG-13
"Black Water: Abyss":
content_rating: R
Blade of the Phantom Master:
content_rating: R
Blue Story:
content_rating: R
Bon Voyage, Charlie Brown (and Don't Come Back!):
content_rating: G
A Boy Named Charlie Brown:
content_rating: G
Bright:
content_rating: PG
"Bring It On: All or Nothing":
content_rating: PG
Canada Russia '72:
content_rating: PG-13
Cast Away:
content_rating: PG-13
Castle in the Sky:
content_rating: PG
originally_available: 1986-04-01
Catwoman:
content_rating: PG-13
A Charlie Brown Christmas:
content_rating: G
A Charlie Brown Thanksgiving:
content_rating: G
A Christmas Carol:
content_rating: PG
"The Chronicles of Riddick: Dark Fury":
content_rating: R
"A Cinderella Story: If the Shoe Fits":
content_rating: PG
Class Action Park:
content_rating: PG-13
The Cocoanuts:
content_rating: PG
Color Out of Space:
content_rating: R
"Constantine: City of Demons":
content_rating: R
Coogan's Bluff:
content_rating: R
"Crystal Lake Memories: The Complete History of Friday the 13th":
content_rating: R
"DC Super Hero Girls: Legends of Atlantis":
content_rating: G
"DC Super Hero Girls: Intergalactic Games":
content_rating: G
"DC Super Hero Girls: Super Hero High":
content_rating: G
Death Note:
content_rating: R
Death Proof:
content_rating: R
Destroy All Monsters:
content_rating: G
originally_available: 1968-05-23
Duck Soup:
content_rating: PG
"Dungeons & Dragons: Wrath of the Dragon God":
content_rating: PG-13
Ebirah, Horror of the Deep:
content_rating: G
originally_available: 1966-01-01
"Evangelion: 2.0 You Can (Not) Advance":
content_rating: PG-13
"Evangelion: 3.0 You Can (Not) Redo":
content_rating: PG-13
A Fistful of Dollars:
content_rating: R
"The Flintstones & WWE: Stone Age SmackDown!":
content_rating: PG
From Up on Poppy Hill:
originally_available: 2011-11-16
Ghidorah, the Three-Headed Monster:
content_rating: G
originally_available: 1964-10-13
Ghost in the Shell:
content_rating: PG-13
Ghost in the Shell 2.0:
content_rating: PG-13
Godzilla (1954):
title: Godzilla
year: 1954
content_rating: R
Godzilla (1998):
title: Godzilla
year: 1998
sort_title: Godzilla 03
content_rating: PG-13
Shin Godzilla:
sort_title: Godzilla 06
content_rating: R
Godzilla 1985:
content_rating: PG
"Godzilla 2000: Millennium":
originally_available: 1999-08-18
Godzilla Against MechaGodzilla:
originally_available: 2002-03-23
Godzilla Raids Again:
content_rating: G
originally_available: 1955-05-21
Godzilla vs. Biollante:
content_rating: PG
Godzilla vs. Destoroyah:
content_rating: PG
originally_available: 1995-01-19
Godzilla vs. Gigan:
content_rating: G
originally_available: 1972-09-14
Godzilla vs. Hedorah:
content_rating: G
originally_available: 1971-04-01
Godzilla vs. King Ghidorah:
content_rating: PG
originally_available: 1991-04-28
Godzilla vs. Mechagodzilla:
content_rating: G
originally_available: 1974-03-24
Godzilla vs. Mechagodzilla II:
content_rating: PG
Godzilla vs. Megaguirus:
content_rating: PG
originally_available: 2000-08-31
Godzilla vs. Megalon:
content_rating: G
originally_available: 1973-03-17
Godzilla vs. Mothra:
content_rating: PG
originally_available: 1992-04-28
Godzilla vs. SpaceGodzilla:
content_rating: PG
originally_available: 1994-01-19
Godzilla, King of the Monsters!:
content_rating: G
"Godzilla, Mothra and King Ghidorah: Giant Monsters All-Out Attack":
content_rating: PG
originally_available: 2001-08-31
"Godzilla: Final Wars":
content_rating: PG
originally_available: 2004-12-13
"Godzilla: Tokyo S.O.S.":
originally_available: 2003-12-14
Halloween (Rob Zombie):
alt_title: Halloween
year: 2007
"Halo 4: Forward Unto Dawn":
alt_title: Halo 4 Forward Unto Dawn
tmdb_id: 56295
content_rating: R
Harold & Kumar Go to White Castle:
content_rating: R
"Hellboy Animated: Blood and Iron":
content_rating: PG
Holidays:
content_rating: R
Horse Feathers:
content_rating: PG
Howl's Moving Castle:
originally_available: 2004-06-10
Hulk Vs.:
content_rating: PG-13
"Ice Age: The Great Egg-Scapade":
content_rating: PG
Invasion of Astro-Monster:
content_rating: G
originally_available: 1965-07-29
Ip Man:
originally_available: 2008-10-01
Ip Man 2:
originally_available: 2010-01-28
Ip Man 3:
originally_available: 2015-01-22
"Iron Man & Captain America: Heroes United":
content_rating: PG-13
IT:
alt_title: It (1990)
tmdb_id: 19614
content_rating: PG-13
It's Christmastime Again, Charlie Brown:
content_rating: G
It's Magic, Charlie Brown:
content_rating: G
It's the Great Pumpkin, Charlie Brown:
content_rating: G
Kelly's Heroes:
content_rating: R
Kiki's Delivery Service:
originally_available: 1989-12-31
King Kong vs. Godzilla:
content_rating: G
originally_available: 1962-06-03
"The Last Sharknado: It's About Time":
content_rating: PG-13
"Lego Batman: The Movie - DC Super HeroesUnite":
content_rating: PG
"Lego DC Batman: Family Matters":
content_rating: PG
"Lego DC Comics Super Heroes: Justice League Attack of the Legion of Doom!":
content_rating: PG
"LEGO DC Comics Super Heroes: Justice League - Gotham City Breakout":
content_rating: PG
"LEGO DC Comics Super Heroes: Justice League vs. Bizarro League":
content_rating: PG
"LEGO DC Comics Super Heroes: Justice League: Cosmic Clash":
content_rating: PG
"Lego DC Comics Super Heroes: The Flash":
content_rating: PG
"LEGO DC Super Hero Girls: Brain Drain":
content_rating: G
"LEGO DC Super Hero Girls: Super-Villain High":
content_rating: G
"LEGO DC Super Heroes - Aquaman: Rage Of Atlantis":
content_rating: PG
"LEGO DC: Shazam! Magic and Monsters":
content_rating: PG
"Master Z: Ip Man Legacy":
originally_available: 2018-04-12
The Mayflower Voyagers:
content_rating: G
Monkey Business:
content_rating: PG
Mothra:
originally_available: 1961-05-10
Mothra vs. Godzilla:
content_rating: G
My Neighbor Totoro:
originally_available: 1988-12-11
My Neighbors the Yamadas:
originally_available: 1999-08-16
Nausicaä of the Valley of the Wind:
originally_available: 1984-06-13
Ocean Waves:
originally_available: 1993-04-18
Only Yesterday:
originally_available: 1991-08-01
"Parasyte: Part 1":
content_rating: R
Pirates of Silicon Valley:
content_rating: PG-13
Pom Poko:
originally_available: 1994-08-16
Ponyo:
originally_available: 2008-08-14
Princess Mononoke:
originally_available: 1997-10-29
Race for Your Life, Charlie Brown:
content_rating: G
Rebirth of Mothra:
originally_available: 1996-08-03
Rebirth of Mothra III:
originally_available: 1998-04-30
Red Cliff:
originally_available: 2008-07-02
The Red Turtle:
originally_available: 2016-01-20
Reign of the Supermen:
sort_title: Superman Reign of the
Rent:
content_rating: PG-13
"Rent: Filmed Live on Broadway":
content_rating: PG-13
The Return of Godzilla:
content_rating: PG
originally_available: 1984-09-13
The Return of the King:
content_rating: PG
"Riddick: Blindsided":
content_rating: R
The Secret World of Arrietty:
originally_available: 2010-02-17
Sharknado:
content_rating: PG-13
"Sharknado 2: The Second One":
content_rating: PG-13
"Sharknado 3: Oh Hell No!":
content_rating: PG-13
"Sharknado 4: The 4th Awakens":
content_rating: PG-13
"Sharknado 5: Global Swarming":
content_rating: PG-13
Snow White and the Three Stooges:
content_rating: PG
Son of Godzilla:
content_rating: G
originally_available: 1967-01-01
Spirited Away:
originally_available: 2001-06-16
"Star Wars: Clone Wars Volume Two":
originally_available: 2005-05-06
"Super Hero Girls: Hero of the Year":
content_rating: G
All Star Superman:
sort_title: Superman All Star
Sword of the Stranger:
content_rating: R
The Tale of the Princess Kaguya:
originally_available: 2013-10-17
Tales from Earthsea:
originally_available: 2006-08-13
Terror of Mechagodzilla:
content_rating: G
originally_available: 1975-03-24
The Three Stooges Go Around the World in a Daze:
content_rating: PG
The Three Stooges in Orbit:
content_rating: PG
Tokyo Ghoul:
content_rating: R
When Marnie Was There:
originally_available: 2014-05-22
The Wind Rises:
originally_available: 2013-02-21
collection_order: alpha

@ -0,0 +1,210 @@
import logging, requests, time
from modules import util
from modules.util import Failed
from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class AniListAPI:
def __init__(self, config):
self.config = config
self.url = "https://graphql.anilist.co"
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def post(self, query, variables):
return requests.post(self.url, json={"query": query, "variables": variables})
@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)
json_obj = response.json()
if "errors" in json_obj:
if json_obj['errors'][0]['message'] == "Too Many Requests.":
if "Retry-After" in response.headers:
time.sleep(int(response.headers["Retry-After"]))
raise ValueError
else:
raise Failed(f"AniList Error: {json_obj['errors'][0]['message']}")
else:
time.sleep(0.4)
return json_obj
def anilist_id(self, anilist_id):
query = "query ($id: Int) {Media(id: $id) {idMal title{romaji english}}}"
media = self.send_request(query, {"id": anilist_id})["data"]["Media"]
if media["idMal"]:
return media["idMal"], media["title"]["english" if media["title"]["english"] else "romaji"]
raise Failed(f"AniList Error: No MyAnimeList ID found for {anilist_id}")
def get_pagenation(self, query, limit=0, variables=None):
mal_ids = []
count = 0
page_num = 0
if variables is None:
variables = {"page": page_num}
else:
variables["page"] = page_num
next_page = True
while next_page:
page_num += 1
json_obj = self.send_request(query, variables)
next_page = json_obj["data"]["Page"]["pageInfo"]["hasNextPage"]
for media in json_obj["data"]["Page"]["media"]:
if media["idMal"]:
mal_ids.append(media["idMal"])
count += 1
if 0 < limit == count:
break
if 0 < limit == count:
break
return mal_ids
def top_rated(self, limit):
query = """
query ($page: Int) {
Page(page: $page) {
pageInfo {hasNextPage}
media(averageScore_greater: 3, sort: SCORE_DESC, type: ANIME) {idMal}
}
}
"""
return self.get_pagenation(query, limit=limit)
def popular(self, limit):
query = """
query ($page: Int) {
Page(page: $page) {
pageInfo {hasNextPage}
media(popularity_greater: 1000, sort: POPULARITY_DESC, type: ANIME) {idMal}
}
}
"""
return self.get_pagenation(query, limit=limit)
def season(self, season, year, sort, limit):
query = """
query ($page: Int, $season: String, $year: Int, $sort: String) {
Page(page: $page){
pageInfo {hasNextPage}
media(season: $season, seasonYear: $year, type: ANIME, sort: $sort){idMal}
}
}
"""
variables = {"season": season.upper(), "year": year, "sort": "SCORE_DESC" if sort == "score" else "POPULARITY_DESC"}
return self.get_pagenation(query, limit=limit, variables=variables)
def studio(self, studio_id):
query = """
query ($page: Int, $id: Int) {
Studio(id: $id) {
name
media(page: $page) {
nodes {idMal type}
pageInfo {hasNextPage}
}
}
}
"""
mal_ids = []
page_num = 0
next_page = True
name = None
while next_page:
page_num += 1
json_obj = self.send_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"]
for media in json_obj["data"]["Studio"]["media"]["nodes"]:
if media["idMal"] and media["type"] == "ANIME":
mal_ids.append(media["idMal"])
return mal_ids, name
def relations(self, anilist_id, ignore_ids=None):
query = """
query ($id: Int) {
Media(id: $id) {
idMal
relations {
edges {node{id idMal type} relationType}
nodes {id idMal type}
}
}
}
"""
anilist_ids = []
mal_ids = []
name = ""
if not ignore_ids:
ignore_ids = [anilist_id]
mal_id, name = self.anilist_id(anilist_id)
mal_ids.append(mal_id)
json_obj = self.send_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"]:
if media["idMal"] and media["id"] not in ignore_ids and media["id"] in edges and media["type"] == "ANIME":
anilist_ids.append(media["id"])
ignore_ids.append(media["id"])
mal_ids.append(media["idMal"])
for next_id in anilist_ids:
new_mal_ids, ignore_ids, _ = self.relations(next_id, ignore_ids=ignore_ids)
mal_ids.extend(new_mal_ids)
return mal_ids, ignore_ids, name
def validate_anilist_ids(self, anilist_ids, studio=False):
anilist_values = []
for anilist_id in anilist_ids:
if studio: query = "query ($id: Int) {Studio(id: $id) {name}}"
else: query = "query ($id: Int) {Media(id: $id) {idMal}}"
try:
self.send_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, status_message=True):
if status_message:
logger.debug(f"Data: {data}")
pretty = util.pretty_names[method] if method in util.pretty_names else method
if method == "anilist_id":
mal_id, name = self.anilist_id(data)
mal_ids = [mal_id]
if status_message:
logger.info(f"Processing {pretty}: ({data}) {name}")
elif method in ["anilist_popular", "anilist_top_rated"]:
mal_ids = self.popular(data) if method == "anilist_popular" else self.top_rated(data)
if status_message:
logger.info(f"Processing {pretty}: {data} Anime")
elif method == "anilist_season":
mal_ids = self.season(data["season"], data["year"], data["sort_by"], data["limit"])
if status_message:
logger.info(f"Processing {pretty}: {data['limit']} Anime from {util.pretty_seasons[data['season']]} {data['year']} sorted by {util.anilist_pretty[data['sort_by']]}")
elif method in ["anilist_studio", "anilist_relations"]:
if method == "anilist_studio": mal_ids, name = self.studio(data)
else: mal_ids, _, name = self.relations(data)
if status_message:
logger.info(f"Processing {pretty}: ({data}) {name} ({len(mal_ids)} Anime)")
else:
raise Failed(f"AniList Error: Method {method} not supported")
show_ids = []
movie_ids = []
for mal_id in mal_ids:
try:
ids = self.config.MyAnimeListIDList.find_mal_ids(mal_id)
if "thetvdb_id" in ids and int(ids["thetvdb_id"]) > 0: show_ids.append(int(ids["thetvdb_id"]))
elif "themoviedb_id" in ids and int(ids["themoviedb_id"]) > 0: movie_ids.append(int(ids["themoviedb_id"]))
else: raise Failed(f"MyAnimeList Error: MyAnimeList ID: {mal_id} has no other IDs associated with it")
except Failed as e:
if status_message:
logger.error(e)
if status_message:
logger.debug(f"MyAnimeList IDs Found: {mal_ids}")
logger.debug(f"Shows Found: {show_ids}")
logger.debug(f"Movies Found: {movie_ids}")
return movie_ids, show_ids

@ -307,7 +307,7 @@ class CollectionBuilder:
final_values.append(value)
self.methods.append(("plex_search", [[(method_name, final_values)]]))
elif method_name == "title":
self.methods.append(("plex_search", [[(method_name, data[m])]]))
self.methods.append(("plex_search", [[(method_name, util.get_list(data[m], split=False))]]))
elif method_name in util.plex_searches:
self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]]))
elif method_name == "plex_all":
@ -325,6 +325,8 @@ class CollectionBuilder:
self.methods.append((method_name, util.get_int_list(data[m], "MyAnimeList ID")))
elif method_name in ["anidb_id", "anidb_relation"]:
self.methods.append((method_name, config.AniDB.validate_anidb_list(util.get_int_list(data[m], "AniDB ID"), self.library.Plex.language)))
elif method_name in ["anilist_id", "anilist_relations", "anilist_studio"]:
self.methods.append((method_name, config.AniList.validate_anilist_ids(util.get_int_list(data[m], "AniList ID"), studio=method_name == "anilist_studio")))
elif method_name == "trakt_list":
self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m]))))
elif method_name == "trakt_list_details":
@ -430,7 +432,7 @@ class CollectionBuilder:
searches.append((search, util.get_int_list(data[m][s], util.remove_not(search))))
elif search == "title":
used.append(util.remove_not(search))
searches.append((search, data[m][s]))
searches.append((search, util.get_list(data[m][s], split=False)))
elif search in util.plex_searches:
used.append(util.remove_not(search))
searches.append((search, util.get_list(data[m][s])))
@ -542,13 +544,33 @@ class CollectionBuilder:
new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, maximum=1000)
self.methods.append((method_name, [new_dictionary]))
elif method_name == "anilist_season":
new_dictionary = {"sort_by": "score"}
if "sort_by" not in data[m]: logger.warning("Collection Warning: anilist_season sort_by attribute not found using score as default")
elif not data[m]["sort_by"]: logger.warning("Collection Warning: anilist_season sort_by attribute is blank using score as default")
elif data[m]["sort_by"] not in ["score", "popular"]: logger.warning(f"Collection Warning: anilist_season sort_by attribute {data[m]['sort_by']} invalid must be either 'score' or 'popular' using score as default")
else: new_dictionary["sort_by"] = data[m]["sort_by"]
if current_time.month in [12, 1, 2]: new_dictionary["season"] = "winter"
elif current_time.month in [3, 4, 5]: new_dictionary["season"] = "spring"
elif current_time.month in [6, 7, 8]: new_dictionary["season"] = "summer"
elif current_time.month in [9, 10, 11]: new_dictionary["season"] = "fall"
if "season" not in data[m]: logger.warning(f"Collection Warning: anilist_season season attribute not found using the current season: {new_dictionary['season']} as default")
elif not data[m]["season"]: logger.warning(f"Collection Warning: anilist_season season attribute is blank using the current season: {new_dictionary['season']} as default")
elif data[m]["season"] not in util.pretty_seasons: logger.warning(f"Collection Warning: anilist_season season attribute {data[m]['season']} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {new_dictionary['season']} as default")
else: new_dictionary["season"] = data[m]["season"]
new_dictionary["year"] = get_int(method_name, "year", data[m], current_time.year, minimum=1917, maximum=current_time.year + 1)
new_dictionary["limit"] = get_int(method_name, "limit", data[m], 0, maximum=500)
self.methods.append((method_name, [new_dictionary]))
else:
raise Failed(f"Collection Error: {m} attribute is not a dictionary: {data[m]}")
elif method_name in util.count_lists:
list_count = util.regex_first_int(data[m], "List Size", default=20)
list_count = util.regex_first_int(data[m], "List Size", default=10)
if list_count < 1:
logger.warning(f"Collection Warning: {method_name} must be an integer greater then 0 defaulting to 20")
list_count = 20
logger.warning(f"Collection Warning: {method_name} must be an integer greater then 0 defaulting to 10")
list_count = 10
self.methods.append((method_name, [list_count]))
elif "tvdb" in method_name:
values = util.get_list(data[m])
@ -655,14 +677,17 @@ class CollectionBuilder:
items_found += len(items)
elif method == "plex_search":
search_terms = {}
title_search = None
title_searches = None
has_processed = False
for search_method, search_data in value:
for search_method, search_list in value:
if search_method == "title":
title_search = search_data
logger.info(f"Processing {pretty}: title({title_search})")
ors = ""
for o, param in enumerate(search_list):
ors += f"{' OR ' if o > 0 else ''}{param}"
title_searches = search_list
logger.info(f"Processing {pretty}: title({ors})")
has_processed = True
break
for search_method, search_list in value:
if search_method != "title":
final_method = search_method[:-4] + "!" if search_method[-4:] == ".not" else search_method
@ -673,13 +698,15 @@ class CollectionBuilder:
for o, param in enumerate(search_list):
or_des = " OR " if o > 0 else f"{search_method}("
ors += f"{or_des}{param}"
if title_search or has_processed:
if has_processed:
logger.info(f"\t\t AND {ors})")
else:
logger.info(f"Processing {pretty}: {ors})")
has_processed = True
if title_search:
items = self.library.Plex.search(title_search, **search_terms)
if title_searches:
items = []
for title_search in title_searches:
items.extend(self.library.Plex.search(title_search, **search_terms))
else:
items = self.library.Plex.search(**search_terms)
items_found += len(items)
@ -715,6 +742,7 @@ class CollectionBuilder:
items = self.library.Tautulli.get_items(self.library, time_range=value["list_days"], stats_count=value["list_size"], list_type=value["list_type"], stats_count_buffer=value["list_buffer"])
items_found += len(items)
elif "anidb" in method: items_found += check_map(self.config.AniDB.get_items(method, value, self.library.Plex.language))
elif "anilist" in method: items_found += check_map(self.config.AniList.get_items(method, value))
elif "mal" in method: items_found += check_map(self.config.MyAnimeList.get_items(method, value))
elif "tvdb" in method: items_found += check_map(self.config.TVDb.get_items(method, value, self.library.Plex.language))
elif "imdb" in method: items_found += check_map(self.config.IMDb.get_items(method, value, self.library.Plex.language))

@ -36,6 +36,13 @@ class Cache:
expiration_date TEXT,
media_type TEXT)"""
)
cursor.execute(
"""CREATE TABLE IF NOT EXISTS letterboxd_map (
INTEGER PRIMARY KEY,
letterboxd_id TEXT UNIQUE,
tmdb_id TEXT,
expiration_date TEXT)"""
)
cursor.execute(
"""CREATE TABLE IF NOT EXISTS omdb_data (
INTEGER PRIMARY KEY,
@ -176,6 +183,29 @@ class Cache:
cursor.execute("INSERT OR IGNORE INTO imdb_map(imdb_id) VALUES(?)", (imdb_id,))
cursor.execute("UPDATE imdb_map SET t_id = ?, expiration_date = ?, media_type = ? WHERE imdb_id = ?", (t_id, expiration_date.strftime("%Y-%m-%d"), media_type, imdb_id))
def query_letterboxd_map(self, letterboxd_id):
tmdb_id = None
expired = None
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
cursor.execute("SELECT * FROM letterboxd_map WHERE letterboxd_id = ?", (letterboxd_id, ))
row = cursor.fetchone()
if row and row["tmdb_id"]:
datetime_object = datetime.strptime(row["expiration_date"], "%Y-%m-%d")
time_between_insertion = datetime.now() - datetime_object
tmdb_id = int(row["tmdb_id"])
expired = time_between_insertion.days > self.expiration
return tmdb_id, expired
def update_letterboxd(self, expired, letterboxd_id, tmdb_id):
expiration_date = datetime.now() if expired is True else (datetime.now() - timedelta(days=random.randint(1, self.expiration)))
with sqlite3.connect(self.cache_path) as connection:
connection.row_factory = sqlite3.Row
with closing(connection.cursor()) as cursor:
cursor.execute("INSERT OR IGNORE INTO letterboxd_map(letterboxd_id) VALUES(?)", (letterboxd_id,))
cursor.execute("UPDATE letterboxd_map SET tmdb_id = ?, expiration_date = ? WHERE letterboxd_id = ?", (tmdb_id, expiration_date.strftime("%Y-%m-%d"), letterboxd_id))
def query_omdb(self, imdb_id):
omdb_dict = {}
expired = None

@ -1,6 +1,7 @@
import logging, os, re, requests, time
from modules import util
from modules.anidb import AniDBAPI
from modules.anilist import AniListAPI
from modules.builder import CollectionBuilder
from modules.cache import Cache
from modules.imdb import IMDbAPI
@ -17,7 +18,6 @@ from modules.trakttv import TraktAPI
from modules.tvdb import TVDbAPI
from modules.util import Failed
from plexapi.exceptions import BadRequest
from plexapi.media import Guid
from ruamel import yaml
logger = logging.getLogger("Plex Meta Manager")
@ -227,7 +227,8 @@ class Config:
self.TVDb = TVDbAPI(self)
self.IMDb = IMDbAPI(self)
self.AniDB = AniDBAPI(self)
self.Letterboxd = LetterboxdAPI()
self.AniList = AniListAPI(self)
self.Letterboxd = LetterboxdAPI(self)
util.separator()
@ -324,7 +325,7 @@ class Config:
library = PlexAPI(params, self.TMDb, self.TVDb)
logger.info(f"{params['name']} Library Connection Successful")
except Failed as e:
util.print_multiline(e)
logger.error(e)
logger.info(f"{params['name']} Library Connection Failed")
continue
@ -690,8 +691,7 @@ class Config:
if url_parsed.scheme == "tmdb": tmdb_id = int(url_parsed.netloc)
elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc
elif item_type == "plex" and check_id == "show":
item.reload()
for guid_tag in item.findItems(item._data, Guid):
for guid_tag in item.guids:
url_parsed = requests.utils.urlparse(guid_tag.id)
if url_parsed.scheme == "tvdb": tvdb_id = int(url_parsed.netloc)
elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc
@ -791,13 +791,14 @@ class Config:
elif id_name: error_message = f"Configure TMDb or Trakt to covert {id_name} to {service_name}"
else: error_message = f"No ID to convert to {service_name}"
if self.Cache and ((tmdb_id and library.is_movie) or ((tvdb_id or ((anidb_id or mal_id) and tmdb_id)) and library.is_show)):
if isinstance(tmdb_id, list):
if not isinstance(tmdb_id, list): tmdb_id = [tmdb_id]
if not isinstance(imdb_id, list): imdb_id = [imdb_id]
for i in range(len(tmdb_id)):
util.print_end(length, f"Cache | {'^' if expired is True else '+'} | {item.guid:<46} | {tmdb_id[i] if tmdb_id[i] else 'None':<6} | {imdb_id[i] if imdb_id[i] else 'None':<10} | {tvdb_id if tvdb_id else 'None':<6} | {anidb_id if anidb_id else 'None':<5} | {mal_id if mal_id else 'None':<5} | {item.title}")
self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id[i], imdb_id[i], tvdb_id, anidb_id, mal_id, expired)
else:
util.print_end(length, f"Cache | {'^' if expired is True else '+'} | {item.guid:<46} | {tmdb_id if tmdb_id else 'None':<6} | {imdb_id if imdb_id else 'None':<10} | {tvdb_id if tvdb_id else 'None':<6} | {anidb_id if anidb_id else 'None':<5} | {mal_id if mal_id else 'None':<5} | {item.title}")
self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id, imdb_id, tvdb_id, anidb_id, mal_id, expired)
try: imdb_value = imdb_id[i]
except IndexError: imdb_value = None
util.print_end(length, f"Cache | {'^' if expired is True else '+'} | {item.guid:<46} | {tmdb_id[i] if tmdb_id[i] else 'None':<6} | {imdb_value if imdb_value else 'None':<10} | {tvdb_id if tvdb_id else 'None':<6} | {anidb_id if anidb_id else 'None':<5} | {mal_id if mal_id else 'None':<5} | {item.title}")
self.Cache.update_guid("movie" if library.is_movie else "show", item.guid, tmdb_id[i], imdb_value, tvdb_id, anidb_id, mal_id, expired)
if tmdb_id and library.is_movie: return "movie", tmdb_id
elif tvdb_id and library.is_show: return "show", tvdb_id
elif (anidb_id or mal_id) and tmdb_id: return "movie", tmdb_id

@ -1,4 +1,4 @@
import logging, math, re, requests
import logging, requests
from lxml import html
from modules import util
from modules.util import Failed
@ -7,52 +7,68 @@ from retrying import retry
logger = logging.getLogger("Plex Meta Manager")
class LetterboxdAPI:
def __init__(self):
def __init__(self, config):
self.config = config
self.url = "https://letterboxd.com"
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def send_request(self, url, language):
return html.fromstring(requests.get(url, header={"Accept-Language": language, "User-Agent": "Mozilla/5.0 x64"}).content)
return html.fromstring(requests.get(url, headers={"Accept-Language": language, "User-Agent": "Mozilla/5.0 x64"}).content)
def get_list_description(self, list_url, language):
descriptions = self.send_request(list_url, language).xpath("//meta[@property='og:description']/@content")
return descriptions[0] if len(descriptions) > 0 and len(descriptions[0]) > 0 else None
def parse_list_for_slugs(self, list_url, language):
def parse_list(self, list_url, language):
response = self.send_request(list_url, language)
slugs = response.xpath("//div[@class='poster film-poster really-lazy-load']/@data-film-slug")
letterboxd_ids = response.xpath("//div[@class='poster film-poster really-lazy-load']/@data-film-id")
items = []
for letterboxd_id in letterboxd_ids:
slugs = response.xpath(f"//div[@data-film-id='{letterboxd_id}']/@data-film-slug")
items.append((letterboxd_id, slugs[0]))
next_url = response.xpath("//a[@class='next']/@href")
if len(next_url) > 0:
slugs.extend(self.parse_list_for_slugs(f"{self.url}{next_url[0]}", language))
return slugs
items.extend(self.parse_list(f"{self.url}{next_url[0]}", language))
return items
def get_tmdb_from_slug(self, slug, language):
return self.get_tmdb(f"{self.url}{slug}", language)
def get_tmdb(self, letterboxd_url, language):
response = self.send_request(letterboxd_url, language)
ids = response.xpath("//body/@data-tmdb-id")
if len(ids) > 0:
return int(ids[0])
raise Failed(f"Letterboxd Error: TMDb ID not found at {letterboxd_url}")
ids = response.xpath("//a[@data-track-action='TMDb']/@href")
if len(ids) > 0 and ids[0]:
if "themoviedb.org/movie" in ids[0]:
return util.regex_first_int(ids[0], "TMDB Movie ID")
raise Failed(f"Letterboxd Error: TMDb Movie ID not found in {ids[0]}")
raise Failed(f"Letterboxd Error: TMDb Movie ID not found at {letterboxd_url}")
def get_items(self, method, data, language, status_message=True):
pretty = util.pretty_names[method] if method in util.pretty_names else method
movie_ids = []
if status_message:
logger.info(f"Processing {pretty}: {data}")
slugs = self.parse_list_for_slugs(data, language)
total_slugs = len(slugs)
if total_slugs == 0:
items = self.parse_list(data, language)
total_items = len(items)
if total_items == 0:
raise Failed(f"Letterboxd Error: No List Items found in {data}")
length = 0
for i, slug in enumerate(slugs, 1):
length = util.print_return(length, f"Finding TMDb ID {i}/{total_slugs}")
for i, item in enumerate(items, 1):
length = util.print_return(length, f"Finding TMDb ID {i}/{total_items}")
tmdb_id = None
expired = None
if self.config.Cache:
tmdb_id, expired = self.config.Cache.query_letterboxd_map(item[0])
if not tmdb_id or expired is not False:
try:
movie_ids.append(self.get_tmdb(slug, language))
tmdb_id = self.get_tmdb_from_slug(item[1], language)
except Failed as e:
logger.error(e)
util.print_end(length, f"Processed {total_slugs} TMDb IDs")
continue
if self.config.Cache:
self.config.Cache.update_letterboxd(expired, item[0], tmdb_id)
movie_ids.append(tmdb_id)
util.print_end(length, f"Processed {total_items} TMDb IDs")
if status_message:
logger.debug(f"TMDb IDs Found: {movie_ids}")
return movie_ids, []

@ -123,32 +123,29 @@ class MyAnimeListAPI:
if "error" in response: raise Failed(f"MyAnimeList Error: {response['error']}")
else: return response
def parse_mal_ids(self, data):
mal_ids = []
if "data" in data:
for d in data["data"]:
mal_ids.append(d["node"]["id"])
return mal_ids
def request_and_parse_mal_ids(self, url):
data = self.send_request(url)
return [d["node"]["id"] for d in data["data"]] if "data" in data else []
def get_username(self):
return self.send_request(f"{self.urls['user']}/@me")["name"]
def get_ranked(self, ranking_type, limit):
url = f"{self.urls['ranking']}?ranking_type={ranking_type}&limit={limit}"
return self.parse_mal_ids(self.send_request(url))
return self.request_and_parse_mal_ids(url)
def get_season(self, season, year, sort_by, limit):
url = f"{self.urls['season']}/{year}/{season}?sort={sort_by}&limit={limit}"
return self.parse_mal_ids(self.send_request(url))
return self.request_and_parse_mal_ids(url)
def get_suggestions(self, limit):
url = f"{self.urls['suggestions']}?limit={limit}"
return self.parse_mal_ids(self.send_request(url))
return self.request_and_parse_mal_ids(url)
def get_userlist(self, username, status, sort_by, limit):
final_status = "" if status == "all" else f"status={status}&"
url = f"{self.urls['user']}/{username}/animelist?{final_status}sort={sort_by}&limit={limit}"
return self.parse_mal_ids(self.send_request(url))
return self.request_and_parse_mal_ids(url)
def get_items(self, method, data, status_message=True):
if status_message:

@ -1,5 +1,4 @@
import logging, math, re, requests
from lxml import html
import logging, requests
from modules import util
from modules.util import Failed
from retrying import retry

@ -130,13 +130,18 @@ class PlexAPI:
length = util.print_return(length, f"Filtering {(' ' * (max_length - len(str(i)))) + str(i)}/{total} {current.title}")
for filter_method, filter_data in filters:
modifier = filter_method[-4:]
method = util.filter_alias[filter_method[:-4]] if modifier in [".not", ".lte", ".gte"] else util.filter_alias[filter_method]
if method == "max_age":
method = filter_method[:-4] if modifier in [".not", ".lte", ".gte"] else filter_method
if method in util.method_alias:
method_name = util.method_alias[method]
logger.warning(f"Collection Warning: {method} attribute will run as {method_name}")
else:
method_name = method
if method_name == "max_age":
threshold_date = datetime.now() - timedelta(days=filter_data)
if current.originallyAvailableAt is None or current.originallyAvailableAt < threshold_date:
match = False
break
elif method == "original_language":
elif method_name == "original_language":
movie = None
for key, value in movie_map.items():
if current.ratingKey == value:
@ -151,8 +156,24 @@ class PlexAPI:
if (modifier == ".not" and movie.original_language in filter_data) or (modifier != ".not" and movie.original_language not in filter_data):
match = False
break
elif method_name == "audio_track_title":
jailbreak = False
for media in current.media:
for part in media.parts:
for audio in part.audioStreams():
for check_title in filter_data:
title = audio.title if audio.title else ""
if check_title.lower() in title.lower():
jailbreak = True
break
if jailbreak: break
if jailbreak: break
if jailbreak: break
if (jailbreak and modifier == ".not") or (not jailbreak and modifier != ".not"):
match = False
break
elif modifier in [".gte", ".lte"]:
if method == "vote_count":
if method_name == "vote_count":
tmdb_item = None
for key, value in movie_map.items():
if current.ratingKey == value:
@ -166,20 +187,20 @@ class PlexAPI:
continue
attr = tmdb_item.vote_count
else:
attr = getattr(current, method) / 60000 if method == "duration" else getattr(current, method)
attr = getattr(current, method_name) / 60000 if method_name == "duration" else getattr(current, method_name)
if (modifier == ".lte" and attr > filter_data) or (modifier == ".gte" and attr < filter_data):
match = False
break
else:
attrs = []
if method in ["video_resolution", "audio_language", "subtitle_language"]:
if method_name in ["video_resolution", "audio_language", "subtitle_language"]:
for media in current.media:
if method == "video_resolution": attrs = [media.videoResolution]
if method_name == "video_resolution": attrs.extend([media.videoResolution])
for part in media.parts:
if method == "audio_language": attrs = ([a.language for a in part.audioStreams()])
if method == "subtitle_language": attrs = ([s.language for s in part.subtitleStreams()])
elif method in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]: attrs = [str(getattr(current, method))]
elif method in ["actors", "countries", "directors", "genres", "writers", "collections"]: attrs = [getattr(x, "tag") for x in getattr(current, method)]
if method_name == "audio_language": attrs.extend([a.language for a in part.audioStreams()])
if method_name == "subtitle_language": attrs.extend([s.language for s in part.subtitleStreams()])
elif method_name in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]: attrs = [str(getattr(current, method_name))]
elif method_name in ["actors", "countries", "directors", "genres", "writers", "collections"]: attrs = [getattr(x, "tag") for x in getattr(current, method_name)]
if (not list(set(filter_data) & set(attrs)) and modifier != ".not") or (list(set(filter_data) & set(attrs)) and modifier == ".not"):
match = False
@ -274,8 +295,7 @@ class PlexAPI:
add_edit("content_rating", item.contentRating, self.metadata[m], key="contentRating")
add_edit("original_title", item.originalTitle, self.metadata[m], key="originalTitle", value=original_title)
add_edit("studio", item.studio, self.metadata[m], value=studio)
item_tagline = item.tagline if self.is_movie else item._data.attrib.get("tagline")
add_edit("tagline", item_tagline, self.metadata[m], value=tagline)
add_edit("tagline", item.tagline, self.metadata[m], value=tagline)
add_edit("summary", item.summary, self.metadata[m], value=summary)
if len(edits) > 0:
logger.debug(f"Details Update: {edits}")

@ -13,6 +13,7 @@ logger = logging.getLogger("Plex Meta Manager")
class TraktAPI:
def __init__(self, params, authorization=None):
self.base_url = "https://api.trakt.tv"
self.redirect_uri = "urn:ietf:wg:oauth:2.0:oob"
self.aliases = {
"trakt_trending": "Trakt Trending",
@ -93,10 +94,6 @@ class TraktAPI:
return lookup.get_key(to_source) if to_source == "imdb" else int(lookup.get_key(to_source))
raise Failed(f"No {to_source.upper().replace('B', 'b')} ID found for {from_source.upper().replace('B', 'b')} ID {external_id}")
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def trending(self, amount, is_movie):
return Trakt["movies" if is_movie else "shows"].trending(per_page=amount)
@retry(stop_max_attempt_number=6, wait_fixed=10000, retry_on_exception=util.retry_if_not_failed)
def watchlist(self, data, is_movie):
items = Trakt[f"users/{data}/watchlist"].movies() if is_movie else Trakt[f"users/{data}/watchlist"].shows()
@ -110,6 +107,15 @@ class TraktAPI:
if trakt_list is None: raise Failed("Trakt Error: No List found")
else: return trakt_list
@retry(stop_max_attempt_number=6, wait_fixed=10000)
def send_request(self, url):
return requests.get(url, headers={"Content-Type": "application/json", "trakt-api-version": "2", "trakt-api-key": self.client_id}).json()
def get_pagenation(self, pagenation, amount, is_movie):
items = self.send_request(f"{self.base_url}/{'movies' if is_movie else 'shows'}/{pagenation}?limit={amount}")
if is_movie: return [item["ids"]["tmdb"] for item in items], []
else: return [], [item["ids"]["tvdb"] for item in items]
def validate_trakt_list(self, values):
trakt_values = []
for value in values:
@ -139,23 +145,24 @@ class TraktAPI:
logger.debug(f"Data: {data}")
pretty = self.aliases[method] if method in self.aliases else method
media_type = "Movie" if is_movie else "Show"
if method == "trakt_trending":
trakt_items = self.trending(int(data), is_movie)
if method in ["trakt_trending", "trakt_popular", "trakt_recommended", "trakt_watched", "trakt_collected"]:
movie_ids, show_ids = self.get_pagenation(method[6:], data, is_movie)
if status_message:
logger.info(f"Processing {pretty}: {data} {media_type}{'' if data == 1 else 's'}")
else:
show_ids = []
movie_ids = []
if method == "trakt_watchlist": trakt_items = self.watchlist(data, is_movie)
elif method == "trakt_list": trakt_items = self.standard_list(data).items()
else: raise Failed(f"Trakt Error: Method {method} not supported")
if status_message: logger.info(f"Processing {pretty}: {data}")
show_ids = []
movie_ids = []
for trakt_item in trakt_items:
if isinstance(trakt_item, Movie): movie_ids.append(int(trakt_item.get_key("tmdb")))
elif isinstance(trakt_item, Show) and trakt_item.pk[1] not in show_ids: show_ids.append(int(trakt_item.pk[1]))
elif (isinstance(trakt_item, (Season, Episode))) and trakt_item.show.pk[1] not in show_ids: show_ids.append(int(trakt_item.show.pk[1]))
if status_message:
logger.debug(f"Trakt {media_type} Found: {trakt_items}")
if status_message:
logger.debug(f"TMDb IDs Found: {movie_ids}")
logger.debug(f"TVDb IDs Found: {show_ids}")
return movie_ids, show_ids

@ -36,22 +36,14 @@ method_alias = {
}
filter_alias = {
"actor": "actors",
"audio_language": "audio_language",
"collection": "collections",
"content_rating": "contentRating",
"country": "countries",
"director": "directors",
"genre": "genres",
"max_age": "max_age",
"originally_available": "originallyAvailableAt",
"original_language": "original_language",
"rating": "rating",
"studio": "studio",
"subtitle_language": "subtitle_language",
"tmdb_vote_count": "vote_count",
"writer": "writers",
"video_resolution": "video_resolution",
"year": "year"
"writer": "writers"
}
days_alias = {
"monday": 0, "mon": 0, "m": 0,
@ -95,6 +87,12 @@ pretty_names = {
"anidb_id": "AniDB ID",
"anidb_relation": "AniDB Relation",
"anidb_popular": "AniDB Popular",
"anilist_id": "AniList ID",
"anilist_popular": "AniList Popular",
"anilist_relations": "AniList Relations",
"anilist_season": "AniList Season",
"anilist_studio": "AniList Studio",
"anilist_top_rated": "AniList Top Rated",
"imdb_list": "IMDb List",
"imdb_id": "IMDb ID",
"letterboxd_list": "Letterboxd List",
@ -145,9 +143,13 @@ pretty_names = {
"tmdb_trending_weekly": "TMDb Trending Weekly",
"tmdb_writer": "TMDb Writer",
"tmdb_writer_details": "TMDb Writer",
"trakt_collected": "Trakt Collected",
"trakt_list": "Trakt List",
"trakt_list_details": "Trakt List",
"trakt_popular": "Trakt Popular",
"trakt_recommended": "Trakt Recommended",
"trakt_trending": "Trakt Trending",
"trakt_watched": "Trakt Watched",
"trakt_watchlist": "Trakt Watchlist",
"tvdb_list": "TVDb List",
"tvdb_list_details": "TVDb List",
@ -206,6 +208,10 @@ mal_userlist_status = [
"dropped",
"plan_to_watch"
]
anilist_pretty = {
"score": "Average Score",
"popular": "Popularity"
}
pretty_ids = {
"anidbid": "AniDB",
"imdbid": "IMDb",
@ -218,6 +224,12 @@ all_lists = [
"anidb_id",
"anidb_relation",
"anidb_popular",
"anilist_id",
"anilist_popular",
"anilist_relations",
"anilist_season",
"anilist_studio",
"anilist_top_rated",
"imdb_list",
"imdb_id",
"letterboxd_list",
@ -266,9 +278,13 @@ all_lists = [
"tmdb_trending_weekly",
"tmdb_writer",
"tmdb_writer_details",
"trakt_collected",
"trakt_list",
"trakt_list_details",
"trakt_popular",
"trakt_recommended",
"trakt_trending",
"trakt_watched",
"trakt_watchlist",
"tvdb_list",
"tvdb_list_details",
@ -283,7 +299,7 @@ collectionless_lists = [
"collection_order", "plex_collectionless",
"url_poster", "tmdb_poster", "tmdb_profile", "file_poster",
"url_background", "file_background",
"name_mapping", "label", "label_sync_mode"
"name_mapping", "label", "label_sync_mode", "test"
]
other_attributes = [
"run_again",
@ -295,6 +311,7 @@ other_attributes = [
]
dictionary_lists = [
"filters",
"anilist_season",
"mal_season",
"mal_userlist",
"plex_collectionless",
@ -349,6 +366,8 @@ tmdb_searches = [
]
count_lists = [
"anidb_popular",
"anilist_popular",
"anilist_top_rated",
"mal_all",
"mal_airing",
"mal_upcoming",
@ -364,7 +383,11 @@ count_lists = [
"tmdb_now_playing",
"tmdb_trending_daily",
"tmdb_trending_weekly",
"trakt_trending"
"trakt_trending",
"trakt_popular",
"trakt_recommended",
"trakt_watched",
"trakt_collected"
]
tmdb_lists = [
"tmdb_actor",
@ -422,6 +445,7 @@ tmdb_type = {
all_filters = [
"actor", "actor.not",
"audio_language", "audio_language.not",
"audio_track_title", "audio_track_title.not",
"collection", "collection.not",
"content_rating", "content_rating.not",
"country", "country.not",
@ -441,6 +465,7 @@ all_filters = [
]
movie_only_filters = [
"audio_language", "audio_language.not",
"audio_track_title", "audio_track_title.not",
"country", "country.not",
"director", "director.not",
"duration.gte", "duration.lte",

@ -65,7 +65,7 @@ logger.info(util.get_centered_text("| |_) | |/ _ \\ \\/ / | |\\/| |/ _ \\ __/ _`
logger.info(util.get_centered_text("| __/| | __/> < | | | | __/ || (_| | | | | | (_| | | | | (_| | (_| | __/ | "))
logger.info(util.get_centered_text("|_| |_|\\___/_/\\_\\ |_| |_|\\___|\\__\\__,_| |_| |_|\\__,_|_| |_|\\__,_|\\__, |\\___|_| "))
logger.info(util.get_centered_text(" |___/ "))
logger.info(util.get_centered_text(" Version: 1.4.0 "))
logger.info(util.get_centered_text(" Version: 1.5.0 "))
util.separator()
if args.tests:

@ -1,6 +1,6 @@
# Remove
# Less common, pinned
PlexAPI==4.4.0
PlexAPI==4.4.1
tmdbv3api==1.7.5
trakt.py==4.2.0
# More common, flexible

Loading…
Cancel
Save