diff --git a/Dockerfile b/Dockerfile index 857f16bb7..6541b9039 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,10 +6,9 @@ RUN \ apt-get update && \ apt-get upgrade -y --no-install-recommends && \ apt-get install -y tzdata --no-install-recommends && \ + apt-get install -y gcc g++ libxml2-dev libxslt-dev libz-dev && \ echo "**** install python packages ****" && \ pip3 install --no-cache-dir --upgrade --requirement /requirements.txt && \ - echo "**** install Plex-Auto-Collections ****" && \ - chmod +x /plex_meta_manager.py && \ echo "**** cleanup ****" && \ apt-get autoremove -y && \ apt-get clean && \ diff --git a/README.md b/README.md index dd6930a50..cc13ce668 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ # Plex Meta Manager -#### Version 1.1.0 +#### Version 1.2.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. @@ -18,5 +18,5 @@ The script is designed to work with most Metadata agents including the new Plex * If you're getting an Error or have an Enhancement post in the [Issues](https://github.com/meisnate12/Plex-Meta-Manager/issues) * If you have a configuration question visit the [Discussions](https://github.com/meisnate12/Plex-Meta-Manager/discussions) * To see user submitted Metadata configuration files and you could even add your own go to the [Plex Meta Manager Configs](https://github.com/meisnate12/Plex-Meta-Manager-Configs) -* Pull Request are welcome +* Pull Request are welcome but please submit them to the develop branch * [Buy Me a Pizza](https://www.buymeacoffee.com/meisnate12) diff --git a/config/Movies.yml.template b/config/Movies.yml.template index f573d3541..d33113a4b 100644 --- a/config/Movies.yml.template +++ b/config/Movies.yml.template @@ -1,3 +1,105 @@ +## This file is a template remove the .template to use the file + +templates: + Chart Alpha: + sort_title: ++++_<><> + sync_mode: sync + collection_order: alpha + Chart Release: + sort_title: ++++_<> + sync_mode: sync + collection_order: release + Best of: + trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-<> + sort_title: ++++_Best of <> + sync_mode: sync + summary: Rotten Tomatoes Best Movies of <> + collection_order: release + Studio: + tmdb_company: <> + sort_title: +++_<> + sync_mode: sync + collection_order: alpha + Studio Alpha: + sort_title: +++_<> + sync_mode: sync + collection_order: alpha + IMDb Genre: + default: + title: feature + limit: 100 + imdb_list: + - url: https://www.imdb.com/search/title/?title_type=<>&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=<<genre>> + limit: <<limit>> + - url: https://www.imdb.com/search/title/?title_type=<<title>>&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=<<genre>>&sort=user_rating,desc + limit: <<limit>> + sort_title: ++_<<collection_name>> + sync_mode: sync + collection_order: alpha + Other Genre: + sort_title: ++_<<collection_name>> + sync_mode: sync + collection_order: alpha + Actor: + actor: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Actor Director: + actor: tmdb + director: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Actor Director Writer: + actor: tmdb + director: tmdb + writer: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Actor Writer: + actor: tmdb + writer: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Director: + director: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Director Writer: + director: tmdb + writer: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Writer: + writer: tmdb + tmdb_person: <<person>> + sort_title: +_<<collection_name>> + sync_mode: sync + collection_order: release + Collection: + tmdb_collection_details: <<collection>> + sync_mode: sync + collection_order: release + Collection Movie: + tmdb_collection_details: <<collection>> + tmdb_movie: <<movie>> + sync_mode: sync + collection_order: release + Other Collection: + sync_mode: sync + collection_order: release + collections: ###################################################### @@ -5,6 +107,7 @@ collections: ###################################################### Plex Popular: + template: {name: Chart Alpha, num: 1} tautulli_popular: list_days: 30 list_size: 20 @@ -13,2178 +116,1180 @@ collections: list_days: 30 list_size: 20 list_buffer: 20 - schedule: daily - sort_title: ++++_1Plex Popular - sync_mode: sync summary: Movies Popular on Plex - collection_order: alpha Trending: + template: {name: Chart Alpha, num: 2} trakt_trending: 40 tmdb_trending_daily: 40 tmdb_trending_weekly: 40 - sort_title: ++++_3Trending - sync_mode: sync summary: Movies Trending across the internet - collection_order: alpha Popular: + template: {name: Chart Alpha, num: 3} tmdb_popular: 40 imdb_list: url: https://www.imdb.com/search/title/?title_type=feature,tv_movie,documentary,short limit: 40 - sort_title: ++++_4Popular - sync_mode: sync summary: Popular Movies across the internet - collection_order: alpha Top Rated: + template: {name: Chart Alpha, num: 4} imdb_list: https://www.imdb.com/search/title/?groups=top_250&count=250 tmdb_top_rated: 250 - sort_title: ++++_5Top Rated - sync_mode: sync summary: Top Rated Movies across the internet - collection_order: alpha Best of 2014: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2014 - sort_title: ++++_Best of 2014 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2014 - collection_order: release + template: {name: Best of, year: 2014} Best of 2015: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2015 - sort_title: ++++_Best of 2015 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2015 - collection_order: release + template: {name: Best of, year: 2015} Best of 2016: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2016 - sort_title: ++++_Best of 2016 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2016 - collection_order: release + template: {name: Best of, year: 2016} Best of 2017: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2017 - sort_title: ++++_Best of 2017 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2017 - collection_order: release + template: {name: Best of, year: 2017} Best of 2018: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2018 - sort_title: ++++_Best of 2018 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2018 - collection_order: release + template: {name: Best of, year: 2018} Best of 2019: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2019 - sort_title: ++++_Best of 2019 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2019 - collection_order: release + template: {name: Best of, year: 2019} Best of 2020: - trakt_list: https://trakt.tv/users/lish408/lists/rotten-tomatoes-best-of-2020 - sort_title: ++++_Best of 2020 - sync_mode: sync - summary: Rotten Tomatoes Best Movies of 2020 - collection_order: release + template: {name: Best of, year: 2020} + Best of 2021: + template: {name: Best of, year: 2021} Oscars: + template: {name: Chart Release} imdb_list: https://www.imdb.com/search/title/?title_type=feature,documentary&groups=oscar_winner - sort_title: ++++_Oscars - sync_mode: sync summary: Oscar Winning Movies - collection_order: release - ###################################################### # Studio Collections # ###################################################### - Aardman: - tmdb_company: 297 - sort_title: +++_Aardman - sync_mode: sync + template: {name: Studio, company: 297} summary: Aardman Animations, Ltd. is a British animation studio based in Bristol, England. Aardman is known for films made using stop-motion clay animation techniques, particularly those featuring Plasticine characters Wallace and Gromit. Blue Sky: - tmdb_company: 9383 - sort_title: +++_Blue Sky - sync_mode: sync + template: {name: Studio, company: 9383} summary: Blue Sky Studios, Inc. is an American computer animation film studio based in Greenwich, Connecticut. It is a subsidiary of 20th Century Animation, a division of Disney Studios Content. DreamWorks: - tmdb_company: 521 + template: {name: Studio, company: 521} filters: collection.not: Aardman - sort_title: +++_DreamWorks - sync_mode: sync summary: DreamWorks Animation LLC (also simply known as DreamWorks) is an American animation studio and a subsidiary of Universal Pictures, itself a subsidiary of Comcast's NBCUniversal. It is based in Glendale, California, and produces animated feature films, television programs, and online virtual games. Illumination Entertainment: - tmdb_company: 6704 - sort_title: +++_Illumination Entertainment - sync_mode: sync + template: {name: Studio, company: 6704} summary: Illumination is an American film and animation studio founded by Chris Meledandri in 2007 and owned by Universal Pictures, a division of NBCUniversal, which is itself a division of Comcast. Meledandri produces the films, while Universal finances and distributes all the films. The studio is responsible for the Despicable Me and The Secret Life of Pets franchises and the film adaptations of Dr. Seuss’ books The Lorax and How the Grinch Stole Christmas. The Minions, characters from the Despicable Me series, are the studio's mascots. Pixar: - tmdb_company: 3 - sort_title: +++_Pixar - sync_mode: sync + template: {name: Studio, company: 3} summary: Pixar Animation Studios, commonly known as Pixar, is an American computer animation studio based in Emeryville, California, a subsidiary of Disney Studios Content owned by The Walt Disney Company. Pixar began in 1979 as part of the Lucasfilm computer division, known as the Graphics Group, before its spin-off as a corporation on February 3, 1986, with funding from Apple co-founder Steve Jobs, who became its majority shareholder. Disney purchased Pixar in 2006 at a valuation of $7.4 billion by converting each share of Pixar stock to 2.3 shares of Disney stock, a transaction that resulted in Jobs becoming Disney's largest single shareholder at the time. Pixar is best known for its feature films technologically powered by RenderMan, the company's own implementation of the industry-standard RenderMan Interface Specification image-rendering application programming interface. Luxo Jr., a desk lamp from the studio's 1986 short film of the same name, is the studio's mascot. Studio Ghibli: - tmdb_company: 10342 - sort_title: +++_Studio Ghibli - sync_mode: sync + template: {name: Studio, company: 10342} summary: Studio Ghibli Inc. is a Japanese animation film studio headquartered in Koganei, Tokyo. The studio is best known for its animated feature films, and has also produced several short films, television commercials, and one television film. The studio’s mascot and most recognizable symbol is the character, Totoro, a giant cat-like spirit from the 1988 classic, My Neighbor Totoro. Sony Pictures Animation: - tmdb_company: 2251 - sort_title: +++_Sony Pictures Animation - sync_mode: sync + template: {name: Studio, company: 2251} summary: Sony Pictures Animation Inc. is an American animation studio owned by Sony Entertainment's Sony Pictures Entertainment through their Motion Picture Group division and founded on May 9, 2002. The studio's films are distributed worldwide by Sony Pictures Releasing under their Columbia Pictures label, while all direct-to-video releases are released by Sony Pictures Home Entertainment. Warner Animation Group: - tmdb_company: 25120 - sort_title: +++_Warner Animation Group - sync_mode: sync + 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} imdb_list: https://www.imdb.com/list/ls059383351/ - sort_title: +++_Walt Disney Animation Studios - sync_mode: sync 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} imdb_list: https://www.imdb.com/list/ls077114097/ - sort_title: +++_Walt Disney Pictures - sync_mode: sync 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. - - - - Godzilla: - tmdb_collection: 374509, 374511, 374512, 535313 - tmdb_movie: 18983, 39256, 293167 - schedule: daily - - ###################################################### # Genre Collections # ###################################################### Action: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=action - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=action&sort=user_rating,desc - limit: 100 - sort_title: ++_Action - sync_mode: sync + template: {name: IMDb Genre, genre: action} summary: Action film is a genre wherein physical action takes precedence in the storytelling. The film will often have continuous motion and action including physical stunts, chases, fights, battles, and races. The story usually revolves around a hero that has a goal, but is facing incredible odds to obtain it. - collection_order: alpha Adventure: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=adventure - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=adventure&sort=user_rating,desc - limit: 100 - sort_title: ++_Adventure - sync_mode: sync + template: {name: IMDb Genre, genre: adventure} summary: Adventure film is a genre that revolves around the conquests and explorations of a protagonist. The purpose of the conquest can be to retrieve a person or treasure, but often the main focus is simply the pursuit of the unknown. These films generally take place in exotic locations and play on historical myths. Adventure films incorporate suspenseful puzzles and intricate obstacles that the protagonist must overcome in order to achieve the end goal. - collection_order: alpha Animation: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=animation - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=animation&sort=user_rating,desc - limit: 100 - sort_title: ++_Animation - sync_mode: sync + template: {name: IMDb Genre, genre: animation} summary: Animated film is a collection of illustrations that are photographed frame-by-frame and then played in a quick succession. Since its inception, animation has had a creative and imaginative tendency. Being able to bring animals and objects to life, this genre has catered towards fairy tales and children’s stories. However, animation has long been a genre enjoyed by all ages. As of recent, there has even been an influx of animation geared towards adults. Animation is commonly thought of as a technique, thus it’s ability to span over many different genres. - collection_order: alpha - Christmas: - trakt_list: - - https://trakt.tv/users/movistapp/lists/christmas-movies - - https://trakt.tv/users/2borno2b/lists/christmas-movies-extravanganza - imdb_list: - - https://www.imdb.com/list/ls025976544/ - - https://www.imdb.com/list/ls003863000/ - - https://www.imdb.com/list/ls027454200/ - - https://www.imdb.com/list/ls027886673/ - - https://www.imdb.com/list/ls097998599/ - sort_title: ++_Christmas - sync_mode: sync - summary: Christmas film is a genre that revolves around the plot involving Christmas. - collection_order: alpha Comedy: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=comedy - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=comedy&sort=user_rating,desc - limit: 100 - sort_title: ++_Comedy - sync_mode: sync + template: {name: IMDb Genre, genre: comedy} summary: Comedy is a genre of film that uses humor as a driving force. The aim of a comedy film is to illicit laughter from the audience through entertaining stories and characters. Although the comedy film may take on some serious material, most have a happy ending. Comedy film has the tendency to become a hybrid sub-genre because humor can be incorporated into many other genres. Comedies are more likely than other films to fall back on the success and popularity of an individual star. - collection_order: alpha Crime: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=crime - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=crime&sort=user_rating,desc - limit: 100 - sort_title: ++_Crime - sync_mode: sync + template: {name: IMDb Genre, genre: crime} summary: Crime film is a genre that revolves around the action of a criminal mastermind. A Crime film will often revolve around the criminal himself, chronicling his rise and fall. Some Crime films will have a storyline that follows the criminal's victim, yet others follow the person in pursuit of the criminal. This genre tends to be fast paced with an air of mystery – this mystery can come from the plot or from the characters themselves. - collection_order: alpha Documentary: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=documentary&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=documentary - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=documentary&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=documentary&sort=user_rating,desc - limit: 100 - sort_title: ++_Documentary - sync_mode: sync + template: {name: IMDb Genre, genre: documentary, title: documentary} summary: Documentary film is a non-fiction genre intended to document reality primarily for the purposes of instruction, education, or maintaining a historical record. - collection_order: alpha Drama: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=drama - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=drama&sort=user_rating,desc - limit: 100 - sort_title: ++_Drama - sync_mode: sync + template: {name: IMDb Genre, genre: drama} summary: Drama film is a genre that relies on the emotional and relational development of realistic characters. While Drama film relies heavily on this kind of development, dramatic themes play a large role in the plot as well. Often, these dramatic themes are taken from intense, real life issues. Whether heroes or heroines are facing a conflict from the outside or a conflict within themselves, Drama film aims to tell an honest story of human struggles. - collection_order: alpha Family: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=family - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=family&sort=user_rating,desc - limit: 100 - sort_title: ++_Family - sync_mode: sync + template: {name: IMDb Genre, genre: family} summary: Family film is a genre that is contains appropriate content for younger viewers. Family film aims to appeal not only to children, but to a wide range of ages. While the storyline may appeal to a younger audience, there are components of the film that are geared towards adults- such as witty jokes and humor. This genre may fall into many other genres, including comedy, adventure, fantasy, and animated film. - collection_order: alpha Fantasy: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=fantasy - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=fantasy&sort=user_rating,desc - limit: 100 - sort_title: ++_Fantasy - sync_mode: sync + template: {name: IMDb Genre, genre: fantasy} summary: Fantasy film is a genre that incorporates imaginative and fantastic themes. These themes usually involve magic, supernatural events, or fantasy worlds. Although it is its own distinct genre, these films can overlap into the horror and science fiction genres. Unlike science fiction, a fantasy film does not need to be rooted in fact. This element allows the audience to be transported into a new and unique world. Often, these films center on an ordinary hero in an extraordinary situation. - collection_order: alpha Gangster: + template: {name: Other Genre} imdb_list: - https://www.imdb.com/list/ls026270180/ - https://www.imdb.com/list/ls000093502/ - sort_title: ++_Gangster - sync_mode: sync summary: Gangster film is a sub-genre of crime films that center on organized crime or the mafia. Often the plot revolves around the rise and fall of an organized crime leader. Many Gangster films explore the destructive nature of organized crime, while others attempt to show the humanity of the individual characters. - collection_order: alpha Halloween.: + template: {name: Other Genre} trakt_list: - https://trakt.tv/users/kairbear08/lists/halloween - https://trakt.tv/users/mybicycle/lists/halloween - https://trakt.tv/users/jayinftl/lists/halloween - https://trakt.tv/users/roswellgeek/lists/halloween - sort_title: ++_Halloween - sync_mode: sync name_mapping: Halloween (Season) summary: Halloween film is a genre that revolves around the plot involving Halloween. - collection_order: alpha History: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=history - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=history&sort=user_rating,desc - limit: 100 - sort_title: ++_History - sync_mode: sync + template: {name: IMDb Genre, genre: history} summary: History film is a genre that takes historical events and people and interprets them in a larger scale. Historical accuracy is not the main focus, but rather the telling of a grandiose story. The drama of an History film is often accentuated by a sweeping musical score, lavish costumes, and high production value. - collection_order: alpha Horror: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=horror - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=horror&sort=user_rating,desc - limit: 100 - sort_title: ++_Horror - sync_mode: sync + template: {name: IMDb Genre, genre: horror} summary: Horror film is a genre that aims to create a sense of fear, panic, alarm, and dread for the audience. These films are often unsettling and rely on scaring the audience through a portrayal of their worst fears and nightmares. Horror films usually center on the arrival of an evil force, person, or event. Many Horror films include mythical creatures such as ghosts, vampires, and zombies. Traditionally, Horror films incorporate a large amount of violence and gore into the plot. Though it has its own style, Horror film often overlaps into Fantasy, Thriller, and Science-Fiction genres. - collection_order: alpha LGBTQ+: + template: {name: Other Genre} imdb_list: https://www.imdb.com/list/ls062688328/ - sort_title: ++_LGBTQ+ - sync_mode: sync summary: LGBTQ+ film is a genre of films where the characters decpict lesbian, gay, bisexual, transgender, queer and intersex people. - collection_order: alpha Martial Arts: + template: {name: Other Genre} imdb_list: - https://www.imdb.com/list/ls000099643/ - https://www.imdb.com/list/ls068611186/ - https://www.imdb.com/list/ls068378513/ - https://www.imdb.com/list/ls090404120/ - sort_title: ++_Martial Arts - sync_mode: sync summary: Martial Arts film is a sub-genre of action films that feature numerous martial arts combat between characters. These combats are usually the films' primary appeal and entertainment value, and often are a method of storytelling and character expression and development. Martial Arts are frequently featured in training scenes and other sequences in addition to fights. Martial Arts films commonly include other types of action, such as hand-to-hand combat, stuntwork, chases, and gunfights. - collection_order: alpha Music: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=music - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=music&sort=user_rating,desc - limit: 200 - sort_title: ++_Music - sync_mode: sync + template: {name: IMDb Genre, genre: music, limit: 200} summary: Music film is genre that revolves around music being an integral part of the characters lives. - collection_order: alpha Musical: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=musical - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=musical&sort=user_rating,desc - limit: 200 - sort_title: ++_Musical - sync_mode: sync + template: {name: IMDb Genre, genre: musical, limit: 200} summary: A Musical interweaves vocal and dance performances into the narrative of the film. The songs of a film can either be used to further the story or simply enhance the experience of the audience. These films are often done on a grand scale and incorporate lavish costumes and sets. Traditional musicals center on a well-known star, famous for their dancing or singing skills (i.e. Fred Astaire, Gene Kelly, Judy Garland). These films explore concepts such are love and success, allowing the audience to escape from reality. - collection_order: alpha Mystery: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=mystery - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=mystery&sort=user_rating,desc - limit: 100 - sort_title: ++_Mystery - sync_mode: sync + template: {name: IMDb Genre, genre: mystery} summary: A Mystery film centers on a person of authority, usually a detective, that is trying to solve a mysterious crime. The main protagonist uses clues, investigation, and logical reasoning. The biggest element in these films is a sense of “whodunit” suspense, usually created through visual cues and unusual plot twists. - collection_order: alpha Pandemic: + template: {name: Other Genre} imdb_list: https://www.imdb.com/list/ls092321048/ - sort_title: ++_Pandemic - sync_mode: sync summary: A Pandemic film resolves around widespread viruses, plagues, and diseases. - collection_order: alpha Romance: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=romance - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=romance&sort=user_rating,desc - limit: 200 - sort_title: ++_Romance - sync_mode: sync + template: {name: IMDb Genre, genre: romance, limit: 200} summary: "Romance film can be defined as a genre wherein the plot revolves around the love between two protagonists. This genre usually has a theme that explores an issue within love, including but not limited to: love at first sight, forbidden love, love triangles, and sacrificial love. The tone of Romance film can vary greatly. Whether the end is happy or tragic, Romance film aims to evoke strong emotions in the audience." - collection_order: alpha Romantic Comedy: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=romance,comedy - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=romance,comedy&sort=user_rating,desc - limit: 200 - sort_title: ++_Romantic Comedy - sync_mode: sync + template: {name: IMDb Genre, genre: "romance,comedy", limit: 200} summary: Romantic Comedy is a genre that attempts to catch the viewer’s 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. - collection_order: alpha filters: genre: Comedy + test: true Romantic Drama: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=romance,drama - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=romance,drama&sort=user_rating,desc - limit: 200 - sort_title: ++_Romantic Drama - sync_mode: sync + 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. - collection_order: alpha filters: genre: Drama Sci-Fi: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=sci-fi - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=sci-fi&sort=user_rating,desc - limit: 100 - sort_title: ++_Sci-Fi - sync_mode: sync + template: {name: IMDb Genre, genre: sci-fi} summary: Science Fiction (Sci-Fi) film is a genre that incorporates hypothetical, science-based themes into the plot of the film. Often, this genre incorporates futuristic elements and technologies to explore social, political, and philosophical issues. The film itself is usually set in the future, either on earth or in space. Traditionally, a Science Fiction film will incorporate heroes, villains, unexplored locations, fantastical quests, and advanced technology. - collection_order: alpha Sports: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=sport - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=sport&sort=user_rating,desc - limit: 200 - sort_title: ++_Sport - sync_mode: sync + template: {name: IMDb Genre, genre: sport, limit: 200} summary: A Sport Film revolves around a sport setting, event, or an athlete. Often, these films will center on a single sporting event that carries significant importance. Sports films traditionally have a simple plot that builds up to the significant sporting event. This genre is known for incorporating film techniques to build anticipation and intensity. Sport films have a large range of sub-genres, from comedies to dramas, and are more likely than other genres to be based true-life events. - collection_order: alpha Stand Up Comedy: + template: {name: Other Genre} imdb_list: - https://www.imdb.com/list/ls070221411/ - https://www.imdb.com/list/ls086584751/ - https://www.imdb.com/list/ls086022668/ - https://www.imdb.com/list/ls049792208/ - sort_title: ++_Stand Up Comedy - sync_mode: sync summary: Stand-up comedy is a comedic style in which a comedian performs in front of a live audience, speaking directly to them through a microphone. Comedians give the illusion that they are dialoguing, but in actuality, they are monologuing a grouping of humorous stories, jokes and one-liners, typically called a shtick, routine, act, or set. Some stand-up comedians use props, music or magic tricks to enhance their acts. Stand-up comedians perform quasi-autobiographical and fictionalized extensions of their offstage selves. - collection_order: alpha Sword & Sorcery: + template: {name: Other Genre} imdb_list: https://www.imdb.com/list/ls022909805 - sort_title: ++_Sword & Sorcery - sync_mode: sync summary: Sword and Sorcery film is a sub-genre of Fantasy that tend to be more plot-driven. These films rely on heavy action and battle scenes. Common themes in Sword and Sorcery films include a rescue mission, saving a princess, and battling a fantastical monster. The worlds and characters in these films are often much less developed than in other fantasy sub-genres. Of all the Fantasy sub-genres, Sword and Sorcery is most likely to be geared towards a younger audience, as many of these films are animated. - collection_order: alpha Thriller: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=thriller - limit: 100 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=thriller&sort=user_rating,desc - limit: 100 - sort_title: ++_Thriller - sync_mode: sync + template: {name: IMDb Genre, genre: thriller} summary: Thriller Film is a genre that revolves around anticipation and suspense. The aim for Thrillers is to keep the audience alert and on the edge of their seats. The protagonist in these films is set against a problem – an escape, a mission, or a mystery. No matter what sub-genre a Thriller film falls into, it will emphasize the danger that the protagonist faces. The tension with the main problem is built on throughout the film and leads to a highly stressful climax. - collection_order: alpha War: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=war - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=war&sort=user_rating,desc - limit: 200 - sort_title: ++_War - sync_mode: sync + template: {name: IMDb Genre, genre: war, limit: 200} summary: War Film is a genre that looks at the reality of war on a grand scale. They often focus on landmark battles as well as political issues within war. This genre usually focuses on a main character and his team of support, giving the audience an inside look into the gritty reality of war. - collection_order: alpha Western: - imdb_list: - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=western - limit: 200 - - url: https://www.imdb.com/search/title/?title_type=feature&release_date=1990-01-01,&user_rating=5.0,10.0&num_votes=100000,&genres=western&sort=user_rating,desc - limit: 200 - sort_title: ++_Western - sync_mode: sync + template: {name: IMDb Genre, genre: western, limit: 200} summary: "Western Film is a genre that revolves around stories primarily set in the late 19th century in the American Old West. Most Westerns are set between the American Civil War (1865) and the early 1900s. Common themes within Western Film include: the conquest of the wild west, the cultural separation of the East and the West, the West’s resistance to modern change, the conflict between Cowboys and Indians, outlaws, and treasure/gold hunting. American Western Film usually revolves around a stoic hero and emphasizes the importance of honor and sacrifice." - collection_order: alpha ###################################################### # People Collections # ###################################################### Adam Sandler: - actor: tmdb - writer: tmdb - tmdb_person: 19292 - sort_title: +_Adam Sandler - sync_mode: sync + template: {name: Actor Writer, person: 19292} Al Pacino: - actor: tmdb - tmdb_person: 1158 - sort_title: +_Al Pacino - sync_mode: sync + template: {name: Actor, person: 1158} Alfred Hitchcock: - director: tmdb - tmdb_person: 2636 + template: {name: Director, person: 2636} trakt_list: https://trakt.tv/users/movistapp/lists/hitchcock - sort_title: +_Alfred Hitchcock - sync_mode: sync Amy Adams: - actor: tmdb - tmdb_person: 9273 - sort_title: +_Amy Adams - sync_mode: sync + template: {name: Actor, person: 9273} Angelina Jolie: - actor: tmdb - tmdb_person: 11701 - sort_title: +_Angelina Jolie - sync_mode: sync + template: {name: Actor, person: 11701} Anne Hathaway: - actor: tmdb - tmdb_person: 1813 - sort_title: +_Anne Hathaway - sync_mode: sync + template: {name: Actor, person: 1813} Anthony Hopkins: - actor: tmdb - tmdb_person: 4173 - sort_title: +_Anthony Hopkins - sync_mode: sync + template: {name: Actor, person: 4173} Antonio Banderas: - actor: tmdb - tmdb_person: 3131 - sort_title: +_Antonio Banderas - sync_mode: sync + template: {name: Actor, person: 3131} Arnold Schwarzenegger: - actor: tmdb - tmdb_person: 1100 - sort_title: +_Arnold Schwarzenegger - sync_mode: sync + template: {name: Actor, person: 1100} Ben Affleck: - actor: tmdb - tmdb_person: 880 - sort_title: +_Ben Affleck - sync_mode: sync + template: {name: Actor, person: 880} Ben Stiller: - actor: tmdb - tmdb_person: 7399 - sort_title: +_Ben Stiller - sync_mode: sync + template: {name: Actor, person: 7399} Bill Murray: - actor: tmdb - tmdb_person: 1532 - sort_title: +_Bill Murray - sync_mode: sync + template: {name: Actor, person: 1532} Brad Pitt: - actor: tmdb - tmdb_person: 287 - sort_title: +_Brad Pitt - sync_mode: sync + template: {name: Actor, person: 287} Brendan Fraser: - actor: tmdb - tmdb_person: 18269 - sort_title: +_Brendan Fraser - sync_mode: sync + template: {name: Actor, person: 18269} Bruce Lee: - actor: tmdb - tmdb_person: 19429 - sort_title: +_Bruce Lee - sync_mode: sync + template: {name: Actor, person: 19429} Bruce Willis: - actor: tmdb - tmdb_person: 62 - sort_title: +_Bruce Willis - sync_mode: sync + template: {name: Actor, person: 62} Cameron Diaz: - actor: tmdb - tmdb_person: 6941 - sort_title: +_Cameron Diaz - sync_mode: sync + template: {name: Actor, person: 6941} Carrie Fisher: - actor: tmdb - tmdb_person: 4 - sort_title: +_Carrie Fisher - sync_mode: sync + template: {name: Actor, person: 4} Cate Blanchett: - actor: tmdb - tmdb_person: 112 - sort_title: +_Cate Blanchett - sync_mode: sync + template: {name: Actor, person: 112} Catherine Zeta-Jones: - actor: tmdb - tmdb_person: 1922 - sort_title: +_Catherine Zeta-Jones - sync_mode: sync + template: {name: Actor, person: 1922} Channing Tatum: - actor: tmdb - tmdb_person: 38673 - sort_title: +_Channing Tatum - sync_mode: sync + template: {name: Actor, person: 38673} Charlie Chaplin: - actor: tmdb - tmdb_person: 13848 - sort_title: +_Charlie Chaplin - sync_mode: sync + template: {name: Actor, person: 13848} Charlize Theron: - actor: tmdb - tmdb_person: 6885 - sort_title: +_Charlize Theron - sync_mode: sync + template: {name: Actor, person: 6885} Christian Bale: - actor: tmdb - tmdb_person: 3894 - sort_title: +_Christian Bale - sync_mode: sync + template: {name: Actor, person: 3894} Chris Evans: - actor: tmdb - tmdb_person: 16828 - sort_title: +_Chris Evans - sync_mode: sync + template: {name: Actor, person: 16828} Chris Hemsworth: - actor: tmdb - tmdb_person: 74568 - sort_title: +_Chris Hemsworth - sync_mode: sync + template: {name: Actor, person: 74568} Chris Pratt: - actor: tmdb - tmdb_person: 73457 - sort_title: +_Chris Pratt - sync_mode: sync + template: {name: Actor, person: 73457} Chris Rock: - actor: tmdb - tmdb_person: 2632 - sort_title: +_Chris Rock - sync_mode: sync + template: {name: Actor, person: 2632} Christopher Nolan: - director: tmdb - tmdb_person: 525 - sort_title: +_Christopher Nolan - sync_mode: sync + template: {name: Director, person: 525} Christopher Walken: - actor: tmdb - tmdb_person: 4690 - sort_title: +_Christopher Walken - sync_mode: sync + template: {name: Actor, person: 4690} Chuck Norris: - actor: tmdb - tmdb_person: 51576 - sort_title: +_Chuck Norris - sync_mode: sync + template: {name: Actor, person: 51576} Clint Eastwood: - actor: tmdb - director: tmdb - tmdb_person: 190 - sort_title: +_Clint Eastwood - sync_mode: sync + template: {name: Actor Director, person: 190} Coen Brothers: - writer: tmdb - director: tmdb - tmdb_person: 1223, 1224 - sort_title: +_Coen Brothers - sync_mode: sync + template: {name: Director Writer, person: "1223, 1224"} summary: "Joel Coen and Ethan Coen, collectively referred to as the Coen Brothers, are American film directors, producers, screenwriters, and editors. Their films span many genres and styles, which they frequently subvert or parody. Their most acclaimed works include: Raising Arizona (1987), Miller's Crossing (1990), Fargo (1996), The Big Lebowski (1998), O Brother, Where Art Thou? (2000), No Country for Old Men (2007), Burn After Reading (2008), A Serious Man (2009), True Grit (2010), Inside Llewyn Davis (2013), and The Ballad of Buster Scruggs (2018)." Daniel Craig: - actor: tmdb - tmdb_person: 8784 - sort_title: +_Daniel Craig - sync_mode: sync + template: {name: Actor, person: 8784} Dave Bautista: - actor: tmdb - tmdb_person: 543530 - sort_title: +_Dave Bautista - sync_mode: sync + template: {name: Actor, person: 543530} Dave Chappelle: - actor: tmdb - tmdb_person: 4169 - sort_title: +_Dave Chappelle - sync_mode: sync + template: {name: Actor, person: 4169} Denzel Washington: - actor: tmdb - tmdb_person: 5292 - sort_title: +_Denzel Washington - sync_mode: sync + template: {name: Actor, person: 5292} Diane Keaton: - actor: tmdb - tmdb_person: 3092 - sort_title: +_Diane Keaton - sync_mode: sync + template: {name: Actor, person: 3092} Don Cheadle: - actor: tmdb - tmdb_person: 1896 - sort_title: +_Don Cheadle - sync_mode: sync + template: {name: Actor, person: 1896} Dustin Hoffman: - actor: tmdb - tmdb_person: 4483 - sort_title: +_Dustin Hoffman - sync_mode: sync + template: {name: Actor, person: 4483} Dwayne Johnson: - actor: tmdb - tmdb_person: 18918 - sort_title: +_Dwayne Johnson - sync_mode: sync + template: {name: Actor, person: 18918} Eddie Murphy: - actor: tmdb - tmdb_person: 776 - sort_title: +_Eddie Murphy - sync_mode: sync + template: {name: Actor, person: 776} Edward Norton: - actor: tmdb - tmdb_person: 819 - sort_title: +_Edward Norton - sync_mode: sync + template: {name: Actor, person: 819} Elliot Page: - actor: tmdb - tmdb_person: 27578 - sort_title: +_Elliot Page - sync_mode: sync + template: {name: Actor, person: 27578} Emma Stone: - actor: tmdb - tmdb_person: 54693 - sort_title: +_Emma Stone - sync_mode: sync + template: {name: Actor, person: 54693} Emma Watson: - actor: tmdb - tmdb_person: 10990 - sort_title: +_Emma Watson - sync_mode: sync + template: {name: Actor, person: 10990} Ewan McGregor: - actor: tmdb - tmdb_person: 3061 - sort_title: +_Ewan McGregor - sync_mode: sync + template: {name: Actor, person: 3061} George Clooney: - actor: tmdb - tmdb_person: 1461 - sort_title: +_George Clooney - sync_mode: sync + template: {name: Actor, person: 1461} Gerard Butler: - actor: tmdb - tmdb_person: 17276 - sort_title: +_Gerard Butler - sync_mode: sync + template: {name: Actor, person: 17276} Harrison Ford: - actor: tmdb - tmdb_person: 3 - sort_title: +_Harrison Ford - sync_mode: sync + template: {name: Actor, person: 3} Hayao Miyazaki: - director: tmdb - tmdb_person: 608 - sort_title: +_Hayao Miyazaki - sync_mode: sync + template: {name: Director, person: 608} Hugh Jackman: - actor: tmdb - tmdb_person: 6968 - sort_title: +_Hugh Jackman - sync_mode: sync + template: {name: Actor, person: 6968} Ian McKellen: - actor: tmdb - tmdb_person: 1327 - sort_title: +_Ian McKellen - sync_mode: sync + template: {name: Actor, person: 1327} Ice Cube: - actor: tmdb - tmdb_person: 9778 - sort_title: +_Ice Cube - sync_mode: sync + template: {name: Actor, person: 9778} J.J. Abrams: - director: tmdb - tmdb_person: 15344 - sort_title: +_J.J. Abrams - sync_mode: sync + template: {name: Director, person: 15344} Jack Black: - actor: tmdb - tmdb_person: 70851 - sort_title: +_Jack Black - sync_mode: sync + template: {name: Actor, person: 70851} Jackie Chan: - actor: tmdb - tmdb_person: 18897 - sort_title: +_Jackie Chan - sync_mode: sync + template: {name: Actor, person: 18897} James Cameron: - director: tmdb - tmdb_person: 2710 - sort_title: +_James Cameron - sync_mode: sync + template: {name: Director, person: 2710} James Franco: - actor: tmdb - tmdb_person: 17051 - sort_title: +_James Franco - sync_mode: sync + template: {name: Actor, person: 17051} Jamie Foxx: - actor: tmdb - tmdb_person: 134 - sort_title: +_Jamie Foxx - sync_mode: sync - Jason Bateman: - actor: tmdb - tmdb_person: 23532 - sort_title: +_Jason Bateman - sync_mode: sync + template: {name: Actor, person: 134} + Jason Bateman:: + template: {name: Actor, person: 23532} Jason Statham: - actor: tmdb - tmdb_person: 976 - sort_title: +_Jason Statham - sync_mode: sync + template: {name: Actor, person: 976} Jeff Bridges: - actor: tmdb - tmdb_person: 1229 - sort_title: +_Jeff Bridges - sync_mode: sync + template: {name: Actor, person: 1229} Jeff Goldblum: - actor: tmdb - tmdb_person: 4785 - sort_title: +_Jeff Goldblum - sync_mode: sync + template: {name: Actor, person: 4785} Jennifer Aniston: - actor: tmdb - tmdb_person: 4491 - sort_title: +_Jennifer Aniston - sync_mode: sync + template: {name: Actor, person: 4491} Jennifer Lawrence: - actor: tmdb - tmdb_person: 72129 - sort_title: +_Jennifer Lawrence - sync_mode: sync + template: {name: Actor, person: 72129} Jesse Eisenberg: - actor: tmdb - tmdb_person: 44735 - sort_title: +_Jesse Eisenberg - sync_mode: sync + template: {name: Actor, person: 44735} Jessica Alba: - actor: tmdb - tmdb_person: 56731 - sort_title: +_Jessica Alba - sync_mode: sync + template: {name: Actor, person: 56731} Jet Li: - actor: tmdb - tmdb_person: 1336 - sort_title: +_Jet Li - sync_mode: sync + template: {name: Actor, person: 1336} Jim Carrey: - actor: tmdb - tmdb_person: 206 - sort_title: +_Jim Carrey - sync_mode: sync + template: {name: Actor, person: 206} John Candy: - actor: tmdb - tmdb_person: 7180 - sort_title: +_John Candy - sync_mode: sync + template: {name: Actor, person: 7180} John Travolta: - actor: tmdb - tmdb_person: 8891 - sort_title: +_John Travolta - sync_mode: sync + template: {name: Actor, person: 8891} John Wayne: - actor: tmdb - tmdb_person: 4165 - sort_title: +_John Wayne - sync_mode: sync + template: {name: Actor, person: 4165} Johnny Depp: - actor: tmdb - tmdb_person: 85 - sort_title: +_Johnny Depp - sync_mode: sync + template: {name: Actor, person: 85} Julia Roberts: - actor: tmdb - tmdb_person: 1204 - sort_title: +_Julia Roberts - sync_mode: sync + template: {name: Actor, person: 1204} Kevin Bacon: - actor: tmdb - tmdb_person: 4724 - sort_title: +_Kevin Bacon - sync_mode: sync + template: {name: Actor, person: 4724} Kevin Costner: - actor: tmdb - tmdb_person: 1269 - sort_title: +_Kevin Costner - sync_mode: sync + template: {name: Actor, person: 1269} Kevin Hart: - actor: tmdb - tmdb_person: 55638 - sort_title: +_Kevin Hart - sync_mode: sync + template: {name: Actor, person: 55638} Kevin Smith: - writer: tmdb - director: tmdb - tmdb_person: 19303 - sort_title: +_Kevin Smith - sync_mode: sync + template: {name: Director Writer, person: 19303} Leonardo DiCaprio: - actor: tmdb - tmdb_person: 6193 - sort_title: +_Leonardo DiCaprio - sync_mode: sync + template: {name: Actor, person: 6193} Liam Neeson: - actor: tmdb - tmdb_person: 3896 - sort_title: +_Liam Neeson - sync_mode: sync + template: {name: Actor, person: 3896} Lucy Liu: - actor: tmdb - tmdb_person: 140 - sort_title: +_Lucy Liu - sync_mode: sync + template: {name: Actor, person: 140} M. Night Shyamalan: - director: tmdb - tmdb_person: 11614 - sort_title: +_M. Night Shyamalan - sync_mode: sync + template: {name: Director, person: 11614} Mark Wahlberg: - actor: tmdb - tmdb_person: 13240 - sort_title: +_Mark Wahlberg - sync_mode: sync + template: {name: Actor, person: 13240} Matt Damon: - actor: tmdb - tmdb_person: 1892 - sort_title: +_Matt Damon - sync_mode: sync + template: {name: Actor, person: 1892} Matthew McConaughey: - actor: tmdb - tmdb_person: 10297 - sort_title: +_Matthew McConaughey - sync_mode: sync + template: {name: Actor, person: 10297} Megan Fox: - actor: tmdb - tmdb_person: 19537 - sort_title: +_Megan Fox - sync_mode: sync + template: {name: Actor, person: 19537} Mel Brooks: - actor: tmdb - writer: tmdb - director: tmdb - tmdb_person: 14639 - sort_title: +_Mel Brooks - sync_mode: sync + template: {name: Actor Director Writer, person: 14639} Mel Gibson: - actor: tmdb - director: tmdb - tmdb_person: 2461 - sort_title: +_Mel Gibson - sync_mode: sync + template: {name: Actor Director, person: 2461} Melissa McCarthy: - actor: tmdb - tmdb_person: 55536 - sort_title: +_Melissa McCarthy - sync_mode: sync + template: {name: Actor, person: 55536} Meryl Streep: - actor: tmdb - tmdb_person: 5064 - sort_title: +_Meryl Streep - sync_mode: sync + template: {name: Actor, person: 5064} Michael Bay: - director: tmdb - tmdb_person: 865 - sort_title: +_Michael Bay - sync_mode: sync + template: {name: Director, person: 865} Michael Caine: - actor: tmdb - tmdb_person: 3895 - sort_title: +_Michael Caine - sync_mode: sync + template: {name: Actor, person: 3895} Michael Keaton: - actor: tmdb - tmdb_person: 2232 - sort_title: +_Michael Keaton - sync_mode: sync + template: {name: Actor, person: 2232} Mike Myers: - actor: tmdb - tmdb_person: 12073 - sort_title: +_Mike Myers - sync_mode: sync + template: {name: Actor, person: 12073} Mila Kunis: - actor: tmdb - tmdb_person: 18973 - sort_title: +_Mila Kunis - sync_mode: sync + template: {name: Actor, person: 18973} Milla Jovovich: - actor: tmdb - tmdb_person: 63 - sort_title: +_Milla Jovovich - sync_mode: sync + template: {name: Actor, person: 63} Morgan Freeman: - actor: tmdb - tmdb_person: 192 - sort_title: +_Morgan Freeman - sync_mode: sync + template: {name: Actor, person: 192} Natalie Portman: - actor: tmdb - tmdb_person: 524 - sort_title: +_Natalie Portman - sync_mode: sync + template: {name: Actor, person: 524} Nicolas Cage: - actor: tmdb - tmdb_person: 2963 - sort_title: +_Nicolas Cage - sync_mode: sync + template: {name: Actor, person: 2963} Nicole Kidman: - actor: tmdb - tmdb_person: 2227 - sort_title: +_Nicole Kidman - sync_mode: sync + template: {name: Actor, person: 2227} Orlando Bloom: - actor: tmdb - tmdb_person: 114 - sort_title: +_Orlando Bloom - sync_mode: sync + template: {name: Actor, person: 114} Owen Wilson: - actor: tmdb - tmdb_person: 887 - sort_title: +_Owen Wilson - sync_mode: sync + template: {name: Actor, person: 887} Patrick Stewart: - actor: tmdb - tmdb_person: 2387 - sort_title: +_Patrick Stewart - sync_mode: sync + template: {name: Actor, person: 2387} Quentin Tarantino: - actor: tmdb - writer: tmdb - director: tmdb - tmdb_person: 138 - sort_title: +_Quentin Tarantino - sync_mode: sync + template: {name: Actor Director Writer, person: 138} Reese Witherspoon: - actor: tmdb - tmdb_person: 368 - sort_title: +_Reese Witherspoon - sync_mode: sync + template: {name: Actor, person: 368} Ridley Scott: - director: tmdb - tmdb_person: 578 - sort_title: +_Ridley Scott - sync_mode: sync + template: {name: Director, person: 578} Robert De Niro: - actor: tmdb - tmdb_person: 380 - sort_title: +_Robert De Niro - sync_mode: sync + template: {name: Actor, person: 380} Robert Downey Jr.: - actor: tmdb - tmdb_person: 3223 - sort_title: +_Robert Downey Jr. - sync_mode: sync + template: {name: Actor, person: 3223} Robin Williams: - actor: tmdb - tmdb_person: 2157 - sort_title: +_Robin Williams - sync_mode: sync + template: {name: Actor, person: 2157} Rosario Dawson: - actor: tmdb - tmdb_person: 5916 - sort_title: +_Rosario Dawson - sync_mode: sync + template: {name: Actor, person: 5916} Ryan Reynolds: - actor: tmdb - tmdb_person: 10859 - sort_title: +_Ryan Reynolds - sync_mode: sync + template: {name: Actor, person: 10859} Samuel L. Jackson: - actor: tmdb - tmdb_person: 2231 - sort_title: +_Samuel L. Jackson - sync_mode: sync + template: {name: Actor, person: 2231} Sandra Bullock: - actor: tmdb - tmdb_person: 18277 - sort_title: +_Sandra Bullock - sync_mode: sync + template: {name: Actor, person: 18277} Sacha Baron Cohen: - actor: tmdb - tmdb_person: 6730 - sort_title: +_Sacha Baron Cohen - sync_mode: sync + template: {name: Actor, person: 6730} Scarlett Johansson: - actor: tmdb - tmdb_person: 1245 - sort_title: +_Scarlett Johansson - sync_mode: sync + template: {name: Actor, person: 1245} Sean Connery: - actor: tmdb - tmdb_person: 738 - sort_title: +_Sean Connery - sync_mode: sync + template: {name: Actor, person: 738} Seth Rogen: - actor: tmdb - writer: tmdb - director: tmdb - tmdb_person: 19274 - sort_title: +_Seth Rogen - sync_mode: sync + template: {name: Actor Director Writer, person: 19274} Shia LaBeouf: - actor: tmdb - tmdb_person: 10959 - sort_title: +_Shia LaBeouf - sync_mode: sync + template: {name: Actor, person: 10959} Steve Buscemi: - actor: tmdb - tmdb_person: 884 - sort_title: +_Steve Buscemi - sync_mode: sync + template: {name: Actor, person: 884} Steve Carell: - actor: tmdb - tmdb_person: 4495 - sort_title: +_Steve Carell - sync_mode: sync + template: {name: Actor, person: 4495} Steve Martin: - actor: tmdb - tmdb_person: 67773 - sort_title: +_Steve Martin - sync_mode: sync + template: {name: Actor, person: 67773} Steven Seagal: - actor: tmdb - tmdb_person: 23880 - sort_title: +_Steven Seagal - sync_mode: sync + template: {name: Actor, person: 23880} Steven Spielberg: - director: tmdb - tmdb_person: 488 - sort_title: +_Steven Spielberg - sync_mode: sync + template: {name: Director, person: 488} Sylvester Stallone: - actor: tmdb - tmdb_person: 16483 - sort_title: +_Sylvester Stallone - sync_mode: sync + template: {name: Actor, person: 16483} Tim Burton: - writer: tmdb - director: tmdb - tmdb_person: 510 - sort_title: +_Tim Burton - sync_mode: sync + template: {name: Director Writer, person: 510} Tom Cruise: - actor: tmdb - tmdb_person: 500 - sort_title: +_Tom Cruise - sync_mode: sync + template: {name: Actor, person: 500} Tom Hanks: - actor: tmdb - tmdb_person: 31 - sort_title: +_Tom Hanks - sync_mode: sync + template: {name: Actor, person: 31} Tommy Lee Jones: - actor: tmdb - tmdb_person: 2176 - sort_title: +_Tommy Lee Jones - sync_mode: sync + template: {name: Actor, person: 2176} Vin Diesel: - actor: tmdb - tmdb_person: 12835 - sort_title: +_Vin Diesel - sync_mode: sync + template: {name: Actor, person: 12835} Vince Vaughn: - actor: tmdb - tmdb_person: 4937 - sort_title: +_Vince Vaughn - sync_mode: sync + template: {name: Actor, person: 4937} Wesley Snipes: - actor: tmdb - tmdb_person: 10814 - sort_title: +_Wesley Snipes - sync_mode: sync + template: {name: Actor, person: 10814} Will Ferrell: - actor: tmdb - tmdb_person: 23659 - sort_title: +_Will Ferrell - sync_mode: sync + template: {name: Actor, person: 23659} Will Smith: - actor: tmdb - tmdb_person: 2888 - sort_title: +_Will Smith - sync_mode: sync + template: {name: Actor, person: 2888} Woody Harrelson: - actor: tmdb - tmdb_person: 57755 - sort_title: +_Woody Harrelson - sync_mode: sync + template: {name: Actor, person: 57755} Zack Snyder: - director: tmdb - tmdb_person: 15217 - sort_title: +_Zack Snyder - sync_mode: sync + template: {name: Director, person: 15217} ###################################################### # TMDb Collections # ###################################################### + 101 Dalmatians: - tmdb_collection_details: 100693 - schedule: daily + template: {name: Collection, collection: 100693} 101 Dalmatians (Live-Action): - tmdb_collection_details: 124916 - schedule: daily + template: {name: Collection, collection: 124916} 28 Days/Weeks Later: - tmdb_collection_details: 1565 + template: {name: Collection, collection: 1565} name_mapping: 28 Days-Weeks Later - schedule: daily 3 Ninja: - tmdb_collection_details: 71458 - schedule: daily + template: {name: Collection, collection: 71458} "300": - tmdb_collection_details: 125570 - schedule: daily + template: {name: Collection, collection: 125570} Addams Family: - tmdb_collection_details: 11716 - schedule: daily + template: {name: Collection, collection: 11716} Air Bud: - tmdb_collection_details: 97445 - schedule: daily + template: {name: Collection, collection: 97445} Aladdin: - tmdb_collection_details: 86027 - schedule: daily + template: {name: Collection, collection: 86027} Alice in Wonderland: - tmdb_collection_details: 261307 - schedule: daily + template: {name: Collection, collection: 261307} Alien: - tmdb_collection_details: 8091, 135416 - schedule: daily + template: {name: Collection, collection: "8091, 135416"} All Dogs Go to Heaven: - tmdb_collection_details: 140910 - schedule: daily + template: {name: Collection, collection: 140910} Almighty: - tmdb_collection_details: 124949 - schedule: daily + template: {name: Collection, collection: 124949} Alvin and the Chipmunks: - tmdb_collection_details: 167613 - schedule: daily + template: {name: Collection, collection: 167613} The Amazing Spider-Man: - tmdb_collection_details: 125574 - schedule: daily + template: {name: Collection, collection: 125574} American Pie: - tmdb_collection_details: 2806, 298820 - schedule: daily + template: {name: Collection, collection: "2806, 298820"} American Psycho: - tmdb_collection_details: 86105 - schedule: daily + template: {name: Collection, collection: 86105} An American Tail: - tmdb_collection_details: 8783 - schedule: daily + template: {name: Collection, collection: 8783} Anaconda: - tmdb_collection_details: 105995 - tmdb_movie: 336560 - schedule: daily + template: {name: Collection Movie, collection: 105995, movie: 336560} Anchorman: - tmdb_collection_details: 93791 - schedule: daily + template: {name: Collection, collection: 93791} Angels in the ...: - tmdb_collection_details: 508334 + template: {name: Collection, collection: 508334} name_mapping: Angels in the - schedule: daily The Angry Birds: - tmdb_collection_details: 531315 - schedule: daily - add_to_arr: true + template: {name: Collection, collection: 531315} Annabelle: - tmdb_collection_details: 402074 - schedule: daily + template: {name: Collection, collection: 402074} Ant-Man: - tmdb_collection_details: 422834 - schedule: daily + template: {name: Collection, collection: 422834} Appleseed: - tmdb_collection_details: 87800, 371526 - schedule: daily + template: {name: Collection, collection: "87800, 371526"} Atlantis: - tmdb_collection_details: 100965 - schedule: daily + template: {name: Collection, collection: 100965} Attack on Titan: - tmdb_collection_details: 370411 - schedule: daily + template: {name: Collection, collection: 370411} Austin Powers: - tmdb_collection_details: 1006 - schedule: daily + template: {name: Collection, collection: 1006} The Avengers: - tmdb_collection_details: 86311 - schedule: daily + template: {name: Collection, collection: 86311} AVP: - tmdb_collection_details: 115762 - schedule: daily + template: {name: Collection, collection: 115762} Babe: - tmdb_collection_details: 9435 - schedule: daily + template: {name: Collection, collection: 9435} Back to the Future: - tmdb_collection_details: 264 - schedule: daily + template: {name: Collection, collection: 264} Bad Boys: - tmdb_collection_details: 14890 - schedule: daily + template: {name: Collection, collection: 14890} Bad Moms: - tmdb_collection_details: 487376 - schedule: daily + template: {name: Collection, collection: 487376} Bad Santa: - tmdb_collection_details: 423173 - schedule: daily + template: {name: Collection, collection: 423173} Balto: - tmdb_collection_details: 117693 - schedule: daily + template: {name: Collection, collection: 117693} Bambi: - tmdb_collection_details: 87250 - schedule: daily + template: {name: Collection, collection: 87250} Barbershop: - tmdb_collection_details: 176097 - tmdb_movie: 14177 - schedule: daily + template: {name: Collection Movie, collection: 176097, movie: 14177} Batman: - tmdb_collection_details: 120794 - schedule: daily + template: {name: Collection, collection: 120794} Batman (Adam West) Animation: - tmdb_collection_details: 626517 - schedule: daily + template: {name: Collection, collection: 626517} Beauty and the Beast: - tmdb_collection_details: 153010 - schedule: daily + template: {name: Collection, collection: 153010} Bill & Ted's Most Excellent: - tmdb_collection_details: 91746 - schedule: daily + template: {name: Collection, collection: 91746} Black Water: - tmdb_collection_details: 730166 - schedule: daily + template: {name: Collection, collection: 730166} Blade: - tmdb_collection_details: 735 - schedule: daily + template: {name: Collection, collection: 735} Blade Runner: - tmdb_collection_details: 422837 - schedule: daily + template: {name: Collection, collection: 422837} The Blues Brothers: - tmdb_collection_details: 112636 - schedule: daily + template: {name: Collection, collection: 112636} The Boondock Saints: - tmdb_collection_details: 87186 - schedule: daily + template: {name: Collection, collection: 87186} Borat Moviefilms: - tmdb_collection_details: 747168 - schedule: daily + template: {name: Collection, collection: 747168} The Bourne: - tmdb_collection_details: 31562 - schedule: daily + template: {name: Collection, collection: 31562} The Boy: - tmdb_collection_details: 666337 - schedule: daily + template: {name: Collection, collection: 666337} Bring It On: - tmdb_collection_details: 430186 - schedule: daily + template: {name: Collection, collection: 430186} Brother Bear: - tmdb_collection_details: 96472 - schedule: daily + template: {name: Collection, collection: 96472} The Buddies: - tmdb_collection_details: 91657 - schedule: daily + template: {name: Collection, collection: 91657} Captain America: - tmdb_collection_details: 131295 - schedule: daily + template: {name: Collection, collection: 131295} Carrie: - tmdb_collection_details: 257053 - schedule: daily + template: {name: Collection, collection: 257053} Cars: - tmdb_collection_details: 87118 - schedule: daily + template: {name: Collection, collection: 87118} Charlie Brown: + template: {name: Other Collection} imdb_list: https://www.imdb.com/list/ls054850259/ summary: Collection of Movies and TV Specials with the beloved Peanuts characters. - schedule: daily Charlie's Angels: - tmdb_collection_details: 86029 - schedule: daily + template: {name: Collection, collection: 86029} Cheaper by the Dozen: - tmdb_collection_details: 114783 - schedule: daily + template: {name: Collection, collection: 114783} The Chronicles of Narnia: - tmdb_collection_details: 420 - schedule: daily + template: {name: Collection, collection: 420} The Chronicles of Riddick: - tmdb_collection_details: 2794 - schedule: daily + template: {name: Collection, collection: 2794} Cinderella: - tmdb_collection_details: 55419 - schedule: daily + template: {name: Collection, collection: 55419} Cinderella Story: - tmdb_collection_details: 437451 - schedule: daily + template: {name: Collection, collection: 437451} City Slickers: - tmdb_collection_details: 150156 - schedule: daily + template: {name: Collection, collection: 150156} Clash of the Titans: - tmdb_collection_details: 86780 - schedule: daily + template: {name: Collection, collection: 86780} Clerks: - tmdb_collection_details: 182813 - schedule: daily + template: {name: Collection, collection: 182813} Cloudy with a Chance of Meatballs: - tmdb_collection_details: 177467 - schedule: daily + template: {name: Collection, collection: 177467} Cloverfield: + template: {name: Other 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. - schedule: daily The Conjuring: - tmdb_collection_details: 313086 - schedule: daily + template: {name: Collection, collection: 313086} Cornetto Trilogy: + template: {name: Other 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). - schedule: daily Creed: - tmdb_collection_details: 553717 - schedule: daily + template: {name: Collection, collection: 553717} Crocodile Dundee: - tmdb_collection_details: 9332 - schedule: daily + template: {name: Collection, collection: 9332} The Croods: - tmdb_collection_details: 464577 - schedule: daily + template: {name: Collection, collection: 464577} Crouching Tiger, Hidden Dragon: - tmdb_collection_details: 290973 - schedule: daily + template: {name: Collection, collection: 290973} Daddy's Home: - tmdb_collection_details: 473971 - schedule: daily + template: {name: Collection, collection: 473971} The Dark Knight: - tmdb_collection_details: 263 - schedule: daily + template: {name: Collection, collection: 263} DC Super Hero Girls: - tmdb_collection_details: 477208, 557495 - schedule: daily + template: {name: Collection, collection: "477208, 557495"} Deadpool: - tmdb_collection_details: 448150 - tmdb_movie: 567604 - schedule: daily + template: {name: Collection Movie, collection: 448150, movie: 567604} Death Note: - tmdb_collection_details: 102019 - schedule: daily + template: {name: Collection, collection: 102019} Death Race: - tmdb_collection_details: 86116 - schedule: daily + template: {name: Collection, collection: 86116} The Debt Collector: - tmdb_collection_details: 709271 - schedule: daily + template: {name: Collection, collection: 709271} Despicable Me: - tmdb_collection_details: 86066, 544669 - schedule: daily + template: {name: Collection, collection: "86066, 544669"} Die Hard: - tmdb_collection_details: 1570 - schedule: daily + template: {name: Collection, collection: 1570} Dirty Harry: - tmdb_collection_details: 10456 - schedule: daily + template: {name: Collection, collection: 10456} Divergent: - tmdb_collection_details: 283579 - schedule: daily + template: {name: Collection, collection: 283579} A Dog's Purpose: - tmdb_collection_details: 591028 - schedule: daily + template: {name: Collection, collection: 591028} DragonHeart: - tmdb_collection_details: 169452 - schedule: daily + template: {name: Collection, collection: 169452} Dumb and Dumber: - tmdb_collection_details: 96665 - schedule: daily + template: {name: Collection, collection: 96665} Dungeons & Dragons: - tmdb_collection_details: 106498 - add_to_arr: true - schedule: daily + template: {name: Collection, collection: 106498} The Emperor's New Groove: - tmdb_collection_details: 178117 - schedule: daily + template: {name: Collection, collection: 178117} The Equalizer: - tmdb_collection_details: 523855 - schedule: daily + template: {name: Collection, collection: 523855} Escape From ...: - tmdb_collection_details: 115838 + template: {name: Collection, collection: 115838} name_mapping: Escape From - schedule: daily Escape Plan: - tmdb_collection_details: 525891 - schedule: daily + template: {name: Collection, collection: 525891} Evangelion: - tmdb_collection_details: 210303 + template: {name: Collection, collection: 210303} summary: A Japanese animated film series and a retelling of the original Neon Genesis Evangelion anime television series, produced by Studio Khara. Hideaki Anno served as the writer and general manager of the project, with Kazuya Tsurumaki and Masayuki directing the films themselves. Yoshiyuki Sadamoto, Ikuto Yamashita and Shiro Sagisu returned to provide character designs, mechanical designs and music respectively. - schedule: daily The Expendables: - tmdb_collection_details: 126125 - schedule: daily + template: {name: Collection, collection: 126125} Fantasia: - tmdb_collection_details: 55427 - schedule: daily + template: {name: Collection, collection: 55427} Fantastic Beasts: - tmdb_collection_details: 435259 - schedule: daily + template: {name: Collection, collection: 435259} Fantastic Four: - tmdb_collection_details: 9744 - schedule: daily + template: {name: Collection, collection: 9744} The Fast and the Furious: - tmdb_collection_details: 9485, 688042 - schedule: daily + template: {name: Collection, collection: "9485, 688042"} Fifty Shades: - tmdb_collection_details: 344830 - schedule: daily + template: {name: Collection, collection: 344830} Final Destination: - tmdb_collection_details: 8864 - schedule: daily + template: {name: Collection, collection: 8864} Final Fantasy: + template: {name: Other 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. - schedule: daily Finding Nemo: - tmdb_collection_details: 137697 - schedule: daily + template: {name: Collection, collection: 137697} The Flintstones: - tmdb_collection_details: 351684 - schedule: daily + template: {name: Collection, collection: 351684} The Fox and the Hound: - tmdb_collection_details: 100970 - schedule: daily + template: {name: Collection, collection: 100970} Free Willy: - tmdb_collection_details: 9328 - schedule: daily + template: {name: Collection, collection: 9328} Friday: - tmdb_collection_details: 43563 - schedule: daily + template: {name: Collection, collection: 43563} Friday the 13th: - tmdb_collection_details: 9735 - tmdb_movie: 6466, 222724 - schedule: daily + template: {name: Collection Movie, collection: 9735, movie: "6466, 222724"} Frozen: - tmdb_collection_details: 386382 - tmdb_movie: 326359, 460793 - schedule: daily + template: {name: Collection Movie, collection: 386382, movie: "326359, 460793"} G.I. Joe: - tmdb_collection_details: 135468 - schedule: daily + template: {name: Collection, collection: 135468} Garfield: - tmdb_collection_details: 86115, 373918 - schedule: daily + template: {name: Collection, collection: "86115, 373918"} George Carlin Stand Up: + template: {name: Other Collection} imdb_list: https://www.imdb.com/list/ls070221411/ summary: Collection of George Carlin's Stand Up Comedy HBO Specials - schedule: daily George Lopez Stand Up: + template: {name: Other Collection} imdb_list: https://www.imdb.com/list/ls086584751/ summary: Collection of George Lopez's Stand Up Comedy Specials - schedule: daily George of the Jungle: - tmdb_collection_details: 126221 - schedule: daily + template: {name: Collection, collection: 126221} Ghost in the Shell: - tmdb_collection_details: 23026 - schedule: daily + template: {name: Collection, collection: 23026} Ghost Rider: - tmdb_collection_details: 90306 - schedule: daily + template: {name: Collection, collection: 90306} Ghostbusters: - tmdb_collection_details: 2980 - tmdb_movie: 43074 - schedule: daily + template: {name: Collection Movie, collection: 2980, movie: 43074} The Girl - Millennium: - tmdb_collection_details: 575987 - schedule: daily + template: {name: Collection, collection: 575987} The Godfather: - tmdb_collection_details: 230 - schedule: daily + template: {name: Collection, collection: 230} Godzilla (Showa): - tmdb_collection_details: 374509 - tmdb_movie: 18983 + template: {name: Collection Movie, collection: 374509, movie: 18983} sort_title: Godzilla 01 (Showa) - schedule: daily Godzilla (Heisei): - tmdb_collection_details: 374511 - tmdb_movie: 39256 + template: {name: Collection, collection: 374511, movie: 39256} sort_title: Godzilla 02 (Heisei) - schedule: daily Godzilla (Millennium): - tmdb_collection_details: 374512 + template: {name: Collection, collection: 374512} sort_title: Godzilla 04 (Millennium) - schedule: daily Godzilla (MonsterVerse): - tmdb_collection_details: 535313 - tmdb_movie: 293167 + template: {name: Collection, collection: 535313, movie: 293167} sort_title: Godzilla 05 (MonsterVerse) - schedule: daily Godzilla (Anime): - tmdb_collection_details: 535790 + template: {name: Collection, collection: 535790} sort_title: Godzilla 07 (Anime) - schedule: daily A Goofy Movie: - tmdb_collection_details: 410261 - schedule: daily + template: {name: Collection, collection: 410261} Goosebumps: - tmdb_collection_details: 508783 - schedule: daily + template: {name: Collection, collection: 508783} Guardians of the Galaxy: - tmdb_collection_details: 284433 - schedule: daily + template: {name: Collection, collection: 284433} Green Street Hooligans: - tmdb_collection_details: 152544 - schedule: daily + template: {name: Collection, collection: 152544} Grown Ups: - tmdb_collection_details: 180546 - schedule: daily + template: {name: Collection, collection: 180546} Halloween: - tmdb_collection_details: 91361, 126209 - schedule: daily + template: {name: Collection, collection: "91361, 126209"} Halo: + template: {name: Other Collection} tmdb_list_details: 7070832 - schedule: daily The Hangover: - tmdb_collection_details: 86119 - schedule: daily + template: {name: Collection, collection: 86119} Hannibal Lecter: - tmdb_collection_details: 9743 - tmdb_movie: 11454 - schedule: daily + template: {name: Collection Movie, collection: 9743, movie: 11454} Happy Death Day: - tmdb_collection_details: 526380 - schedule: daily + template: {name: Collection, collection: 526380} Happy Feet: - tmdb_collection_details: 92012 - schedule: daily + template: {name: Collection, collection: 92012} Harold & Kumar: - tmdb_collection_details: 30663 - schedule: daily + template: {name: Collection, collection: 30663} Harry Potter: - tmdb_collection_details: 1241 - schedule: daily + template: {name: Collection, collection: 1241} ... Has Fallen: - tmdb_collection_details: 508783 + template: {name: Collection, collection: 508783} name_mapping: Has Fallen - schedule: daily Hellboy: - tmdb_collection_details: 508783 - schedule: daily + template: {name: Collection, collection: 508783} Hellboy (Animated): - tmdb_collection_details: 123203 - schedule: daily + template: {name: Collection, collection: 123203} High School Musical: - tmdb_collection_details: 87253 - schedule: daily + template: {name: Collection, collection: 87253} Highlander: - tmdb_collection_details: 8050 - schedule: daily + template: {name: Collection, collection: 8050} The Hobbit: - tmdb_collection_details: 121938 - schedule: daily + template: {name: Collection, collection: 121938} Home Alone: - tmdb_collection_details: 9888 - schedule: daily + template: {name: Collection, collection: 9888} Honey, I Shrunk the Kids: - tmdb_collection_details: 72119 - schedule: daily + template: {name: Collection, collection: 72119} Horrible Bosses: - tmdb_collection_details: 280588 - schedule: daily + template: {name: Collection, collection: 280588} Hot Tub Time Machine: - tmdb_collection_details: 313576 - schedule: daily + template: {name: Collection, collection: 313576} Hotel Transylvania: - tmdb_collection_details: 185103 - schedule: daily + template: {name: Collection, collection: 185103} House of 1000 Corpses: - tmdb_collection_details: 105625 - schedule: daily + template: {name: Collection, collection: 105625} How to Train Your Dragon: - tmdb_collection_details: 89137 - schedule: daily + template: {name: Collection, collection: 89137} The Human Centipede: - tmdb_collection_details: 96671 - schedule: daily + template: {name: Collection, collection: 96671} The Hunchback of Notre Dame: - tmdb_collection_details: 97456 - schedule: daily + template: {name: Collection, collection: 97456} The Hunger Games: - tmdb_collection_details: 131635 - schedule: daily + template: {name: Collection, collection: 131635} The Huntsman: - tmdb_collection_details: 393379 - schedule: daily + template: {name: Collection, collection: 393379} Ice Age: - tmdb_collection_details: 8354 - tmdb_movie: 79218, 717095, 387893 - schedule: daily + template: {name: Collection Movie, collection: 8354, movie: "79218, 717095, 387893"} The Incredibles: - tmdb_collection_details: 468222 - schedule: daily + template: {name: Collection, collection: 468222} Independence Day: - tmdb_collection_details: 304378 - schedule: daily + template: {name: Collection, collection: 304378} Indiana Jones: - tmdb_collection_details: 84 - schedule: daily + template: {name: Collection, collection: 84} Ip Man: - tmdb_collection_details: 70068 - tmdb_movie: 658009, 643413, 450001, 751391, 44249, 182127, 44865 + template: {name: Collection Movie, collection: 70068, movie: "658009, 643413, 450001, 751391, 44249, 182127, 44865"} collection_order: alpha - schedule: daily Iron Man: - tmdb_collection_details: 131292 - schedule: daily + template: {name: Collection, collection: 131292} It: - tmdb_collection_details: 477962 - schedule: daily + template: {name: Collection, collection: 477962} James Bond: - tmdb_collection_details: 645 - schedule: daily + template: {name: Collection, collection: 645} Jaws: - tmdb_collection_details: 2366 - schedule: daily + template: {name: Collection, collection: 2366} Jay and Silent Bob: - tmdb_collection_details: 726870 - schedule: daily + template: {name: Collection, collection: 726870} Jeff Dunham Stand Up: + template: {name: Other Collection} imdb_list: https://www.imdb.com/list/ls086022668/ summary: Collection of Jeff Dunham's Stand Up Comedy Specials - schedule: daily John Wick: - tmdb_collection_details: 404609 - schedule: daily + template: {name: Collection, collection: 404609} Johnny Tsunami: - tmdb_collection_details: 394316 - schedule: daily + template: {name: Collection, collection: 394316} Jumanji: - tmdb_collection_details: 495527 - schedule: daily + template: {name: Collection, collection: 495527} Jump Street: - tmdb_collection_details: 212562 - schedule: daily + template: {name: Collection, collection: 212562} The Jungle Book: - tmdb_collection_details: 97459 - schedule: daily + template: {name: Collection, collection: 97459} Jurassic Park: - tmdb_collection_details: 328 - tmdb_movie: 630322 - schedule: daily + template: {name: Collection Movie, collection: 328, movie: 630322} The Karate Kid: - tmdb_collection_details: 8580 - tmdb_movie: 38575 - schedule: daily + template: {name: Collection Movie, collection: 8580, movie: 38575} Kevin Hart Stand Up: + template: {name: Other Collection} imdb_list: https://www.imdb.com/list/ls049792208/ summary: Collection of Kevin Hart's Stand Up Comedy Specials - schedule: daily Kick-Ass: - tmdb_collection_details: 179892 - schedule: daily + template: {name: Collection, collection: 179892} Kill Bill: - tmdb_collection_details: 2883 - schedule: daily + template: {name: Collection, collection: 2883} Kingsman: - tmdb_collection_details: 391860 - schedule: daily + template: {name: Collection, collection: 391860} Kung Fu Panda: - tmdb_collection_details: 77816 - schedule: daily + template: {name: Collection, collection: 77816} Lady and the Tramp: - tmdb_collection_details: 97460 - schedule: daily + template: {name: Collection, collection: 97460} Lake Placid: - tmdb_collection_details: 97768 - schedule: daily + template: {name: Collection, collection: 97768} The Land Before Time: - tmdb_collection_details: 19163 - schedule: daily + template: {name: Collection, collection: 19163} Legally Blonde: - tmdb_collection_details: 86024 - schedule: daily + template: {name: Collection, collection: 86024} LEGO DC Comics Super Heroes: - tmdb_collection_details: 386162 - schedule: daily + template: {name: Collection, collection: 386162} The Lego Movie: - tmdb_collection_details: 325470 - schedule: daily + template: {name: Collection, collection: 325470} Lego Star Wars: - tmdb_collection_details: 302331 - schedule: daily + template: {name: Collection, collection: 302331} Lethal Weapon: - tmdb_collection_details: 945 - schedule: daily + template: {name: Collection, collection: 945} Lilo & Stitch: - tmdb_collection_details: 97461 - schedule: daily + template: {name: Collection, collection: 97461} The Lion King: - tmdb_collection_details: 94032 - schedule: daily + template: {name: Collection, collection: 94032} The Little Mermaid: - tmdb_collection_details: 33085 - schedule: daily + template: {name: Collection, collection: 33085} The Lord of the Rings: - tmdb_collection_details: 119 - schedule: daily + template: {name: Collection, collection: 119} The Lord of the Rings (Animated): - tmdb_collection_details: 141290 - schedule: daily + template: {name: Collection, collection: 141290} Mad Max: - tmdb_collection_details: 8945 - schedule: daily + template: {name: Collection, collection: 8945} Madagascar: - tmdb_collection_details: 14740 - tmdb_movie: 161143, 25472, 270946 - schedule: daily + template: {name: Collection, collection: 14740, movie: "161143, 25472, 270946"} Maleficent: - tmdb_collection_details: 531331 - schedule: daily + template: {name: Collection, collection: 531331} Mall Cop: - tmdb_collection_details: 328372 - schedule: daily + template: {name: Collection, collection: 328372} The Man with No Name: + template: {name: Other 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. - schedule: daily Marvel Rising: - tmdb_collection_details: 627234 - schedule: daily + template: {name: Collection, collection: 627234} Marx Brothers: + template: {name: Other 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. - schedule: daily Mary Poppins: - tmdb_collection_details: 527439 - schedule: daily + template: {name: Collection, collection: 527439} The Mask: - tmdb_collection_details: 43072 - schedule: daily + template: {name: Collection, collection: 43072} The Matrix: - tmdb_collection_details: 2344 - schedule: daily + template: {name: Collection, collection: 2344} Maya the Bee: - tmdb_collection_details: 522250 - schedule: daily + template: {name: Collection, collection: 522250} The Maze Runner: - tmdb_collection_details: 295130 - schedule: daily + template: {name: Collection, collection: 295130} Mean Girls: - tmdb_collection_details: 99606 - schedule: daily + template: {name: Collection, collection: 99606} Meet the Parents: - tmdb_collection_details: 51509 - schedule: daily + template: {name: Collection, collection: 51509} Men In Black: - tmdb_collection_details: 86055 - schedule: daily + template: {name: Collection, collection: 86055} The Mighty Ducks: - tmdb_collection_details: 10709 - schedule: daily + template: {name: Collection, collection: 10709} "Mission: Impossible": - tmdb_collection_details: 87359 + template: {name: Collection, collection: 87359} name_mapping: Mission Impossible - schedule: daily Monsters, Inc.: - tmdb_collection_details: 137696 - schedule: daily + template: {name: Collection, collection: 137696} Monty Python: + template: {name: Other 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 - schedule: daily - add_to_arr: false Mortal Kombat: - tmdb_collection_details: 9818 - tmdb_movie: 664767 - schedule: daily + template: {name: Collection Movie, collection: 9818, movie: 664767} Mothra: - tmdb_collection_details: 171732 - tmdb_movie: 39410 - schedule: daily + template: {name: Collection Movie, collection: 171732, movie: 39410} Mulan: - tmdb_collection_details: 87236 - schedule: daily + template: {name: Collection, collection: 87236} The Mummy: - tmdb_collection_details: 1733 - schedule: daily + template: {name: Collection, collection: 1733} The Muppet: - tmdb_collection_details: 256377 - schedule: daily + template: {name: Collection, collection: 256377} National Treasure: - tmdb_collection_details: 52984 - schedule: daily + template: {name: Collection, collection: 52984} Neighbors: - tmdb_collection_details: 400700 - schedule: daily + template: {name: Collection, collection: 400700} The Neverending Story: - tmdb_collection_details: 91430 - schedule: daily + template: {name: Collection, collection: 91430} Night at the Museum: - tmdb_collection_details: 85943 - schedule: daily + template: {name: Collection, collection: 85943} A Nightmare on Elm Street: - tmdb_collection_details: 8581 - tmdb_movie: 6466, 23437 - schedule: daily + template: {name: Collection Movie, collection: 8581, movie: "6466, 23437"} Now You See Me: - tmdb_collection_details: 382685 - schedule: daily + template: {name: Collection, collection: 382685} Ocean's: - tmdb_collection_details: 304 - schedule: daily + template: {name: Collection, collection: 304} Ong Bak: - tmdb_collection_details: 94589 - schedule: daily + template: {name: Collection, collection: 94589} Oz: - tmdb_collection_details: 627517 - tmdb_movie: 13155, 68728 - schedule: daily + template: {name: Collection Movie, collection: 627517, movie: "13155, 68728"} Pacific Rim: - tmdb_collection_details: 363369 - schedule: daily + template: {name: Collection, collection: 363369} Paddington: - tmdb_collection_details: 488924 - schedule: daily + template: {name: Collection, collection: 488924} Parasyte: - tmdb_collection_details: 385386 - schedule: daily + template: {name: Collection, collection: 385386} Percy Jackson: - tmdb_collection_details: 179919 - schedule: daily + template: {name: Collection, collection: 179919} Pet Sematary: - tmdb_collection_details: 10789 - tmdb_movie: 157433 - schedule: daily + template: {name: Collection Movie, collection: 10789, movie: 157433} Peter Pan: - tmdb_collection_details: 55422 - schedule: daily + template: {name: Collection, collection: 55422} Pirates of the Caribbean: - tmdb_collection_details: 295 - schedule: daily + template: {name: Collection, collection: 295} Pitch Perfect: - tmdb_collection_details: 306031 - schedule: daily + template: {name: Collection, collection: 306031} Planes: - tmdb_collection_details: 270252 - schedule: daily + template: {name: Collection, collection: 270252} Planet of the Apes: - tmdb_collection_details: 173710 - schedule: daily + template: {name: Collection, collection: 173710} Pocahontas: - tmdb_collection_details: 136214 - schedule: daily + template: {name: Collection, collection: 136214} Pokémon: + template: {name: Other 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. - schedule: daily Police Story: - tmdb_collection_details: 269098 - schedule: daily + template: {name: Collection, collection: 269098} Power Rangers: - tmdb_collection_details: 708816 - tmdb_movie: 305470, 306264 - schedule: daily + template: {name: Collection Movie, collection: 708816, movie: "305470, 306264"} Predator: - tmdb_collection_details: 399 - schedule: daily + template: {name: Collection, collection: 399} The Princess Diaries: - tmdb_collection_details: 107674 - schedule: daily + template: {name: Collection, collection: 107674} The Purge: - tmdb_collection_details: 256322 - schedule: daily + template: {name: Collection, collection: 256322} Quarantine: - tmdb_collection_details: 123932 - schedule: daily + template: {name: Collection, collection: 123932} Rambo: - tmdb_collection_details: 5039 - schedule: daily + template: {name: Collection, collection: 5039} Red Cliff: - tmdb_collection_details: 96677 - schedule: daily + template: {name: Collection, collection: 96677} Rent: + template: {name: Other Collection} tmdb_list_details: 7072241 - schedule: daily The Rescuers: - tmdb_collection_details: 57971 - schedule: daily + template: {name: Collection, collection: 57971} Resident Evil: - tmdb_collection_details: 17255 - schedule: daily + template: {name: Collection, collection: 17255} "Resident Evil: Biohazard": - tmdb_collection_details: 133352 + template: {name: Collection, collection: 133352} name_mapping: Resident Evil Biohazard - schedule: daily Ride Along: - tmdb_collection_details: 376650 - schedule: daily + template: {name: Collection, collection: 376650} Rio: - tmdb_collection_details: 229932 - schedule: daily + template: {name: Collection, collection: 229932} Robert Langdon: - tmdb_collection_details: 115776 - schedule: daily + template: {name: Collection, collection: 115776} RoboCop: - tmdb_collection_details: 5547 - schedule: daily + template: {name: Collection, collection: 5547} Rocky: - tmdb_collection_details: 1575 - schedule: daily + template: {name: Collection, collection: 1575} Rugrats: - tmdb_collection_details: 57129 - schedule: daily + template: {name: Collection, collection: 57129} Rurouni Kenshin: - tmdb_collection_details: 247028 - schedule: daily + template: {name: Collection, collection: 247028} Rush Hour: - tmdb_collection_details: 90863 - schedule: daily + template: {name: Collection, collection: 90863} The Sandlot: - tmdb_collection_details: 87214 - schedule: daily + template: {name: Collection, collection: 87214} The Santa Clause: - tmdb_collection_details: 53159 - schedule: daily + template: {name: Collection, collection: 53159} Santa Paws: - tmdb_collection_details: 469648 - schedule: daily + template: {name: Collection, collection: 469648} Saw: - tmdb_collection_details: 656 - schedule: daily + template: {name: Collection, collection: 656} Scary Movie: - tmdb_collection_details: 4246 - schedule: daily + template: {name: Collection, collection: 4246} The Scorpion King: - tmdb_collection_details: 116669 - schedule: daily + template: {name: Collection, collection: 116669} Scream: - tmdb_collection_details: 2602 - schedule: daily + template: {name: Collection, collection: 2602} The Secret Life of Pets: - tmdb_collection_details: 427084 - schedule: daily + template: {name: Collection, collection: 427084} Shaft: - tmdb_collection_details: 495, 608103 - schedule: daily + template: {name: Collection, collection: "495, 608103"} Shanghai Noon: - tmdb_collection_details: 59567 - schedule: daily + template: {name: Collection, collection: 59567} Sharknado: - tmdb_collection_details: 286023 - schedule: daily + template: {name: Collection, collection: 286023} Sherlock Holmes: - tmdb_collection_details: 102322 - schedule: daily + template: {name: Collection, collection: 102322} The Shining: - tmdb_collection_details: 530064 - schedule: daily + template: {name: Collection, collection: 530064} Shrek: - tmdb_collection_details: 2150 - schedule: daily + template: {name: Collection, collection: 2150} Silent Hill: - tmdb_collection_details: 64748 - schedule: daily + template: {name: Collection, collection: 64748} Slap Shot: - tmdb_collection_details: 261526 - schedule: daily + template: {name: Collection, collection: 261526} The Smurfs: - tmdb_collection_details: 134897 - schedule: daily + template: {name: Collection, collection: 134897} Spider-Man (Avengers): - tmdb_collection_details: 531241 - schedule: daily + template: {name: Collection, collection: 531241} Spider-Man (Original): - tmdb_collection_details: 556 - schedule: daily + template: {name: Collection, collection: 556} Spy Kids: - tmdb_collection_details: 86486 - schedule: daily + template: {name: Collection, collection: 86486} "Star Trek: Alternate Reality": - tmdb_collection_details: 115575 + template: {name: Collection, collection: 115575} name_mapping: Star Trek Alternate Reality - schedule: daily "Star Trek: The Next Generation": - tmdb_collection_details: 115570 + template: {name: Collection, collection: 115570} name_mapping: Star Trek The Next Generation - schedule: daily "Star Trek: The Original Series": - tmdb_collection_details: 151 + template: {name: Collection, collection: 151} name_mapping: Star Trek The Original Series - schedule: daily "Star Wars: Skywalker Saga": - tmdb_collection_details: 10 + template: {name: Collection, collection: 10} name_mapping: Star Wars Skywalker Saga - schedule: daily "Star Wars: Legends": + template: {name: Other Collection} tmdb_movie: 348350, 330459 summary: "Star Wars Anthology Films and other Star Wars Movies" name_mapping: Star Wars Legends - schedule: daily Step Up: - tmdb_collection_details: 86092 - schedule: daily + template: {name: Collection, collection: 86092} Street Fighter: - tmdb_collection_details: 190435 - tmdb_movie: 687354, 11667 - schedule: daily + template: {name: Collection Movie, collection: 190435, movie: "687354, 11667"} Stuart Little: - tmdb_collection_details: 99727 - schedule: daily + template: {name: Collection, collection: 99727} Super Troopers: - tmdb_collection_details: 449462 - schedule: daily + template: {name: Collection, collection: 449462} Superman (Original): - tmdb_collection_details: 8537 - schedule: daily + template: {name: Collection, collection: 8537} Surf's Up: - tmdb_collection_details: 436295 - schedule: daily + template: {name: Collection, collection: 436295} Taken: - tmdb_collection_details: 135483 - schedule: daily + template: {name: Collection, collection: 135483} Tarzan: - tmdb_collection_details: 106768 - schedule: daily + template: {name: Collection, collection: 106768} Ted: - tmdb_collection_details: 266672 - schedule: daily + template: {name: Collection, collection: 266672} Teenage Mutant Ninja Turtles: - tmdb_collection_details: 1582, 401562 - tmdb_movie: 1273 - schedule: daily + template: {name: Collection Movie, collection: "1582, 401562", movie: 1273} Tekken: - tmdb_collection_details: 294172 - schedule: daily + template: {name: Collection, collection: 294172} The Terminator: - tmdb_collection_details: 528 - schedule: daily + template: {name: Collection, collection: 528} Texas Chainsaw Massacre: - tmdb_collection_details: 111751, 425175 - schedule: daily + template: {name: Collection, collection: "111751, 425175"} Thor: - tmdb_collection_details: 131296 - schedule: daily + template: {name: Collection, collection: 131296} The Three Stooges: + template: {name: Other 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. - schedule: daily Tinker Bell: - tmdb_collection_details: 315595 - schedule: daily + template: {name: Collection, collection: 315595} Tokyo Ghoul: - tmdb_collection_details: 551278 - schedule: daily + template: {name: Collection, collection: 551278} Tom and Jerry: + template: {name: Other Collection} imdb_list: https://www.imdb.com/list/ls022966050/ summary: Tom and Jerry's animated feature-length films based on the series. - schedule: daily Tomb Raider: - tmdb_collection_details: 2467, 621142 - schedule: daily + template: {name: Collection, collection: "2467, 621142"} Tomie: - tmdb_collection_details: 139394 - schedule: daily + template: {name: Collection, collection: 139394} Toy Story: - tmdb_collection_details: 10194 - tmdb_movie: 130925 - schedule: daily + template: {name: Collection Movie, collection: 10194, movie: 130925} Trainspotting: - tmdb_collection_details: 424202 - schedule: daily + template: {name: Collection, collection: 424202} Transformers: - tmdb_collection_details: 8650 - schedule: daily + template: {name: Collection, collection: 8650} The Transporter: - tmdb_collection_details: 9518 - schedule: daily + template: {name: Collection, collection: 9518} Tremors: - tmdb_collection_details: 91799 - schedule: daily + template: {name: Collection, collection: 91799} Trolls: - tmdb_collection_details: 489724 - schedule: daily + template: {name: Collection, collection: 489724} TRON: - tmdb_collection_details: 63043 - tmdb_movie: 73362 - schedule: daily + template: {name: Collection Movie, collection: 63043, movie: 73362} Unbreakable: + template: {name: Other 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). - schedule: daily Underworld: - tmdb_collection_details: 2326 - schedule: daily + template: {name: Collection, collection: 2326} Viy: - tmdb_collection_details: 428046 - schedule: daily + template: {name: Collection, collection: 428046} Wall Street: - tmdb_collection_details: 52783 - schedule: daily + template: {name: Collection, collection: 52783} Wallace & Gromit: - tmdb_collection_details: 529 - schedule: daily + template: {name: Collection, collection: 529} Wayne's World: - tmdb_collection_details: 8979 - schedule: daily + template: {name: Collection, collection: 8979} Wonder Woman: - tmdb_collection_details: 468552 - schedule: daily + template: {name: Collection, collection: 468552} Wreck-It Ralph: - tmdb_collection_details: 404825 - schedule: daily + template: {name: Collection, collection: 404825} X-Men: - tmdb_collection_details: 748, 453993, 448150 - tmdb_movie: 567604 - schedule: daily + template: {name: Collection Movie, collection: "748, 453993, 448150", movie: 567604} xXx: - tmdb_collection_details: 52785 - schedule: daily + template: {name: Collection, collection: 52785} Zenon: - tmdb_collection_details: 321148 - schedule: daily + template: {name: Collection, collection: 321148} Zombieland: - tmdb_collection_details: 537982 - schedule: daily + template: {name: Collection, collection: 537982} Zoolander: - tmdb_collection_details: 352789 - schedule: daily + template: {name: Collection, collection: 352789} Zorro: - tmdb_collection_details: 1657 - schedule: daily + template: {name: Collection, collection: 1657} ###################################################### # Collectionless Collection # @@ -2198,6 +1303,10 @@ collections: sort_title: ~_Collectionless collection_order: alpha +###################################################### +# Metadata Edits # +###################################################### + metadata: The Adventures of Ichabod and Mr. Toad: content_rating: G diff --git a/config/config.yml.template b/config/config.yml.template index 185049721..ab29f74a9 100644 --- a/config/config.yml.template +++ b/config/config.yml.template @@ -1,3 +1,5 @@ +## This file is a template remove the .template to use the file + libraries: Movies: library_type: movie @@ -5,16 +7,24 @@ libraries: library_type: show Anime: library_type: show -cache: +settings: # Can be individually specified per library as well cache: true cache_expiration: 60 -plex: # Can be individually specified per library as well - url: http://192.168.1.12:32400 - token: #################### - sync_mode: append asset_directory: config/assets + sync_mode: append show_unmanaged: true show_filtered: false + show_missing: true + save_missing: true +plex: # Can be individually specified per library as well + url: http://192.168.1.12:32400 + token: #################### +tmdb: + apikey: ################################ + language: en +tautulli: # Can be individually specified per library as well + url: http://192.168.1.12:8181 + apikey: ################################ radarr: # Can be individually specified per library as well url: http://192.168.1.12:7878 token: ################################ @@ -31,12 +41,6 @@ sonarr: # Can be individually specified root_folder_path: "S:/TV Shows" add: false search: false -tautulli: # Can be individually specified per library as well - url: http://192.168.1.12:8181 - apikey: ################################ -tmdb: - apikey: ################################ - language: en trakt: client_id: ################################################################ client_secret: ################################################################ diff --git a/modules/builder.py b/modules/builder.py new file mode 100644 index 000000000..82c5dcc92 --- /dev/null +++ b/modules/builder.py @@ -0,0 +1,754 @@ +import glob, logging, os, re +from datetime import datetime, timedelta +from modules import util +from modules.util import Failed + +logger = logging.getLogger("Plex Meta Manager") + +class CollectionBuilder: + def __init__(self, config, library, name, data): + self.config = config + self.library = library + self.name = name + self.data = data + self.details = { + "arr_tag": None, + "show_filtered": library.show_filtered, + "show_missing": library.show_missing, + "save_missing": library.save_missing + } + self.methods = [] + self.filters = [] + self.posters = [] + self.backgrounds = [] + self.schedule = None + + if "template" in data: + if not self.library.templates: + raise Failed("Collection Error: No templates found") + elif not data["template"]: + raise Failed("Collection Error: template attribute is blank") + else: + for data_template in util.get_list(data["template"], split=False): + if not isinstance(data_template, dict): + raise Failed("Collection Error: template attribute is not a dictionary") + elif "name" not in data_template: + raise Failed("Collection Error: template sub-attribute name is required") + elif not data_template["name"]: + raise Failed("Collection Error: template sub-attribute name is blank") + elif data_template["name"] not in self.library.templates: + raise Failed("Collection Error: template {} not found".format(data_template["name"])) + elif not isinstance(self.library.templates[data_template["name"]], dict): + raise Failed("Collection Error: template {} is not a dictionary".format(data_template["name"])) + else: + for tm in data_template: + if not data_template[tm]: + raise Failed("Collection Error: template sub-attribute {} is blank".format(data_template[tm])) + + template_name = data_template["name"] + template = self.library.templates[template_name] + default = {} + if "default" in template: + if template["default"]: + if isinstance(template["default"], dict): + for dv in template["default"]: + if template["default"][dv]: + default[dv] = template["default"][dv] + else: + raise Failed("Collection Error: template default sub-attribute {} is blank".format(dv)) + else: + raise Failed("Collection Error: template sub-attribute default is not a dictionary") + else: + raise Failed("Collection Error: template sub-attribute default is blank") + + + + for m in template: + if m not in self.data and m != "default": + if template[m]: + attr = None + def replace_txt(txt): + txt = str(txt) + for tm in data_template: + if tm != "name" and "<<{}>>".format(tm) in txt: + txt = txt.replace("<<{}>>".format(tm), str(data_template[tm])) + if "<<collection_name>>" in txt: + txt = txt.replace("<<collection_name>>", str(self.name)) + for dm in default: + if "<<{}>>".format(dm) in txt: + txt = txt.replace("<<{}>>".format(dm), str(default[dm])) + if txt in ["true", "True"]: return True + elif txt in ["false", "False"]: return False + else: + try: return int(txt) + except ValueError: return txt + if isinstance(template[m], dict): + attr = {} + for sm in template[m]: + if isinstance(template[m][sm], list): + temp_list = [] + for li in template[m][sm]: + temp_list.append(replace_txt(li)) + attr[sm] = temp_list + else: + attr[sm] = replace_txt(template[m][sm]) + elif isinstance(template[m], list): + attr = [] + for li in template[m]: + if isinstance(li, dict): + temp_dict = {} + for sm in li: + temp_dict[sm] = replace_txt(li[sm]) + attr.append(temp_dict) + else: + attr.append(replace_txt(li)) + else: + attr = replace_txt(template[m]) + self.data[m] = attr + else: + raise Failed("Collection Error: template attribute {} is blank".format(m)) + + skip_collection = True + if "schedule" not in data: + skip_collection = False + elif not data["schedule"]: + logger.error("Collection Error: schedule attribute is blank. Running daily") + skip_collection = False + else: + schedule_list = util.get_list(data["schedule"]) + current_time = datetime.now() + next_month = current_time.replace(day=28) + timedelta(days=4) + last_day = next_month - timedelta(days=next_month.day) + for schedule in schedule_list: + run_time = str(schedule).lower() + if run_time.startswith("day") or run_time.startswith("daily"): + skip_collection = False + if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): + match = re.search("\\(([^)]+)\\)", run_time) + if match: + param = match.group(1) + if run_time.startswith("week"): + if param.lower() in util.days_alias: + weekday = util.days_alias[param.lower()] + self.schedule += "\nScheduled weekly on {}".format(util.pretty_days[weekday]) + if weekday == current_time.weekday(): + skip_collection = False + else: + logger.error("Collection Error: weekly schedule attribute {} invalid must be a day of the weeek i.e. weekly(Monday)".format(schedule)) + elif run_time.startswith("month"): + try: + if 1 <= int(param) <= 31: + self.schedule += "\nScheduled monthly on the {}".format(util.make_ordinal(param)) + if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day): + skip_collection = False + else: + logger.error("Collection Error: monthly schedule attribute {} invalid must be between 1 and 31".format(schedule)) + except ValueError: + logger.error("Collection Error: monthly schedule attribute {} invalid must be an integer".format(schedule)) + elif run_time.startswith("year"): + match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param) + if match: + month = int(match.group(1)) + day = int(match.group(2)) + self.schedule += "\nScheduled yearly on {} {}".format(util.pretty_months[month], util.make_ordinal(day)) + if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)): + skip_collection = False + else: + logger.error("Collection Error: yearly schedule attribute {} invalid must be in the MM/DD format i.e. yearly(11/22)".format(schedule)) + else: + logger.error("Collection Error: failed to parse schedule: {}".format(schedule)) + else: + logger.error("Collection Error: schedule attribute {} invalid".format(schedule)) + if self.schedule is None: + skip_collection = False + if skip_collection: + raise Failed("Skipping Collection {}".format(c)) + + logger.info("Scanning {} Collection".format(self.name)) + + self.collectionless = "plex_collectionless" in data + + self.sync = self.library.sync_mode == "sync" + if "sync_mode" in data: + if not data["sync_mode"]: logger.warning("Collection Warning: sync_mode attribute is blank using general: {}".format(self.library.sync_mode)) + elif data["sync_mode"] not in ["append", "sync"]: logger.warning("Collection Warning: {} sync_mode invalid using general: {}".format(self.library.sync_mode, data["sync_mode"])) + else: self.sync = data["sync_mode"] == "sync" + + if "tmdb_person" in data: + if data["tmdb_person"]: + valid_names = [] + for tmdb_id in util.get_int_list(data["tmdb_person"], "TMDb Person ID"): + person = config.TMDb.get_person(tmdb_id) + valid_names.append(person.name) + if "summary" not in self.details and hasattr(person, "biography") and person.biography: + self.details["summary"] = person.biography + if "poster" not in self.details and hasattr(person, "profile_path") and person.profile_path: + self.details["poster"] = ("url", "{}{}".format(config.TMDb.image_url, person.profile_path), "tmdb_person") + if len(valid_names) > 0: self.details["tmdb_person"] = valid_names + else: raise Failed("Collection Error: No valid TMDb Person IDs in {}".format(data["tmdb_person"])) + else: + raise Failed("Collection Error: tmdb_person attribute is blank") + + for m in data: + if "tmdb" in m and not config.TMDb: raise Failed("Collection Error: {} requires TMDb to be configured".format(m)) + elif "trakt" in m and not config.Trakt: raise Failed("Collection Error: {} requires Trakt todo be configured".format(m)) + elif "imdb" in m and not config.IMDb: raise Failed("Collection Error: {} requires TMDb or Trakt to be configured".format(m)) + elif "tautulli" in m and not self.library.Tautulli: raise Failed("Collection Error: {} requires Tautulli to be configured".format(m)) + elif "mal" in m and not config.MyAnimeList: raise Failed("Collection Error: {} requires MyAnimeList to be configured".format(m)) + elif data[m] is not None: + logger.debug("") + logger.debug("Method: {}".format(m)) + logger.debug("Value: {}".format(data[m])) + if m in util.method_alias: + method_name = util.method_alias[m] + logger.warning("Collection Warning: {} attribute will run as {}".format(m, method_name)) + else: + method_name = m + if method_name in util.show_only_lists and self.library.is_movie: + raise Failed("Collection Error: {} attribute only works for show libraries".format(method_name)) + elif method_name in util.movie_only_lists and self.library.is_show: + raise Failed("Collection Error: {} attribute only works for movie libraries".format(method_name)) + elif method_name in util.movie_only_searches and self.library.is_show: + raise Failed("Collection Error: {} plex search only works for movie libraries".format(method_name)) + elif method_name not in util.collectionless_lists and self.collectionless: + raise Failed("Collection Error: {} attribute does not work for Collectionless collection".format(method_name)) + elif method_name == "tmdb_summary": + self.details["summary"] = config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).overview + elif method_name == "tmdb_description": + self.details["summary"] = config.TMDb.get_list(util.regex_first_int(data[m], "TMDb List ID")).description + elif method_name == "tmdb_biography": + self.details["summary"] = config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).biography + elif method_name == "collection_mode": + if data[m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]: + if data[m] == "hide_items": self.details[method_name] = "hideItems" + elif data[m] == "show_items": self.details[method_name] = "showItems" + else: self.details[method_name] = data[m] + else: + raise Failed("Collection Error: {} collection_mode Invalid\n| \tdefault (Library default)\n| \thide (Hide Collection)\n| \thide_items (Hide Items in this Collection)\n| \tshow_items (Show this Collection and its Items)".format(data[m])) + elif method_name == "collection_order": + if data[m] in ["release", "alpha"]: + self.details[method_name] = data[m] + else: + raise Failed("Collection Error: {} collection_order Invalid\n| \trelease (Order Collection by release dates)\n| \talpha (Order Collection Alphabetically)".format(data[m])) + elif method_name == "url_poster": + self.posters.append(("url", data[m], method_name)) + elif method_name == "tmdb_poster": + self.posters.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).poster_path), method_name)) + elif method_name == "tmdb_profile": + self.posters.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_person(util.regex_first_int(data[m], "TMDb Person ID")).profile_path), method_name)) + elif method_name == "file_poster": + if os.path.exists(data[m]): self.posters.append(("file", os.path.abspath(data[m]), method_name)) + else: raise Failed("Collection Error: Poster Path Does Not Exist: {}".format(os.path.abspath(data[m]))) + elif method_name == "url_background": + self.backgrounds.append(("url", data[m], method_name)) + elif method_name == "tmdb_background": + self.backgrounds.append(("url", "{}{}".format(config.TMDb.image_url, config.TMDb.get_movie_show_or_collection(util.regex_first_int(data[m], "TMDb ID"), self.library.is_movie).poster_path), method_name)) + elif method_name == "file_background": + if os.path.exists(data[m]): self.backgrounds.append(("file", os.path.abspath(data[m]), method_name)) + else: raise Failed("Collection Error: Background Path Does Not Exist: {}".format(os.path.abspath(data[m]))) + elif method_name == "label_sync_mode": + if data[m] in ["append", "sync"]: self.details[method_name] = data[m] + else: raise Failed("Collection Error: label_sync_mode attribute must be either 'append' or 'sync'") + elif method_name in ["arr_tag", "label"]: + self.details[method_name] = util.get_list(data[m]) + elif method_name in util.boolean_details: + if isinstance(data[m], bool): self.details[method_name] = data[m] + else: raise Failed("Collection Error: {} attribute must be either true or false".format(method_name)) + elif method_name in util.all_details: + self.details[method_name] = data[m] + elif method_name in ["year", "year.not"]: + self.methods.append(("plex_search", [[(method_name, util.get_year_list(data[m], method_name))]])) + elif method_name in ["decade", "decade.not"]: + self.methods.append(("plex_search", [[(method_name, util.get_int_list(data[m], util.remove_not(method_name)))]])) + elif method_name in util.tmdb_searches: + final_values = [] + for value in util.get_list(data[m]): + if value.lower() == "tmdb" and "tmdb_person" in self.details: + for name in self.details["tmdb_person"]: + final_values.append(name) + else: + final_values.append(value) + self.methods.append(("plex_search", [[(method_name, final_values)]])) + elif method_name in util.plex_searches: + self.methods.append(("plex_search", [[(method_name, util.get_list(data[m]))]])) + elif method_name == "plex_all": + self.methods.append((method_name, [""])) + elif method_name == "plex_collection": + self.methods.append((method_name, self.library.validate_collections(data[m] if isinstance(data[m], list) else [data[m]]))) + elif method_name == "anidb_popular": + list_count = util.regex_first_int(data[m], "List Size", default=40) + if 1 <= list_count <= 30: + self.methods.append((method_name, [list_count])) + else: + logger.warning("Collection Error: anidb_popular must be an integer between 1 and 30 defaulting to 30") + self.methods.append((method_name, [30])) + elif method_name == "mal_id": + 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 == "trakt_list": + self.methods.append((method_name, config.Trakt.validate_trakt_list(util.get_list(data[m])))) + elif method_name == "trakt_watchlist": + self.methods.append((method_name, config.Trakt.validate_trakt_watchlist(util.get_list(data[m]), self.library.is_movie))) + elif method_name == "imdb_list": + new_list = [] + for imdb_list in util.get_list(data[m], split=False): + new_dictionary = {} + if isinstance(imdb_list, dict): + if "url" in imdb_list and imdb_list["url"]: imdb_url = imdb_list["url"] + else: raise Failed("Collection Error: imdb_list attribute url is required") + list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) if "limit" in imdb_list and imdb_list["limit"] else 0 + else: + imdb_url = str(imdb_list) + list_count = 0 + new_list.append({"url": imdb_url, "limit": list_count}) + self.methods.append((method_name, new_list)) + elif method_name in util.dictionary_lists: + if isinstance(data[m], dict): + def get_int(parent, method, data, default, min=1, max=None): + if method not in data: logger.warning("Collection Warning: {} {} attribute not found using {} as default".format(parent, method, default)) + elif not data[method]: logger.warning("Collection Warning: {} {} attribute is blank using {} as default".format(parent, method, default)) + elif isinstance(data[method], int) and data[method] >= min: + if max is None or data[method] <= max: return data[method] + else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer <= {} using {} as default".format(parent, method, data[method], max, default)) + else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer >= {} using {} as default".format(parent, method, data[method], min, default)) + return default + if method_name == "filters": + for f in data[m]: + if f in util.method_alias or (f.endswith(".not") and f[:-4] in util.method_alias): + filter = (util.method_alias[f[:-4]] + f[-4:]) if f.endswith(".not") else util.method_alias[f] + logger.warning("Collection Warning: {} filter will run as {}".format(f, filter)) + else: + filter = f + if filter in util.movie_only_filters and self.library.is_show: raise Failed("Collection Error: {} filter only works for movie libraries".format(filter)) + elif data[m][f] is None: raise Failed("Collection Error: {} filter is blank".format(filter)) + elif filter in util.all_filters: self.filters.append((filter, data[m][f])) + else: raise Failed("Collection Error: {} filter not supported".format(filter)) + elif method_name == "plex_collectionless": + new_dictionary = {} + prefix_list = [] + if "exclude_prefix" in data[m] and data[m]["exclude_prefix"]: + if isinstance(data[m]["exclude_prefix"], list): prefix_list.extend(data[m]["exclude_prefix"]) + else: prefix_list.append(str(data[m]["exclude_prefix"])) + exact_list = [] + if "exclude" in data[m] and data[m]["exclude"]: + if isinstance(data[m]["exclude"], list): exact_list.extend(data[m]["exclude"]) + else: exact_list.append(str(data[m]["exclude"])) + if len(prefix_list) == 0 and len(exact_list) == 0: raise Failed("Collection Error: you must have at least one exclusion") + self.details["add_to_arr"] = False + self.details["collection_mode"] = "hide" + self.sync = True + new_dictionary["exclude_prefix"] = prefix_list + new_dictionary["exclude"] = exact_list + self.methods.append((method_name, [new_dictionary])) + elif method_name == "plex_search": + searches = [] + used = [] + for s in data[m]: + if s in util.method_alias or (s.endswith(".not") and s[:-4] in util.method_alias): + search = (util.method_alias[s[:-4]] + s[-4:]) if s.endswith(".not") else util.method_alias[s] + logger.warning("Collection Warning: {} plex search attribute will run as {}".format(s, search)) + else: + search = s + if search in util.movie_only_searches and self.library.is_show: + raise Failed("Collection Error: {} plex search attribute only works for movie libraries".format(search)) + elif util.remove_not(search) in used: + raise Failed("Collection Error: Only one instance of {} can be used try using it as a filter instead".format(search)) + elif search in ["year", "year.not"]: + years = util.get_year_list(data[m][s], search) + if len(years) > 0: + used.append(util.remove_not(search)) + searches.append((search, util.get_int_list(data[m][s], util.remove_not(search)))) + elif search in util.plex_searches: + used.append(util.remove_not(search)) + searches.append((search, util.get_list(data[m][s]))) + else: + logger.error("Collection Error: {} plex search attribute not supported".format(search)) + self.methods.append((method_name, [searches])) + elif method_name == "tmdb_discover": + new_dictionary = {"limit": 100} + for attr in data[m]: + if data[m][attr]: + attr_data = data[m][attr] + if (self.library.is_movie and attr in util.discover_movie) or (self.library.is_show and attr in util.discover_tv): + if attr == "language": + if re.compile("([a-z]{2})-([A-Z]{2})").match(str(attr_data)): + new_dictionary[attr] = str(attr_data) + else: + raise Failed("Collection Error: {} attribute {}: {} must match pattern ([a-z]{2})-([A-Z]{2}) e.g. en-US".format(m, attr, attr_data)) + elif attr == "region": + if re.compile("^[A-Z]{2}$").match(str(attr_data)): + new_dictionary[attr] = str(attr_data) + else: + raise Failed("Collection Error: {} attribute {}: {} must match pattern ^[A-Z]{2}$ e.g. US".format(m, attr, attr_data)) + elif attr == "sort_by": + if (self.library.is_movie and attr_data in util.discover_movie_sort) or (self.library.is_show and attr_data in util.discover_tv_sort): + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: {} is invalid".format(m, attr, attr_data)) + elif attr == "certification_country": + if "certification" in data[m] or "certification.lte" in data[m] or "certification.gte" in data[m]: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be used with either certification, certification.lte, or certification.gte".format(m, attr)) + elif attr in ["certification", "certification.lte", "certification.gte"]: + if "certification_country" in data[m]: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be used with certification_country".format(m, attr)) + elif attr in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: + if attr_data is True: + new_dictionary[attr] = attr_data + elif attr in ["primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte"]: + if re.compile("[0-1]?[0-9][/-][0-3]?[0-9][/-][1-2][890][0-9][0-9]").match(str(attr_data)): + the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") + new_dictionary[attr] = "{}-{}-{}".format(the_date[2], the_date[0], the_date[1]) + elif re.compile("[1-2][890][0-9][0-9][/-][0-1]?[0-9][/-][0-3]?[0-9]").match(str(attr_data)): + the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") + new_dictionary[attr] = "{}-{}-{}".format(the_date[0], the_date[1], the_date[2]) + else: + raise Failed("Collection Error: {} attribute {}: {} must match pattern MM/DD/YYYY e.g. 12/25/2020".format(m, attr, attr_data)) + elif attr in ["primary_release_year", "year", "first_air_date_year"]: + if isinstance(attr_data, int) and 1800 < attr_data and attr_data < 2200: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be a valid year e.g. 1990".format(m, attr)) + elif attr in ["vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte"]: + if (isinstance(attr_data, int) or isinstance(attr_data, float)) and 0 < attr_data: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be a valid number greater then 0".format(m, attr)) + elif attr in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {} not supported".format(m, attr)) + elif attr == "limit": + if isinstance(attr_data, int) and attr_data > 0: + new_dictionary[attr] = attr_data + else: + raise Failed("Collection Error: {} attribute {}: must be a valid number greater then 0".format(m, attr)) + else: + raise Failed("Collection Error: {} attribute {} not supported".format(m, attr)) + else: + raise Failed("Collection Error: {} parameter {} is blank".format(m, attr)) + if len(new_dictionary) > 1: + self.methods.append((method_name, [new_dictionary])) + else: + raise Failed("Collection Error: {} had no valid fields".format(m)) + elif "tautulli" in method_name: + new_dictionary = {} + if method_name == "tautulli_popular": new_dictionary["list_type"] = "popular" + elif method_name == "tautulli_watched": new_dictionary["list_type"] = "watched" + else: raise Failed("Collection Error: {} attribute not supported".format(method_name)) + + new_dictionary["list_days"] = get_int(method_name, "list_days", data[m], 30) + new_dictionary["list_size"] = get_int(method_name, "list_size", data[m], 10) + new_dictionary["list_buffer"] = get_int(method_name, "list_buffer", data[m], 20) + self.methods.append((method_name, [new_dictionary])) + elif method_name == "mal_season": + new_dictionary = {"sort_by": "anime_num_list_users"} + if "sort_by" not in data[m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default") + elif not data[m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default") + elif data[m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(data[m]["sort_by"])) + else: new_dictionary["sort_by"] = util.mal_season_sort[data[m]["sort_by"]] + + current_time = datetime.now() + if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter" + elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring" + elif current_time.month in [7, 8, 9]: new_dictionary["season"] = "summer" + elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall" + + if "season" not in data[m]: logger.warning("Collection Warning: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"])) + elif not data[m]["season"]: logger.warning("Collection Warning: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"])) + elif data[m]["season"] not in util.pretty_seasons: logger.warning("Collection Warning: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(data[m]["season"], new_dictionary["season"])) + else: new_dictionary["season"] = data[m]["season"] + + new_dictionary["year"] = get_int(method_name, "year", data[m], current_time.year, min=1917, max=current_time.year + 1) + new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, max=500) + self.methods.append((method_name, [new_dictionary])) + elif method_name == "mal_userlist": + new_dictionary = {"status": "all", "sort_by": "list_score"} + if "username" not in data[m]: raise Failed("Collection Error: mal_userlist username attribute is required") + elif not data[m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank") + else: new_dictionary["username"] = data[m]["username"] + + if "status" not in data[m]: logger.warning("Collection Warning: mal_season status attribute not found using all as default") + elif not data[m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default") + elif data[m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(data[m]["status"])) + else: new_dictionary["status"] = util.mal_userlist_status[data[m]["status"]] + + if "sort_by" not in data[m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default") + elif not data[m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default") + elif data[m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(data[m]["sort_by"])) + else: new_dictionary["sort_by"] = util.mal_userlist_sort[data[m]["sort_by"]] + + new_dictionary["limit"] = get_int(method_name, "limit", data[m], 100, max=1000) + self.methods.append((method_name, [new_dictionary])) + else: + raise Failed("Collection Error: {} attribute is not a dictionary: {}".format(m, data[m])) + elif method_name in util.count_lists: + list_count = util.regex_first_int(data[m], "List Size", default=20) + if list_count < 1: + logger.warning("Collection Warning: {} must be an integer greater then 0 defaulting to 20".format(method_name)) + list_count = 20 + self.methods.append((method_name, [list_count])) + elif method_name in util.tmdb_lists: + values = config.TMDb.validate_tmdb_list(util.get_int_list(data[m], "TMDb {} ID".format(util.tmdb_type[method_name])), util.tmdb_type[method_name]) + if method_name[-8:] == "_details": + if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]: + item = config.TMDb.get_movie_show_or_collection(values[0], self.library.is_movie) + if "summary" not in self.details and hasattr(item, "overview") and item.overview: + self.details["summary"] = item.overview + if "background" not in self.details and hasattr(item, "backdrop_path") and item.backdrop_path: + self.details["background"] = ("url", "{}{}".format(config.TMDb.image_url, item.backdrop_path), method_name[:-8]) + if "poster" not in self.details and hasattr(item, "poster_path") and item.poster_path: + self.details["poster"] = ("url", "{}{}".format(config.TMDb.image_url, item.poster_path), method_name[:-8]) + else: + item = config.TMDb.get_list(values[0]) + if "summary" not in self.details and hasattr(item, "description") and item.description: + self.details["summary"] = item.description + self.methods.append((method_name[:-8], values)) + else: + self.methods.append((method_name, values)) + elif method_name in util.all_lists: + self.methods.append((method_name, util.get_list(data[m]))) + elif method_name not in util.other_attributes: + raise Failed("Collection Error: {} attribute not supported".format(method_name)) + else: + raise Failed("Collection Error: {} attribute is blank".format(m)) + + self.do_arr = False + if self.library.Radarr: + self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Radarr.add + if self.library.Sonarr: + self.do_arr = self.details["add_to_arr"] if "add_to_arr" in self.details else self.library.Sonarr.add + + def run_methods(self, collection_obj, collection_name, map, movie_map, show_map): + items_found = 0 + for method, values in self.methods: + logger.debug("") + logger.debug("Method: {}".format(method)) + logger.debug("Values: {}".format(values)) + pretty = util.pretty_names[method] if method in util.pretty_names else method + for value in values: + items = [] + missing_movies = [] + missing_shows = [] + def check_map(input_ids): + movie_ids, show_ids = input_ids + items_found_inside = 0 + if len(movie_ids) > 0: + items_found_inside += len(movie_ids) + for movie_id in movie_ids: + if movie_id in movie_map: items.append(movie_map[movie_id]) + else: missing_movies.append(movie_id) + if len(show_ids) > 0: + items_found_inside += len(show_ids) + for show_id in show_ids: + if show_id in show_map: items.append(show_map[show_id]) + else: missing_shows.append(show_id) + return items_found_inside + logger.info("") + logger.debug("Value: {}".format(value)) + if method == "plex_all": + logger.info("Processing {} {}".format(pretty, "Movies" if self.library.is_movie else "Shows")) + items = self.library.Plex.all() + items_found += len(items) + elif method == "plex_collection": + items = value.items() + items_found += len(items) + elif method == "plex_search": + search_terms = {} + output = "" + for i, attr_pair in enumerate(value): + search_list = attr_pair[1] + final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0] + if self.library.is_show: + final_method = "show." + final_method + search_terms[final_method] = search_list + ors = "" + for o, param in enumerate(attr_pair[1]): + ors += "{}{}".format(" OR " if o > 0 else "{}(".format(attr_pair[0]), param) + logger.info("\t\t AND {})".format(ors) if i > 0 else "Processing {}: {})".format(pretty, ors)) + items = self.library.Plex.search(**search_terms) + items_found += len(items) + elif method == "plex_collectionless": + good_collections = [] + for col in self.library.get_all_collections(): + keep_collection = True + for pre in value["exclude_prefix"]: + if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)): + keep_collection = False + break + for ext in value["exclude"]: + if col.title == ext or (col.titleSort and col.titleSort == ext): + keep_collection = False + break + if keep_collection: + good_collections.append(col.title.lower()) + + all_items = self.library.Plex.all() + length = 0 + for i, item in enumerate(all_items, 1): + length = util.print_return(length, "Processing: {}/{} {}".format(i, len(all_items), item.title)) + add_item = True + for collection in item.collections: + if collection.tag.lower() in good_collections: + add_item = False + break + if add_item: + items.append(item) + items_found += len(items) + util.print_end(length, "Processed {} {}".format(len(all_items), "Movies" if self.library.is_movie else "Shows")) + elif "tautulli" in method: + 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 "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)) + elif "tmdb" in method: items_found += check_map(self.config.TMDb.get_items(method, value, self.library.is_movie)) + elif "trakt" in method: items_found += check_map(self.config.Trakt.get_items(method, value, self.library.is_movie)) + else: logger.error("Collection Error: {} method not supported".format(method)) + + if len(items) > 0: map = self.library.add_to_collection(collection_obj if collection_obj else collection_name, items, self.filters, self.details["show_filtered"], map, movie_map, show_map) + else: logger.error("No items found to add to this collection ") + + if len(missing_movies) > 0 or len(missing_shows) > 0: + logger.info("") + if len(missing_movies) > 0: + not_lang = None + terms = None + for filter_method, filter_data in self.filters: + if filter_method.startswith("original_language"): + terms = util.get_list(filter_data, lower=True) + not_lang = filter_method.endswith(".not") + break + + missing_movies_with_names = [] + for missing_id in missing_movies: + try: + movie = self.config.TMDb.get_movie(missing_id) + title = str(movie.title) + if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms): + missing_movies_with_names.append((title, missing_id)) + if self.details["show_missing"] is True: + logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) + elif self.details["show_filtered"] is True: + logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id)) + except Failed as e: + logger.error(e) + logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else "")) + if self.details["save_missing"] is True: + self.library.add_missing(collection_name, missing_movies_with_names, True) + if self.do_arr and self.library.Radarr: + self.library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names], tag=self.details["arr_tag"]) + if len(missing_shows) > 0 and self.library.is_show: + missing_shows_with_names = [] + for missing_id in missing_shows: + try: + title = str(self.config.TVDb.get_series(self.library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) + missing_shows_with_names.append((title, missing_id)) + if self.details["show_missing"] is True: + logger.info("{} Collection | ? | {} (TVDB: {})".format(collection_name, title, missing_id)) + except Failed as e: + logger.error(e) + logger.info("{} Show{} Missing".format(len(missing_shows_with_names), "s" if len(missing_shows_with_names) > 1 else "")) + if self.details["save_missing"] is True: + self.library.add_missing(c, missing_shows_with_names, False) + if self.do_arr and self.library.Sonarr: + self.library.Sonarr.add_tvdb([missing_id for title, missing_id in missing_shows_with_names], tag=self.details["arr_tag"]) + + if self.sync and items_found > 0: + logger.info("") + count_removed = 0 + for ratingKey, item in map.items(): + if item is not None: + logger.info("{} Collection | - | {}".format(collection_name, item.title)) + item.removeCollection(collection_name) + count_removed += 1 + logger.info("{} {}{} Removed".format(count_removed, "Movie" if self.library.is_movie else "Show", "s" if count_removed == 1 else "")) + logger.info("") + + def update_details(self, collection): + edits = {} + if "sort_title" in self.details: + edits["titleSort.value"] = self.details["sort_title"] + edits["titleSort.locked"] = 1 + if "content_rating" in self.details: + edits["contentRating.value"] = self.details["content_rating"] + edits["contentRating.locked"] = 1 + if "summary" in self.details: + edits["summary.value"] = self.details["summary"] + edits["summary.locked"] = 1 + if len(edits) > 0: + logger.debug(edits) + collection.edit(**edits) + collection.reload() + logger.info("Details: have been updated") + if "collection_mode" in self.details: + collection.modeUpdate(mode=self.details["collection_mode"]) + if "collection_order" in self.details: + collection.sortUpdate(sort=self.details["collection_order"]) + + if "label" in self.details: + item_labels = [label.tag for label in collection.labels] + labels = util.get_list(self.details["label"]) + if "label_sync_mode" in self.details and self.details["label_sync_mode"] == "sync": + for label in (l for l in item_labels if l not in labels): + collection.removeLabel(label) + logger.info("Detail: Label {} removed".format(label)) + for label in (l for l in labels if l not in item_labels): + collection.addLabel(label) + logger.info("Detail: Label {} added".format(label)) + + if self.library.asset_directory: + name_mapping = self.name + if "name_mapping" in self.details: + if self.details["name_mapping"]: name_mapping = self.details["name_mapping"] + else: logger.error("Collection Error: name_mapping attribute is blank") + for ad in self.library.asset_directory: + path = os.path.join(ad, "{}".format(name_mapping)) + if not os.path.isdir(path): + continue + matches = glob.glob(os.path.join(ad, "{}".format(name_mapping), "poster.*")) + if len(matches) > 0: + for match in matches: + self.posters.append(("file", os.path.abspath(match), "asset_directory")) + matches = glob.glob(os.path.join(ad, "{}".format(name_mapping), "background.*")) + if len(matches) > 0: + for match in matches: + self.backgrounds.append(("file", os.path.abspath(match), "asset_directory")) + dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))] + if len(dirs) > 0: + for item in collection.items(): + folder = os.path.basename(os.path.dirname(item.locations[0])) + if folder in dirs: + matches = glob.glob(os.path.join(path, folder, "poster.*")) + poster_path = os.path.abspath(matches[0]) if len(matches) > 0 else None + matches = glob.glob(os.path.join(path, folder, "background.*")) + background_path = os.path.abspath(matches[0]) if len(matches) > 0 else None + if poster_path: + item.uploadPoster(filepath=poster_path) + logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path)) + if background_path: + item.uploadArt(filepath=background_path) + logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path)) + if poster_path is None and background_path is None: + logger.warning("No Files Found: {}".format(os.path.join(path, folder))) + else: + logger.warning("No Folder: {}".format(os.path.join(path, folder))) + + poster = util.choose_from_list(self.posters, "poster", list_type="tuple") + if not poster and "poster" in self.details: poster = self.details["poster"] + if poster: + if poster[0] == "url": collection.uploadPoster(url=poster[1]) + else: collection.uploadPoster(filepath=poster[1]) + logger.info("Detail: {} updated collection poster to [{}] {}".format(poster[2], poster[0], poster[1])) + + background = util.choose_from_list(self.backgrounds, "background", list_type="tuple") + if not background and "background" in self.details: background = self.details["background"] + if background: + if background[0] == "url": collection.uploadArt(url=background[1]) + else: collection.uploadArt(filepath=background[1]) + logger.info("Detail: {} updated collection background to [{}] {}".format(background[2], background[0], background[1])) diff --git a/modules/config.py b/modules/config.py index dee019ba5..daba8e82c 100644 --- a/modules/config.py +++ b/modules/config.py @@ -1,16 +1,20 @@ -import glob, json, logging, os, re, requests -from datetime import datetime, timedelta +import glob, logging, os, re, requests from modules import util from modules.anidb import AniDBAPI +from modules.builder import CollectionBuilder from modules.cache import Cache from modules.imdb import IMDbAPI -from modules.plex import PlexAPI from modules.mal import MyAnimeListAPI from modules.mal import MyAnimeListIDList +from modules.plex import PlexAPI +from modules.radarr import RadarrAPI +from modules.sonarr import SonarrAPI +from modules.tautulli import TautulliAPI from modules.tmdb import TMDbAPI from modules.trakt import TraktAPI from modules.tvdb import TVDbAPI from modules.util import Failed +from plexapi.exceptions import BadRequest from ruamel import yaml logger = logging.getLogger("Plex Meta Manager") @@ -25,16 +29,66 @@ def __init__(self, default_dir, config_path=None): logger.info("Using {} as config".format(self.config_path)) yaml.YAML().allow_duplicate_keys = True - try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path)) - except yaml.scanner.ScannerError as e: raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - - def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, var_type="str", throw=False, save=True): + try: + new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path)) + def replace_attr(all_data, attr, par): + if "settings" not in all_data: + all_data["settings"] = {} + if par in all_data and all_data[par] and attr in all_data[par] and attr not in all_data["settings"]: + all_data["settings"][attr] = all_data[par][attr] + del all_data[par][attr] + if "libraries" not in new_config: + new_config["libraries"] = {} + if "settings" not in new_config: + new_config["settings"] = {} + if "tmdb" not in new_config: + new_config["tmdb"] = {} + replace_attr(new_config, "cache", "cache") + replace_attr(new_config, "cache_expiration", "cache") + if "config" in new_config: + del new_config["cache"] + replace_attr(new_config, "asset_directory", "plex") + replace_attr(new_config, "sync_mode", "plex") + replace_attr(new_config, "show_unmanaged", "plex") + replace_attr(new_config, "show_filtered", "plex") + replace_attr(new_config, "show_missing", "plex") + replace_attr(new_config, "save_missing", "plex") + if new_config["libraries"]: + for library in new_config["libraries"]: + if "plex" in new_config["libraries"][library]: + replace_attr(new_config["libraries"][library], "asset_directory", "plex") + replace_attr(new_config["libraries"][library], "sync_mode", "plex") + replace_attr(new_config["libraries"][library], "show_unmanaged", "plex") + replace_attr(new_config["libraries"][library], "show_filtered", "plex") + replace_attr(new_config["libraries"][library], "show_missing", "plex") + replace_attr(new_config["libraries"][library], "save_missing", "plex") + new_config["libraries"] = new_config.pop("libraries") + new_config["settings"] = new_config.pop("settings") + new_config["plex"] = new_config.pop("plex") + new_config["tmdb"] = new_config.pop("tmdb") + new_config["tautulli"] = new_config.pop("tautulli") + new_config["radarr"] = new_config.pop("radarr") + new_config["sonarr"] = new_config.pop("sonarr") + new_config["trakt"] = new_config.pop("trakt") + new_config["mal"] = new_config.pop("mal") + yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi) + self.data = new_config + except yaml.scanner.ScannerError as e: + raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) + + def check_for_attribute(data, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, req_default=False, var_type="str", throw=False, save=True): message = "" endline = "" - data = data if parent is None else data[parent] + if parent is not None: + if parent in data: + data = data[parent] + else: + data = None + do_print = False + save = False text = "{} attribute".format(attribute) if parent is None else "{} sub-attribute {}".format(parent, attribute) if data is None or attribute not in data: - message = "Config Error: {} not found".format(text) + message = "{} not found".format(text) if parent and save is True: new_config, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.config_path)) endline = "\n| {} sub-attribute {} added to config".format(parent, attribute) @@ -43,45 +97,60 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" elif attribute not in new_config[parent]: new_config[parent][attribute] = default else: endLine = "" yaml.round_trip_dump(new_config, open(self.config_path, "w"), indent=ind, block_seq_indent=bsi) - elif not data[attribute] and data[attribute] != False: message = "Config Error: {} is blank".format(text) + elif not data[attribute] and data[attribute] != False: + if default_is_none is True: return None + else: message = "{} is blank".format(text) elif var_type == "bool": if isinstance(data[attribute], bool): return data[attribute] - else: message = "Config Error: {} must be either true or false".format(text) + else: message = "{} must be either true or false".format(text) elif var_type == "int": if isinstance(data[attribute], int) and data[attribute] > 0: return data[attribute] - else: message = "Config Error: {} must an integer > 0".format(text) + else: message = "{} must an integer > 0".format(text) elif var_type == "path": if os.path.exists(os.path.abspath(data[attribute])): return data[attribute] - else: message = "Config Error: {} could not be found".format(text) - if default and os.path.exists(os.path.abspath(default)): - return default - elif default: - default = None - default_is_none = True - message = "Config Error: neither {} or the default path {} could be found".format(data[attribute], default) + else: message = "Path {} does not exist".format(os.path.abspath(data[attribute])) + elif var_type == "list": return util.get_list(data[attribute]) + elif var_type == "listpath": + temp_list = [path for path in util.get_list(data[attribute], split=True) if os.path.exists(os.path.abspath(path))] + if len(temp_list) > 0: return temp_list + else: message = "No Paths exist" + elif var_type == "lowerlist": return util.get_list(data[attribute], lower=True) elif test_list is None or data[attribute] in test_list: return data[attribute] - else: message = "Config Error: {}: {} is an invalid input".format(text, data[attribute]) + else: message = "{}: {} is an invalid input".format(text, data[attribute]) + if var_type == "path" and default and os.path.exists(os.path.abspath(default)): + return default + elif var_type == "path" and default: + default = None + message = "neither {} or the default path {} could be found".format(data[attribute], default) if default is not None or default_is_none: message = message + " using {} as default".format(default) message = message + endline + if req_default and default is None: + raise Failed("Config Error: {} attribute must be set under {} globally or under this specific Library".format(attribute, parent)) if (default is None and not default_is_none) or throw: if len(options) > 0: message = message + "\n" + options - raise Failed(message) + raise Failed("Config Error: {}".format(message)) if do_print: - util.print_multiline(message) + util.print_multiline("Config Warning: {}".format(message)) if attribute in data and data[attribute] and test_list is not None and data[attribute] not in test_list: util.print_multiline(options) return default self.general = {} - self.general["cache"] = check_for_attribute(self.data, "cache", parent="cache", options="| \ttrue (Create a cache to store ids)\n| \tfalse (Do not create a cache to store ids)", var_type="bool", default=True) if "cache" in self.data else True - self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="cache", var_type="int", default=60) if "cache" in self.data else 60 + self.general["cache"] = check_for_attribute(self.data, "cache", parent="settings", options=" true (Create a cache to store ids)\n false (Do not create a cache to store ids)", var_type="bool", default=True) + self.general["cache_expiration"] = check_for_attribute(self.data, "cache_expiration", parent="settings", var_type="int", default=60) if self.general["cache"]: util.seperator() self.Cache = Cache(self.config_path, self.general["cache_expiration"]) else: self.Cache = None + self.general["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="settings", var_type="listpath", default=[os.path.join(default_dir, "assets")]) + self.general["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="settings", default="append", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)") + self.general["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="settings", var_type="bool", default=True) + self.general["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="settings", var_type="bool", default=False) + self.general["show_missing"] = check_for_attribute(self.data, "show_missing", parent="settings", var_type="bool", default=True) + self.general["save_missing"] = check_for_attribute(self.data, "save_missing", parent="settings", var_type="bool", default=True) util.seperator() @@ -89,7 +158,8 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" if "tmdb" in self.data: logger.info("Connecting to TMDb...") self.tmdb = {} - self.tmdb["apikey"] = check_for_attribute(self.data, "apikey", parent="tmdb", throw=True) + try: self.tmdb["apikey"] = check_for_attribute(self.data, "apikey", parent="tmdb", throw=True) + except Failed as e: raise Failed(e) self.tmdb["language"] = check_for_attribute(self.data, "language", parent="tmdb", default="en") self.TMDb = TMDbAPI(self.tmdb) logger.info("TMDb Connection {}".format("Failed" if self.TMDb is None else "Successful")) @@ -137,42 +207,41 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" self.IMDb = IMDbAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt, TVDb=self.TVDb) if self.TMDb or self.Trakt else None self.AniDB = AniDBAPI(Cache=self.Cache, TMDb=self.TMDb, Trakt=self.Trakt) + util.seperator() + + logger.info("Connecting to Plex Libraries...") + self.general["plex"] = {} - self.general["plex"]["url"] = check_for_attribute(self.data, "url", parent="plex", default_is_none=True) if "plex" in self.data else None - self.general["plex"]["token"] = check_for_attribute(self.data, "token", parent="plex", default_is_none=True) if "plex" in self.data else None - self.general["plex"]["asset_directory"] = check_for_attribute(self.data, "asset_directory", parent="plex", var_type="path", default=os.path.join(default_dir, "assets")) if "plex" in self.data else os.path.join(default_dir, "assets") - self.general["plex"]["sync_mode"] = check_for_attribute(self.data, "sync_mode", parent="plex", default="append", test_list=["append", "sync"], options="| \tappend (Only Add Items to the Collection)\n| \tsync (Add & Remove Items from the Collection)") if "plex" in self.data else "append" - self.general["plex"]["show_unmanaged"] = check_for_attribute(self.data, "show_unmanaged", parent="plex", var_type="bool", default=True) if "plex" in self.data else True - self.general["plex"]["show_filtered"] = check_for_attribute(self.data, "show_filtered", parent="plex", var_type="bool", default=False) if "plex" in self.data else False + self.general["plex"]["url"] = check_for_attribute(self.data, "url", parent="plex", default_is_none=True) + self.general["plex"]["token"] = check_for_attribute(self.data, "token", parent="plex", default_is_none=True) self.general["radarr"] = {} - self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) if "radarr" in self.data else None - self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=["v2", "v3"], options="| \tv2 (For Radarr 0.2)\n| \tv3 (For Radarr 3.0)", default="v2") if "radarr" in self.data else "v2" - self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) if "radarr" in self.data else None - self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) if "radarr" in self.data else None - self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) if "radarr" in self.data else None - self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) if "radarr" in self.data else False - self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) if "radarr" in self.data else False + self.general["radarr"]["url"] = check_for_attribute(self.data, "url", parent="radarr", default_is_none=True) + self.general["radarr"]["version"] = check_for_attribute(self.data, "version", parent="radarr", test_list=["v2", "v3"], options=" v2 (For Radarr 0.2)\n v3 (For Radarr 3.0)", default="v2") + self.general["radarr"]["token"] = check_for_attribute(self.data, "token", parent="radarr", default_is_none=True) + self.general["radarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="radarr", default_is_none=True) + self.general["radarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="radarr", default_is_none=True) + self.general["radarr"]["add"] = check_for_attribute(self.data, "add", parent="radarr", var_type="bool", default=False) + self.general["radarr"]["search"] = check_for_attribute(self.data, "search", parent="radarr", var_type="bool", default=False) + self.general["radarr"]["tag"] = check_for_attribute(self.data, "tag", parent="radarr", var_type="lowerlist", default_is_none=True) self.general["sonarr"] = {} - self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None - self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None - self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options="| \tv2 (For Sonarr 0.2)\n| \tv3 (For Sonarr 3.0)", default="v2") if "sonarr" in self.data else "v2" - self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None - self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) if "sonarr" in self.data else None - self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) if "sonarr" in self.data else False - self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) if "sonarr" in self.data else False + self.general["sonarr"]["url"] = check_for_attribute(self.data, "url", parent="sonarr", default_is_none=True) + self.general["sonarr"]["token"] = check_for_attribute(self.data, "token", parent="sonarr", default_is_none=True) + self.general["sonarr"]["version"] = check_for_attribute(self.data, "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default="v2") + self.general["sonarr"]["quality_profile"] = check_for_attribute(self.data, "quality_profile", parent="sonarr", default_is_none=True) + self.general["sonarr"]["root_folder_path"] = check_for_attribute(self.data, "root_folder_path", parent="sonarr", default_is_none=True) + self.general["sonarr"]["add"] = check_for_attribute(self.data, "add", parent="sonarr", var_type="bool", default=False) + self.general["sonarr"]["search"] = check_for_attribute(self.data, "search", parent="sonarr", var_type="bool", default=False) + self.general["sonarr"]["tag"] = check_for_attribute(self.data, "tag", parent="sonarr", var_type="lowerlist", default_is_none=True) self.general["tautulli"] = {} - self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True) if "tautulli" in self.data else None - self.general["tautulli"]["apikey"] = check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) if "tautulli" in self.data else None - - util.seperator() - - logger.info("Connecting to Plex Libraries...") + self.general["tautulli"]["url"] = check_for_attribute(self.data, "url", parent="tautulli", default_is_none=True) + self.general["tautulli"]["apikey"] = check_for_attribute(self.data, "apikey", parent="tautulli", default_is_none=True) self.libraries = [] - libs = check_for_attribute(self.data, "libraries", throw=True) + try: libs = check_for_attribute(self.data, "libraries", throw=True) + except Failed as e: raise Failed(e) for lib in libs: util.seperator() params = {} @@ -183,225 +252,77 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" params["name"] = str(lib) logger.info("Connecting to {} Library...".format(params["name"])) default_lib = os.path.join(default_dir, "{}.yml".format(lib)) - try: - if "metadata_path" in libs[lib]: - if libs[lib]["metadata_path"]: - if os.path.exists(libs[lib]["metadata_path"]): params["metadata_path"] = libs[lib]["metadata_path"] - else: raise Failed("metadata_path not found at {}".format(libs[lib]["metadata_path"])) - else: raise Failed("metadata_path attribute is blank") - else: - if os.path.exists(default_lib): params["metadata_path"] = os.path.abspath(default_lib) - else: raise Failed("default metadata_path not found at {}".format(os.path.abspath(os.path.join(default_dir, "{}.yml".format(params["name"]))))) - - if "library_type" in libs[lib]: - if libs[lib]["library_type"]: - if libs[lib]["library_type"] in ["movie", "show"]: params["library_type"] = libs[lib]["library_type"] - else: raise Failed("library_type attribute must be either 'movie' or 'show'") - else: raise Failed("library_type attribute is blank") - else: raise Failed("library_type attribute is required") - - params["plex"] = {} - if "plex" in libs[lib] and libs[lib]["plex"] and "url" in libs[lib]["plex"]: - if libs[lib]["plex"]["url"]: params["plex"]["url"] = libs[lib]["plex"]["url"] - else: raise Failed("url library attribute is blank") - elif self.general["plex"]["url"]: params["plex"]["url"] = self.general["plex"]["url"] - else: raise Failed("url attribute must be set under plex or under this specific Library") - if "plex" in libs[lib] and libs[lib]["plex"] and "token" in libs[lib]["plex"]: - if libs[lib]["plex"]["token"]: params["plex"]["token"] = libs[lib]["plex"]["token"] - else: raise Failed("token library attribute is blank") - elif self.general["plex"]["token"]: params["plex"]["token"] = self.general["plex"]["token"] - else: raise Failed("token attribute must be set under plex or under this specific Library") - except Failed as e: - logger.error("Config Error: Skipping {} Library {}".format(str(lib), e)) - continue - - params["asset_directory"] = None - - if "plex" in libs[lib] and "asset_directory" in libs[lib]["plex"]: - if libs[lib]["plex"]["asset_directory"]: - if os.path.exists(libs[lib]["plex"]["asset_directory"]): - params["asset_directory"] = libs[lib]["plex"]["asset_directory"] - else: - logger.warning("Config Warning: Assets will not be used asset_directory not found at {}".format(libs[lib]["plex"]["asset_directory"])) - else: - logger.warning("Config Warning: Assets will not be used asset_directory library attribute is blank") - elif self.general["plex"]["asset_directory"]: - params["asset_directory"] = self.general["plex"]["asset_directory"] - else: + params["asset_directory"] = check_for_attribute(libs[lib], "asset_directory", parent="settings", var_type="listpath", default=self.general["asset_directory"], default_is_none=True, save=False) + if params["asset_directory"] is None: logger.warning("Config Warning: Assets will not be used asset_directory attribute must be set under config or under this specific Library") - params["sync_mode"] = self.general["plex"]["sync_mode"] - if "plex" in libs[lib] and "sync_mode" in libs[lib]["plex"]: - if libs[lib]["plex"]["sync_mode"]: - if libs[lib]["plex"]["sync_mode"] in ["append", "sync"]: - params["sync_mode"] = libs[lib]["plex"]["sync_mode"] - else: - logger.warning("Config Warning: sync_mode attribute must be either 'append' or 'sync' using general value: {}".format(self.general["plex"]["sync_mode"])) - else: - logger.warning("Config Warning: sync_mode attribute is blank using general value: {}".format(self.general["plex"]["sync_mode"])) - - params["show_unmanaged"] = self.general["plex"]["show_unmanaged"] - if "plex" in libs[lib] and "show_unmanaged" in libs[lib]["plex"]: - if libs[lib]["plex"]["show_unmanaged"]: - if isinstance(libs[lib]["plex"]["show_unmanaged"], bool): - params["plex"]["show_unmanaged"] = libs[lib]["plex"]["show_unmanaged"] - else: - logger.warning("Config Warning: plex sub-attribute show_unmanaged must be either true or false using general value: {}".format(self.general["plex"]["show_unmanaged"])) - else: - logger.warning("Config Warning: plex sub-attribute show_unmanaged is blank using general value: {}".format(self.general["plex"]["show_unmanaged"])) - - params["show_filtered"] = self.general["plex"]["show_filtered"] - if "plex" in libs[lib] and "show_filtered" in libs[lib]["plex"]: - if libs[lib]["plex"]["show_filtered"]: - if isinstance(libs[lib]["plex"]["show_filtered"], bool): - params["plex"]["show_filtered"] = libs[lib]["plex"]["show_filtered"] - else: - logger.warning("Config Warning: plex sub-attribute show_filtered must be either true or false using general value: {}".format(self.general["plex"]["show_filtered"])) - else: - logger.warning("Config Warning: plex sub-attribute show_filtered is blank using general value: {}".format(self.general["plex"]["show_filtered"])) - - params["tmdb"] = self.TMDb - params["tvdb"] = self.TVDb - - params["radarr"] = self.general["radarr"].copy() - if "radarr" in libs[lib] and libs[lib]["radarr"]: - if "url" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["url"]: - params["radarr"]["url"] = libs[lib]["radarr"]["url"] - else: - logger.warning("Config Warning: radarr sub-attribute url is blank using general value: {}".format(self.general["radarr"]["url"])) - - if "token" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["token"]: - params["radarr"]["token"] = libs[lib]["radarr"]["token"] - else: - logger.warning("Config Warning: radarr sub-attribute token is blank using general value: {}".format(self.general["radarr"]["token"])) - - if "version" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["version"]: - if libs[lib]["radarr"]["version"] in ["v2", "v3"]: - params["radarr"]["version"] = libs[lib]["radarr"]["version"] - else: - logger.warning("Config Warning: radarr sub-attribute version must be either 'v2' or 'v3' using general value: {}".format(self.general["radarr"]["version"])) - else: - logger.warning("Config Warning: radarr sub-attribute version is blank using general value: {}".format(self.general["radarr"]["version"])) - - if "quality_profile" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["quality_profile"]: - params["radarr"]["quality_profile"] = libs[lib]["radarr"]["quality_profile"] - else: - logger.warning("Config Warning: radarr sub-attribute quality_profile is blank using general value: {}".format(self.general["radarr"]["quality_profile"])) - - if "root_folder_path" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["root_folder_path"]: - params["radarr"]["root_folder_path"] = libs[lib]["radarr"]["root_folder_path"] - else: - logger.warning("Config Warning: radarr sub-attribute root_folder_path is blank using general value: {}".format(self.general["radarr"]["root_folder_path"])) - - if "add" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["add"]: - if isinstance(libs[lib]["radarr"]["add"], bool): - params["radarr"]["add"] = libs[lib]["radarr"]["add"] - else: - logger.warning("Config Warning: radarr sub-attribute add must be either true or false using general value: {}".format(self.general["radarr"]["add"])) - else: - logger.warning("Config Warning: radarr sub-attribute add is blank using general value: {}".format(self.general["radarr"]["add"])) - - if "search" in libs[lib]["radarr"]: - if libs[lib]["radarr"]["search"]: - if isinstance(libs[lib]["radarr"]["search"], bool): - params["radarr"]["search"] = libs[lib]["radarr"]["search"] - else: - logger.warning("Config Warning: radarr sub-attribute search must be either true or false using general value: {}".format(self.general["radarr"]["search"])) - else: - logger.warning("Config Warning: radarr sub-attribute search is blank using general value: {}".format(self.general["radarr"]["search"])) - - if not params["radarr"]["url"] or not params["radarr"]["token"] or not params["radarr"]["quality_profile"] or not params["radarr"]["root_folder_path"]: - params["radarr"] = None - - params["sonarr"] = self.general["sonarr"].copy() - if "sonarr" in libs[lib] and libs[lib]["sonarr"]: - if "url" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["url"]: - params["sonarr"]["url"] = libs[lib]["sonarr"]["url"] - else: - logger.warning("Config Warning: sonarr sub-attribute url is blank using general value: {}".format(self.general["sonarr"]["url"])) - - if "token" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["token"]: - params["sonarr"]["token"] = libs[lib]["sonarr"]["token"] - else: - logger.warning("Config Warning: sonarr sub-attribute token is blank using general value: {}".format(self.general["sonarr"]["token"])) - - if "version" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["version"]: - if libs[lib]["sonarr"]["version"] in ["v2", "v3"]: - params["sonarr"]["version"] = libs[lib]["sonarr"]["version"] - else: - logger.warning("Config Warning: sonarr sub-attribute version must be either 'v2' or 'v3' using general value: {}".format(self.general["sonarr"]["version"])) - else: - logger.warning("Config Warning: sonarr sub-attribute version is blank using general value: {}".format(self.general["sonarr"]["version"])) - - if "quality_profile" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["quality_profile"]: - params["sonarr"]["quality_profile"] = libs[lib]["sonarr"]["quality_profile"] - else: - logger.warning("Config Warning: sonarr sub-attribute quality_profile is blank using general value: {}".format(self.general["sonarr"]["quality_profile"])) - - if "root_folder_path" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["root_folder_path"]: - params["sonarr"]["root_folder_path"] = libs[lib]["sonarr"]["root_folder_path"] - else: - logger.warning("Config Warning: sonarr sub-attribute root_folder_path is blank using general value: {}".format(self.general["sonarr"]["root_folder_path"])) - - if "add" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["add"]: - if isinstance(libs[lib]["sonarr"]["add"], bool): - params["sonarr"]["add"] = libs[lib]["sonarr"]["add"] - else: - logger.warning("Config Warning: sonarr sub-attribute add must be either true or false using general value: {}".format(self.general["sonarr"]["add"])) - else: - logger.warning("Config Warning: sonarr sub-attribute add is blank using general value: {}".format(self.general["sonarr"]["add"])) - - if "search" in libs[lib]["sonarr"]: - if libs[lib]["sonarr"]["search"]: - if isinstance(libs[lib]["sonarr"]["search"], bool): - params["sonarr"]["search"] = libs[lib]["sonarr"]["search"] - else: - logger.warning("Config Warning: sonarr sub-attribute search must be either true or false using general value: {}".format(self.general["sonarr"]["search"])) - else: - logger.warning("Config Warning: sonarr sub-attribute search is blank using general value: {}".format(self.general["sonarr"]["search"])) - - if not params["sonarr"]["url"] or not params["sonarr"]["token"] or not params["sonarr"]["quality_profile"] or not params["sonarr"]["root_folder_path"] or params["library_type"] == "movie": - params["sonarr"] = None - - - params["tautulli"] = self.general["tautulli"].copy() - if "tautulli" in libs[lib] and libs[lib]["tautulli"]: - if "url" in libs[lib]["tautulli"]: - if libs[lib]["tautulli"]["url"]: - params["tautulli"]["url"] = libs[lib]["tautulli"]["url"] - else: - logger.warning("Config Warning: tautulli sub-attribute url is blank using general value: {}".format(self.general["tautulli"]["url"])) - - if "apikey" in libs[lib]["tautulli"]: - if libs[lib]["tautulli"]["apikey"]: - params["tautulli"]["apikey"] = libs[lib]["tautulli"]["apikey"] - else: - logger.warning("Config Warning: tautulli sub-attribute apikey is blank using general value: {}".format(self.general["tautulli"]["apikey"])) - - if not params["tautulli"]["url"] or not params["tautulli"]["apikey"] : - params["tautulli"] = None + params["sync_mode"] = check_for_attribute(libs[lib], "sync_mode", parent="settings", test_list=["append", "sync"], options=" append (Only Add Items to the Collection)\n sync (Add & Remove Items from the Collection)", default=self.general["sync_mode"], save=False) + params["show_unmanaged"] = check_for_attribute(libs[lib], "show_unmanaged", parent="settings", var_type="bool", default=self.general["show_unmanaged"], save=False) + params["show_filtered"] = check_for_attribute(libs[lib], "show_filtered", parent="settings", var_type="bool", default=self.general["show_filtered"], save=False) + params["show_missing"] = check_for_attribute(libs[lib], "show_missing", parent="settings", var_type="bool", default=self.general["show_missing"], save=False) + params["save_missing"] = check_for_attribute(libs[lib], "save_missing", parent="settings", var_type="bool", default=self.general["save_missing"], save=False) try: - self.libraries.append(PlexAPI(params)) + params["metadata_path"] = check_for_attribute(libs[lib], "metadata_path", var_type="path", default=os.path.join(default_dir, "{}.yml".format(lib)), throw=True) + params["library_type"] = check_for_attribute(libs[lib], "library_type", test_list=["movie", "show"], options=" movie (For Movie Libraries)\n show (For Show Libraries)", throw=True) + params["plex"] = {} + params["plex"]["url"] = check_for_attribute(libs[lib], "url", parent="plex", default=self.general["plex"]["url"], req_default=True, save=False) + params["plex"]["token"] = check_for_attribute(libs[lib], "token", parent="plex", default=self.general["plex"]["token"], req_default=True, save=False) + library = PlexAPI(params, self.TMDb, self.TVDb) logger.info("{} Library Connection Successful".format(params["name"])) except Failed as e: - logger.error(e) + util.print_multiline(e) logger.info("{} Library Connection Failed".format(params["name"])) continue + if self.general["radarr"]["url"] or "radarr" in libs[lib]: + logger.info("Connecting to {} library's Radarr...".format(params["name"])) + radarr_params = {} + try: + radarr_params["url"] = check_for_attribute(libs[lib], "url", parent="radarr", default=self.general["radarr"]["url"], req_default=True, save=False) + radarr_params["token"] = check_for_attribute(libs[lib], "token", parent="radarr", default=self.general["radarr"]["token"], req_default=True, save=False) + radarr_params["version"] = check_for_attribute(libs[lib], "version", parent="radarr", test_list=["v2", "v3"], options=" v2 (For Radarr 0.2)\n v3 (For Radarr 3.0)", default=self.general["radarr"]["version"], save=False) + radarr_params["quality_profile"] = check_for_attribute(libs[lib], "quality_profile", parent="radarr", default=self.general["radarr"]["quality_profile"], req_default=True, save=False) + radarr_params["root_folder_path"] = check_for_attribute(libs[lib], "root_folder_path", parent="radarr", default=self.general["radarr"]["root_folder_path"], req_default=True, save=False) + radarr_params["add"] = check_for_attribute(libs[lib], "add", parent="radarr", var_type="bool", default=self.general["radarr"]["add"], save=False) + radarr_params["search"] = check_for_attribute(libs[lib], "search", parent="radarr", var_type="bool", default=self.general["radarr"]["search"], save=False) + radarr_params["tag"] = check_for_attribute(libs[lib], "search", parent="radarr", var_type="lowerlist", default=self.general["radarr"]["tag"], default_is_none=True, save=False) + library.add_Radarr(RadarrAPI(self.TMDb, radarr_params)) + except Failed as e: + util.print_multiline(e) + logger.info("{} library's Radarr Connection {}".format(params["name"], "Failed" if library.Radarr is None else "Successful")) + + if self.general["sonarr"]["url"] or "sonarr" in libs[lib]: + logger.info("Connecting to {} library's Sonarr...".format(params["name"])) + sonarr_params = {} + try: + sonarr_params["url"] = check_for_attribute(libs[lib], "url", parent="sonarr", default=self.general["sonarr"]["url"], req_default=True, save=False) + sonarr_params["token"] = check_for_attribute(libs[lib], "token", parent="sonarr", default=self.general["sonarr"]["token"], req_default=True, save=False) + sonarr_params["version"] = check_for_attribute(libs[lib], "version", parent="sonarr", test_list=["v2", "v3"], options=" v2 (For Sonarr 0.2)\n v3 (For Sonarr 3.0)", default=self.general["sonarr"]["version"], save=False) + sonarr_params["quality_profile"] = check_for_attribute(libs[lib], "quality_profile", parent="sonarr", default=self.general["sonarr"]["quality_profile"], req_default=True, save=False) + sonarr_params["root_folder_path"] = check_for_attribute(libs[lib], "root_folder_path", parent="sonarr", default=self.general["sonarr"]["root_folder_path"], req_default=True, save=False) + sonarr_params["add"] = check_for_attribute(libs[lib], "add", parent="sonarr", var_type="bool", default=self.general["sonarr"]["add"], save=False) + sonarr_params["search"] = check_for_attribute(libs[lib], "search", parent="sonarr", var_type="bool", default=self.general["sonarr"]["search"], save=False) + sonarr_params["tag"] = check_for_attribute(libs[lib], "search", parent="sonarr", var_type="lowerlist", default=self.general["sonarr"]["tag"], default_is_none=True, save=False) + library.add_Sonarr(SonarrAPI(self.TVDb, sonarr_params, library.Plex.language)) + except Failed as e: + util.print_multiline(e) + logger.info("{} library's Sonarr Connection {}".format(params["name"], "Failed" if library.Sonarr is None else "Successful")) + + if self.general["tautulli"]["url"] or "tautulli" in libs[lib]: + logger.info("Connecting to {} library's Tautulli...".format(params["name"])) + tautulli_params = {} + try: + tautulli_params["url"] = check_for_attribute(libs[lib], "url", parent="tautulli", default=self.general["tautulli"]["url"], req_default=True, save=False) + tautulli_params["apikey"] = check_for_attribute(libs[lib], "apikey", parent="tautulli", default=self.general["tautulli"]["apikey"], req_default=True, save=False) + library.add_Tautulli(TautulliAPI(tautulli_params)) + except Failed as e: + util.print_multiline(e) + logger.info("{} library's Tautulli Connection {}".format(params["name"], "Failed" if library.Tautulli is None else "Successful")) + + self.libraries.append(library) + util.seperator() if len(self.libraries) > 0: @@ -411,92 +332,46 @@ def check_for_attribute(data, attribute, parent=None, test_list=None, options="" util.seperator() - def update_libraries(self): + def update_libraries(self, test, requested_collections): for library in self.libraries: logger.info("") util.seperator("{} Library".format(library.name)) - try: library.update_metadata(self.TMDb) + try: library.update_metadata(self.TMDb, test) except Failed as e: logger.error(e) logger.info("") - util.seperator("{} Library Collections".format(library.name)) - collections = library.collections + util.seperator("{} Library {}Collections".format(library.name, "Test " if test else "")) + collections = {c: library.collections[c] for c in util.get_list(requested_collections) if c in library.collections} if requested_collections else library.collections if collections: logger.info("") util.seperator("Mapping {} Library".format(library.name)) logger.info("") movie_map, show_map = self.map_guids(library) for c in collections: + if test and ("test" not in collections[c] or collections[c]["test"] is not True): + no_template_test = True + if "template" in collections[c] and collections[c]["template"]: + for data_template in util.get_list(collections[c]["template"], split=False): + if "name" in data_template \ + and data_template["name"] \ + and library.templates \ + and data_template["name"] in self.library.templates \ + and self.library.templates[data_template["name"]] \ + and "test" in self.library.templates[data_template["name"]] \ + and self.library.templates[data_template["name"]]["test"] == True: + no_template_test = False + if no_template_test: + continue try: logger.info("") util.seperator("{} Collection".format(c)) logger.info("") map = {} - details = {} - methods = [] - filters = [] - posters_found = [] - backgrounds_found = [] - collectionless = "plex_collectionless" in collections[c] - skip_collection = True - show_filtered = library.show_filtered - - if "schedule" not in collections[c]: - skip_collection = False - elif not collections[c]["schedule"]: - logger.error("Collection Error: schedule attribute is blank. Running daily") - skip_collection = False - else: - schedule_list = util.get_list(collections[c]["schedule"]) - current_time = datetime.now() - next_month = current_time.replace(day=28) + timedelta(days=4) - last_day = next_month - timedelta(days=next_month.day) - for schedule in schedule_list: - run_time = str(schedule).lower() - if run_time.startswith("day") or run_time.startswith("daily"): - skip_collection = False - break - if run_time.startswith("week") or run_time.startswith("month") or run_time.startswith("year"): - match = re.search("\\(([^)]+)\\)", run_time) - if match: - param = match.group(1) - if run_time.startswith("week"): - if param.lower() in util.days_alias: - weekday = util.days_alias[param.lower()] - logger.info("Scheduled weekly on {}".format(util.pretty_days[weekday])) - if weekday == current_time.weekday(): - skip_collection = False - break - else: - logger.error("Collection Error: weekly schedule attribute {} invalid must be a day of the weeek i.e. weekly(Monday)".format(schedule)) - elif run_time.startswith("month"): - try: - if 1 <= int(param) <= 31: - logger.info("Scheduled monthly on the {}".format(util.make_ordinal(param))) - if current_time.day == int(param) or (current_time.day == last_day.day and int(param) > last_day.day): - skip_collection = False - break - else: - logger.error("Collection Error: monthly schedule attribute {} invalid must be between 1 and 31".format(schedule)) - except ValueError: - logger.error("Collection Error: monthly schedule attribute {} invalid must be an integer".format(schedule)) - elif run_time.startswith("year"): - match = re.match("^(1[0-2]|0?[1-9])/(3[01]|[12][0-9]|0?[1-9])$", param) - if match: - month = int(match.group(1)) - day = int(match.group(2)) - logger.info("Scheduled yearly on {} {}".format(util.pretty_months[month], util.make_ordinal(day))) - if current_time.month == month and (current_time.day == day or (current_time.day == last_day.day and day > last_day.day)): - skip_collection = False - break - else: - logger.error("Collection Error: yearly schedule attribute {} invalid must be in the MM/DD format i.e. yearly(11/22)".format(schedule)) - else: - logger.error("Collection Error: failed to parse schedule: {}".format(schedule)) - else: - logger.error("Collection Error: schedule attribute {} invalid".format(schedule)) - if skip_collection: - logger.info("Skipping Collection {}".format(c)) + try: + builder = CollectionBuilder(self, library, c, collections[c]) + except Exception as e: + util.print_stacktrace() + logger.error(e) continue try: @@ -506,12 +381,11 @@ def update_libraries(self): collection_obj = None collection_name = c - sync_collection = library.sync_mode == "sync" - if "sync_mode" in collections[c]: - if not collections[c]["sync_mode"]: logger.warning("Collection Warning: sync_mode attribute is blank using general: {}".format(library.sync_mode)) - elif collections[c]["sync_mode"] not in ["append", "sync"]: logger.warning("Collection Warning: {} sync_mode invalid using general: {}".format(library.sync_mode, collections[c]["sync_mode"])) - else: sync_collection = collections[c]["sync_mode"] == "sync" - if sync_collection or collectionless: + if builder.schedule is not None: + print_multiline(builder.schedule, info=True) + + logger.info("") + if builder.sync: logger.info("Sync Mode: sync") if collection_obj: for item in collection_obj.items(): @@ -519,500 +393,12 @@ def update_libraries(self): else: logger.info("Sync Mode: append") - if "tmdb_person" in collections[c]: - if collections[c]["tmdb_person"]: - valid_names = [] - for tmdb_id in util.get_int_list(collections[c]["tmdb_person"], "TMDb Person ID"): - try: - person = self.TMDb.get_person(tmdb_id) - valid_names.append(person.name) - if "summary" not in details and hasattr(person, "biography") and person.biography: - details["summary"] = person.biography - if "poster" not in details and hasattr(person, "profile_path") and person.profile_path: - details["poster"] = ("url", "{}{}".format(self.TMDb.image_url, person.profile_path), "tmdb_person") - except Failed as e: - util.print_stacktrace() - logger.error(e) - if len(valid_names) > 0: details["tmdb_person"] = valid_names - else: logger.error("Collection Error: No valid TMDb Person IDs in {}".format(collections[c]["tmdb_person"])) - else: - logger.error("Collection Error: tmdb_person attribute is blank") - - for m in collections[c]: - try: - if "tmdb" in m and not self.TMDb: - logger.info("Collection Error: {} skipped. TMDb must be configured".format(m)) - map = {} - elif "trakt" in m and not self.Trakt: - logger.info("Collection Error: {} skipped. Trakt must be configured".format(m)) - map = {} - elif "imdb" in m and not self.IMDb: - logger.info("Collection Error: {} skipped. TMDb or Trakt must be configured".format(m)) - map = {} - elif "tautulli" in m and not library.Tautulli: - logger.info("Collection Error: {} skipped. Tautulli must be configured".format(m)) - map = {} - elif "mal" in m and not self.MyAnimeList: - logger.info("Collection Error: {} skipped. MyAnimeList must be configured".format(m)) - map = {} - elif collections[c][m] is not None: - logger.debug("Method: {}".format(m)) - logger.debug("Value: {}".format(collections[c][m])) - if m in util.method_alias: - method_name = util.method_alias[m] - logger.warning("Collection Warning: {} attribute will run as {}".format(m, method_name)) - else: - method_name = m - if method_name in util.show_only_lists and library.is_movie: raise Failed("Collection Error: {} attribute only works for show libraries".format(method_name)) - elif method_name in util.movie_only_lists and library.is_show: raise Failed("Collection Error: {} attribute only works for movie libraries".format(method_name)) - elif method_name in util.movie_only_searches and library.is_show: raise Failed("Collection Error: {} plex search only works for movie libraries".format(method_name)) - elif method_name not in util.collectionless_lists and collectionless: raise Failed("Collection Error: {} attribute does not work for Collectionless collection".format(method_name)) - elif method_name == "tmdb_summary": details["summary"] = self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).overview - elif method_name == "tmdb_description": details["summary"] = self.TMDb.get_list(util.regex_first_int(collections[c][m], "TMDb List ID")).description - elif method_name == "tmdb_biography": details["summary"] = self.TMDb.get_person(util.regex_first_int(collections[c][m], "TMDb Person ID")).biography - elif method_name == "collection_mode": - if collections[c][m] in ["default", "hide", "hide_items", "show_items", "hideItems", "showItems"]: - if collections[c][m] == "hide_items": details[method_name] = "hideItems" - elif collections[c][m] == "show_items": details[method_name] = "showItems" - else: details[method_name] = collections[c][m] - else: raise Failed("Collection Error: {} collection_mode Invalid\n| \tdefault (Library default)\n| \thide (Hide Collection)\n| \thide_items (Hide Items in this Collection)\n| \tshow_items (Show this Collection and its Items)".format(collections[c][m])) - elif method_name == "collection_order": - if collections[c][m] in ["release", "alpha"]: details[method_name] = collections[c][m] - else: raise Failed("Collection Error: {} collection_order Invalid\n| \trelease (Order Collection by release dates)\n| \talpha (Order Collection Alphabetically)".format(collections[c][m])) - elif method_name == "url_poster": posters_found.append(("url", collections[c][m], method_name)) - elif method_name == "tmdb_poster": posters_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).poster_path), method_name)) - elif method_name == "tmdb_profile": posters_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_person(util.regex_first_int(collections[c][m], "TMDb Person ID")).profile_path), method_name)) - elif method_name == "file_poster": - if os.path.exists(collections[c][m]): posters_found.append(("file", os.path.abspath(collections[c][m]), method_name)) - else: raise Failed("Collection Error: Poster Path Does Not Exist: {}".format(os.path.abspath(collections[c][m]))) - elif method_name == "url_background": backgrounds_found.append(("url", collections[c][m], method_name)) - elif method_name == "tmdb_background": backgrounds_found.append(("url", "{}{}".format(self.TMDb.image_url, self.TMDb.get_movie_show_or_collection(util.regex_first_int(collections[c][m], "TMDb ID"), library.is_movie).poster_path), method_name)) - elif method_name == "file_background": - if os.path.exists(collections[c][m]): backgrounds_found.append(("file", os.path.abspath(collections[c][m]), method_name)) - else: raise Failed("Collection Error: Background Path Does Not Exist: {}".format(os.path.abspath(collections[c][m]))) - elif method_name == "add_to_arr": - if isinstance(collections[c][m], bool): details[method_name] = collections[c][m] - else: raise Failed("Collection Error: add_to_arr must be either true or false") - elif method_name == "show_filtered": - if isinstance(collections[c][m], bool): show_filtered = collections[c][m] - else: raise Failed("Collection Error: show_filtered must be either true or false using the default false") - elif method_name in util.all_details: details[method_name] = collections[c][m] - elif method_name in ["year", "year.not"]: methods.append(("plex_search", [[(method_name, util.get_year_list(collections[c][m], method_name))]])) - elif method_name in ["decade", "decade.not"]: methods.append(("plex_search", [[(method_name, util.get_int_list(collections[c][m], util.remove_not(method_name)))]])) - elif method_name in util.tmdb_searches: - final_values = [] - for value in util.get_list(collections[c][m]): - if value.lower() == "tmdb" and "tmdb_person" in details: - for name in details["tmdb_person"]: - final_values.append(name) - else: - final_values.append(value) - methods.append(("plex_search", [[(method_name, final_values)]])) - elif method_name in util.plex_searches: methods.append(("plex_search", [[(method_name, util.get_list(collections[c][m]))]])) - elif method_name == "plex_all": methods.append((method_name, [""])) - elif method_name == "plex_collection": methods.append((method_name, library.validate_collections(collections[c][m] if isinstance(collections[c][m], list) else [collections[c][m]]))) - elif method_name == "anidb_popular": - list_count = util.regex_first_int(collections[c][m], "List Size", default=40) - if 1 <= list_count <= 30: - methods.append((method_name, [list_count])) - else: - logger.error("Collection Error: anidb_popular must be an integer between 1 and 30 defaulting to 30") - methods.append((method_name, [30])) - elif method_name == "mal_id": methods.append((method_name, util.get_int_list(collections[c][m], "MyAnimeList ID"))) - elif method_name in ["anidb_id", "anidb_relation"]: methods.append((method_name, self.AniDB.validate_anidb_list(util.get_int_list(collections[c][m], "AniDB ID"), library.Plex.language))) - elif method_name == "trakt_list": methods.append((method_name, self.Trakt.validate_trakt_list(util.get_list(collections[c][m])))) - elif method_name == "trakt_watchlist": methods.append((method_name, self.Trakt.validate_trakt_watchlist(util.get_list(collections[c][m]), library.is_movie))) - elif method_name == "imdb_list": - new_list = [] - for imdb_list in util.get_list(collections[c][m]): - new_dictionary = {} - if isinstance(imdb_list, dict): - if "url" in imdb_list and imdb_list["url"]: imdb_url = imdb_list["url"] - else: raise Failed("Collection Error: No I") - if "limit" in imdb_list and imdb_list["limit"]: list_count = util.regex_first_int(imdb_list["limit"], "List Limit", default=0) - else: list_count = 0 - else: - imdb_url = str(imdb_list) - list_count = 0 - new_list.append({"url": imdb_url, "limit": list_count}) - methods.append((method_name, new_list)) - elif method_name in util.dictionary_lists: - if isinstance(collections[c][m], dict): - def get_int(parent, method, data, default, min=1, max=None): - if method not in data: logger.warning("Collection Warning: {} {} attribute not found using {} as default".format(parent, method, default)) - elif not data[method]: logger.warning("Collection Warning: {} {} attribute is blank using {} as default".format(parent, method, default)) - elif isinstance(data[method], int) and data[method] >= min: - if max is None or data[method] <= max: return data[method] - else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer <= {} using {} as default".format(parent, method, data[method], max, default)) - else: logger.warning("Collection Warning: {} {} attribute {} invalid must an integer >= {} using {} as default".format(parent, method, data[method], min, default)) - return default - if method_name == "filters": - for filter in collections[c][m]: - if filter in util.method_alias or (filter.endswith(".not") and filter[:-4] in util.method_alias): - final_filter = (util.method_alias[filter[:-4]] + filter[-4:]) if filter.endswith(".not") else util.method_alias[filter] - logger.warning("Collection Warning: {} filter will run as {}".format(filter, final_filter)) - else: - final_filter = filter - if final_filter in util.movie_only_filters and library.is_show: - logger.error("Collection Error: {} filter only works for movie libraries".format(final_filter)) - elif collections[c][m][filter] is None: - logger.error("Collection Error: {} filter is blank".format(final_filter)) - elif final_filter in util.all_filters: - filters.append((final_filter, collections[c][m][filter])) - else: - logger.error("Collection Error: {} filter not supported".format(final_filter)) - elif method_name == "plex_collectionless": - new_dictionary = {} - - prefix_list = [] - if "exclude_prefix" in collections[c][m] and collections[c][m]["exclude_prefix"]: - if isinstance(collections[c][m]["exclude_prefix"], list): - prefix_list.extend(collections[c][m]["exclude_prefix"]) - else: - prefix_list.append("{}".format(collections[c][m]["exclude_prefix"])) - - exact_list = [] - if "exclude" in collections[c][m] and collections[c][m]["exclude"]: - if isinstance(collections[c][m]["exclude"], list): - exact_list.extend(collections[c][m]["exclude"]) - else: - exact_list.append("{}".format(collections[c][m]["exclude"])) - - if len(prefix_list) == 0 and len(exact_list) == 0: - raise Failed("Collection Error: you must have at least one exclusion") - details["add_to_arr"] = False - details["collection_mode"] = "hide" - new_dictionary["exclude_prefix"] = prefix_list - new_dictionary["exclude"] = exact_list - methods.append((method_name, [new_dictionary])) - elif method_name == "plex_search": - search = [] - searches_used = [] - for search_attr in collections[c][m]: - if search_attr in util.method_alias or (search_attr.endswith(".not") and search_attr[:-4] in util.method_alias): - final_attr = (util.method_alias[search_attr[:-4]] + search_attr[-4:]) if search_attr.endswith(".not") else util.method_alias[search_attr] - logger.warning("Collection Warning: {} plex search attribute will run as {}".format(search_attr, final_attr)) - else: - final_attr = search_attr - if final_attr in util.movie_only_searches and library.is_show: - logger.error("Collection Error: {} plex search attribute only works for movie libraries".format(final_attr)) - elif util.remove_not(final_attr) in searches_used: - logger.error("Collection Error: Only one instance of {} can be used try using it as a filter instead".format(final_attr)) - elif final_attr in ["year", "year.not"]: - years = util.get_year_list(collections[c][m][search_attr], final_attr) - if len(years) > 0: - searches_used.append(util.remove_not(final_attr)) - search.append((final_attr, util.get_int_list(collections[c][m][search_attr], util.remove_not(final_attr)))) - elif final_attr in util.plex_searches: - if final_attr.startswith("tmdb_"): - final_attr = final_attr[5:] - searches_used.append(util.remove_not(final_attr)) - search.append((final_attr, util.get_list(collections[c][m][search_attr]))) - else: - logger.error("Collection Error: {} plex search attribute not supported".format(search_attr)) - methods.append((method_name, [search])) - elif method_name == "tmdb_discover": - new_dictionary = {"limit": 100} - for attr in collections[c][m]: - if collections[c][m][attr]: - attr_data = collections[c][m][attr] - if (library.is_movie and attr in util.discover_movie) or (library.is_show and attr in util.discover_tv): - if attr == "language": - if re.compile("([a-z]{2})-([A-Z]{2})").match(str(attr_data)): - new_dictionary[attr] = str(attr_data) - else: - logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern ([a-z]{2})-([A-Z]{2}) e.g. en-US".format(m, attr, attr_data)) - elif attr == "region": - if re.compile("^[A-Z]{2}$").match(str(attr_data)): - new_dictionary[attr] = str(attr_data) - else: - logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern ^[A-Z]{2}$ e.g. US".format(m, attr, attr_data)) - elif attr == "sort_by": - if (library.is_movie and attr_data in util.discover_movie_sort) or (library.is_show and attr_data in util.discover_tv_sort): - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: {} is invalid".format(m, attr, attr_data)) - elif attr == "certification_country": - if "certification" in collections[c][m] or "certification.lte" in collections[c][m] or "certification.gte" in collections[c][m]: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be used with either certification, certification.lte, or certification.gte".format(m, attr)) - elif attr in ["certification", "certification.lte", "certification.gte"]: - if "certification_country" in collections[c][m]: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be used with certification_country".format(m, attr)) - elif attr in ["include_adult", "include_null_first_air_dates", "screened_theatrically"]: - if attr_data is True: - new_dictionary[attr] = attr_data - elif attr in ["primary_release_date.gte", "primary_release_date.lte", "release_date.gte", "release_date.lte", "air_date.gte", "air_date.lte", "first_air_date.gte", "first_air_date.lte"]: - if re.compile("[0-1]?[0-9][/-][0-3]?[0-9][/-][1-2][890][0-9][0-9]").match(str(attr_data)): - the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") - new_dictionary[attr] = "{}-{}-{}".format(the_date[2], the_date[0], the_date[1]) - elif re.compile("[1-2][890][0-9][0-9][/-][0-1]?[0-9][/-][0-3]?[0-9]").match(str(attr_data)): - the_date = str(attr_data).split("/") if "/" in str(attr_data) else str(attr_data).split("-") - new_dictionary[attr] = "{}-{}-{}".format(the_date[0], the_date[1], the_date[2]) - else: - logger.error("Collection Error: Skipping {} attribute {}: {} must match pattern MM/DD/YYYY e.g. 12/25/2020".format(m, attr, attr_data)) - elif attr in ["primary_release_year", "year", "first_air_date_year"]: - if isinstance(attr_data, int) and 1800 < attr_data and attr_data < 2200: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be a valid year e.g. 1990".format(m, attr)) - elif attr in ["vote_count.gte", "vote_count.lte", "vote_average.gte", "vote_average.lte", "with_runtime.gte", "with_runtime.lte"]: - if (isinstance(attr_data, int) or isinstance(attr_data, float)) and 0 < attr_data: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be a valid number greater then 0".format(m, attr)) - elif attr in ["with_cast", "with_crew", "with_people", "with_companies", "with_networks", "with_genres", "without_genres", "with_keywords", "without_keywords", "with_original_language", "timezone"]: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: {} attribute {} not supported".format(m, attr)) - elif attr == "limit": - if isinstance(attr_data, int) and attr_data > 0: - new_dictionary[attr] = attr_data - else: - logger.error("Collection Error: Skipping {} attribute {}: must be a valid number greater then 0".format(m, attr)) - else: - logger.error("Collection Error: {} attribute {} not supported".format(m, attr)) - else: - logger.error("Collection Error: {} parameter {} is blank".format(m, attr)) - if len(new_dictionary) > 1: - methods.append((method_name, [new_dictionary])) - else: - logger.error("Collection Error: {} had no valid fields".format(m)) - elif "tautulli" in method_name: - new_dictionary = {} - if method_name == "tautulli_popular": new_dictionary["list_type"] = "popular" - elif method_name == "tautulli_watched": new_dictionary["list_type"] = "watched" - else: raise Failed("Collection Error: {} attribute not supported".format(method_name)) - new_dictionary["list_days"] = get_int(method_name, "list_days", collections[c][m], 30) - new_dictionary["list_size"] = get_int(method_name, "list_size", collections[c][m], 10) - new_dictionary["list_buffer"] = get_int(method_name, "list_buffer", collections[c][m], 20) - methods.append((method_name, [new_dictionary])) - elif method_name == "mal_season": - new_dictionary = {"sort_by": "anime_num_list_users"} - current_time = datetime.now() - if current_time.month in [1, 2, 3]: new_dictionary["season"] = "winter" - elif current_time.month in [4, 5, 6]: new_dictionary["season"] = "spring" - elif current_time.month in [7, 8, 9]: new_dictionary["season"] = "summer" - elif current_time.month in [10, 11, 12]: new_dictionary["season"] = "fall" - new_dictionary["year"] = get_int(method_name, "year", collections[c][m], current_time.year, min=1917, max=current_time.year + 1) - new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=500) - if "sort_by" not in collections[c][m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using members as default") - elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using members as default") - elif collections[c][m]["sort_by"] not in util.mal_season_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'members' or 'score' using members as default".format(collections[c][m]["sort_by"])) - else: new_dictionary["sort_by"] = util.mal_season_sort[collections[c][m]["sort_by"]] - if "season" not in collections[c][m]: logger.warning("Collection Warning: mal_season season attribute not found using the current season: {} as default".format(new_dictionary["season"])) - elif not collections[c][m]["season"]: logger.warning("Collection Warning: mal_season season attribute is blank using the current season: {} as default".format(new_dictionary["season"])) - elif collections[c][m]["season"] not in util.pretty_seasons: logger.warning("Collection Warning: mal_season season attribute {} invalid must be either 'winter', 'spring', 'summer' or 'fall' using the current season: {} as default".format(collections[c][m]["season"], new_dictionary["season"])) - else: new_dictionary["season"] = collections[c][m]["season"] - methods.append((method_name, [new_dictionary])) - elif method_name == "mal_userlist": - new_dictionary = {"status": "all", "sort_by": "list_score"} - if "username" not in collections[c][m]: raise Failed("Collection Error: mal_userlist username attribute is required") - elif not collections[c][m]["username"]: raise Failed("Collection Error: mal_userlist username attribute is blank") - else: new_dictionary["username"] = collections[c][m]["username"] - if "status" not in collections[c][m]: logger.warning("Collection Warning: mal_season status attribute not found using all as default") - elif not collections[c][m]["status"]: logger.warning("Collection Warning: mal_season status attribute is blank using all as default") - elif collections[c][m]["status"] not in util.mal_userlist_status: logger.warning("Collection Warning: mal_season status attribute {} invalid must be either 'all', 'watching', 'completed', 'on_hold', 'dropped' or 'plan_to_watch' using all as default".format(collections[c][m]["status"])) - else: new_dictionary["status"] = util.mal_userlist_status[collections[c][m]["status"]] - if "sort_by" not in collections[c][m]: logger.warning("Collection Warning: mal_season sort_by attribute not found using score as default") - elif not collections[c][m]["sort_by"]: logger.warning("Collection Warning: mal_season sort_by attribute is blank using score as default") - elif collections[c][m]["sort_by"] not in util.mal_userlist_sort: logger.warning("Collection Warning: mal_season sort_by attribute {} invalid must be either 'score', 'last_updated', 'title' or 'start_date' using score as default".format(collections[c][m]["sort_by"])) - else: new_dictionary["sort_by"] = util.mal_userlist_sort[collections[c][m]["sort_by"]] - new_dictionary["limit"] = get_int(method_name, "limit", collections[c][m], 100, max=1000) - methods.append((method_name, [new_dictionary])) - else: - logger.error("Collection Error: {} attribute is not a dictionary: {}".format(m, collections[c][m])) - elif method_name in util.count_lists: - list_count = util.regex_first_int(collections[c][m], "List Size", default=20) - if list_count > 0: - methods.append((method_name, [list_count])) - else: - logger.error("Collection Error: {} must be an integer greater then 0 defaulting to 20".format(method_name)) - methods.append((method_name, [20])) - elif method_name in util.tmdb_lists: - values = self.TMDb.validate_tmdb_list(util.get_int_list(collections[c][m], "TMDb {} ID".format(util.tmdb_type[method_name])), util.tmdb_type[method_name]) - if method_name[-8:] == "_details": - if method_name in ["tmdb_collection_details", "tmdb_movie_details", "tmdb_show_details"]: - item = self.TMDb.get_movie_show_or_collection(values[0], library.is_movie) - if "summary" not in details and hasattr(item, "overview") and item.overview: - details["summary"] = item.overview - if "background" not in details and hasattr(item, "backdrop_path") and item.backdrop_path: - details["background"] = ("url", "{}{}".format(self.TMDb.image_url, item.backdrop_path), method_name[:-8]) - if "poster" not in details and hasattr(item, "poster_path") and item.poster_path: - details["poster"] = ("url", "{}{}".format(self.TMDb.image_url, item.poster_path), method_name[:-8]) - else: - item = self.TMDb.get_list(values[0]) - if "summary" not in details and hasattr(item, "description") and item.description: - details["summary"] = item.description - methods.append((method_name[:-8], values)) - else: - methods.append((method_name, values)) - elif method_name in util.all_lists: methods.append((method_name, util.get_list(collections[c][m]))) - elif method_name not in ["sync_mode", "schedule", "tmdb_person"]: logger.error("Collection Error: {} attribute not supported".format(method_name)) - else: - logger.error("Collection Error: {} attribute is blank".format(m)) - except Failed as e: - logger.error(e) - - for i, f in enumerate(filters): + for i, f in enumerate(builder.filters): if i == 0: logger.info("") logger.info("Collection Filter {}: {}".format(f[0], f[1])) - do_arr = False - if library.Radarr: - do_arr = details["add_to_arr"] if "add_to_arr" in details else library.Radarr.add - if library.Sonarr: - do_arr = details["add_to_arr"] if "add_to_arr" in details else library.Sonarr.add - - - items_found = 0 - library.clear_collection_missing(collection_name) - - for method, values in methods: - logger.debug("Method: {}".format(method)) - logger.debug("Values: {}".format(values)) - pretty = util.pretty_names[method] if method in util.pretty_names else method - for value in values: - logger.debug("Value: {}".format(value)) - items = [] - missing_movies = [] - missing_shows = [] - def check_map(input_ids): - movie_ids, show_ids = input_ids - items_found_inside = 0 - if len(movie_ids) > 0: - items_found_inside += len(movie_ids) - for movie_id in movie_ids: - if movie_id in movie_map: items.append(movie_map[movie_id]) - else: missing_movies.append(movie_id) - if len(show_ids) > 0: - items_found_inside += len(show_ids) - for show_id in show_ids: - if show_id in show_map: items.append(show_map[show_id]) - else: missing_shows.append(show_id) - return items_found_inside - logger.info("") - if method == "plex_all": - logger.info("Processing {} {}".format(pretty, "Movies" if library.is_movie else "Shows")) - items = library.Plex.all() - items_found += len(items) - elif method == "plex_collection": - items = value.items() - items_found += len(items) - elif method == "plex_search": - search_terms = {} - output = "" - for i, attr_pair in enumerate(value): - search_list = attr_pair[1] - final_method = attr_pair[0][:-4] + "!" if attr_pair[0][-4:] == ".not" else attr_pair[0] - if library.is_show: - final_method = "show." + final_method - search_terms[final_method] = search_list - ors = "" - for o, param in enumerate(attr_pair[1]): - ors += "{}{}".format(" OR " if o > 0 else "{}(".format(attr_pair[0]), param) - logger.info("\t\t AND {})".format(ors) if i > 0 else "Processing {}: {})".format(pretty, ors)) - items = library.Plex.search(**search_terms) - items_found += len(items) - elif method == "plex_collectionless": - good_collections = [] - for col in library.get_all_collections(): - keep_collection = True - for pre in value["exclude_prefix"]: - if col.title.startswith(pre) or (col.titleSort and col.titleSort.startswith(pre)): - keep_collection = False - break - for ext in value["exclude"]: - if col.title == ext or (col.titleSort and col.titleSort == ext): - keep_collection = False - break - if keep_collection: - good_collections.append(col.title.lower()) - - all_items = library.Plex.all() - length = 0 - for i, item in enumerate(all_items, 1): - length = util.print_return(length, "Processing: {}/{} {}".format(i, len(all_items), item.title)) - add_item = True - for collection in item.collections: - if collection.tag.lower() in good_collections: - add_item = False - break - if add_item: - items.append(item) - items_found += len(items) - util.print_end(length, "Processed {} {}".format(len(all_items), "Movies" if library.is_movie else "Shows")) - elif "tautulli" in method: - items = library.Tautulli.get_items(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.AniDB.get_items(method, value, library.Plex.language)) - elif "mal" in method: items_found += check_map(self.MyAnimeList.get_items(method, value)) - elif "tvdb" in method: items_found += check_map(self.TVDb.get_items(method, value, library.Plex.language)) - elif "imdb" in method: items_found += check_map(self.IMDb.get_items(method, value, library.Plex.language)) - elif "tmdb" in method: items_found += check_map(self.TMDb.get_items(method, value, library.is_movie)) - elif "trakt" in method: items_found += check_map(self.Trakt.get_items(method, value, library.is_movie)) - else: logger.error("Collection Error: {} method not supported".format(method)) - - if len(items) > 0: map = library.add_to_collection(collection_obj if collection_obj else collection_name, items, filters, show_filtered, map, movie_map, show_map) - else: logger.error("No items found to add to this collection ") - - if len(missing_movies) > 0 or len(missing_shows) > 0: - logger.info("") - if len(missing_movies) > 0: - not_lang = None - terms = None - for filter_method, filter_data in filters: - if filter_method.startswith("original_language"): - terms = filter_data if isinstance(filter_data, list) else [lang.strip().lower() for lang in str(filter_data).split(",")] - not_lang = filter_method.endswith(".not") - break - - missing_movies_with_names = [] - for missing_id in missing_movies: - try: - movie = self.TMDb.get_movie(missing_id) - title = str(movie.title) - if not_lang is None or (not_lang is True and movie.original_language not in terms) or (not_lang is False and movie.original_language in terms): - missing_movies_with_names.append((title, missing_id)) - logger.info("{} Collection | ? | {} (TMDb: {})".format(collection_name, title, missing_id)) - elif show_filtered is True: - logger.info("{} Collection | X | {} (TMDb: {})".format(collection_name, title, missing_id)) - except Failed as e: - logger.error(e) - logger.info("{} Movie{} Missing".format(len(missing_movies_with_names), "s" if len(missing_movies_with_names) > 1 else "")) - library.save_missing(collection_name, missing_movies_with_names, True) - if do_arr and library.Radarr: - library.Radarr.add_tmdb([missing_id for title, missing_id in missing_movies_with_names]) - if len(missing_shows) > 0 and library.is_show: - missing_shows_with_names = [] - for missing_id in missing_shows: - try: - title = str(self.TVDb.get_series(library.Plex.language, tvdb_id=missing_id).title.encode("ascii", "replace").decode()) - missing_shows_with_names.append((title, missing_id)) - logger.info("{} Collection | ? | {} (TVDB: {})".format(collection_name, title, missing_id)) - except Failed as e: - logger.error(e) - logger.info("{} Show{} Missing".format(len(missing_shows), "s" if len(missing_shows) > 1 else "")) - library.save_missing(c, missing_shows_with_names, False) - if do_arr and library.Sonarr: - library.Sonarr.add_tvdb(missing_shows) - - library.del_collection_if_empty(collection_name) - - if (sync_collection or collectionless) and items_found > 0: - logger.info("") - count_removed = 0 - for ratingKey, item in map.items(): - if item is not None: - logger.info("{} Collection | - | {}".format(collection_name, item.title)) - item.removeCollection(collection_name) - count_removed += 1 - logger.info("{} {}{} Removed".format(count_removed, "Movie" if library.is_movie else "Show", "s" if count_removed == 1 else "")) - logger.info("") + builder.run_methods(collection_obj, collection_name, map, movie_map, show_map) try: plex_collection = library.get_collection(collection_name) @@ -1020,85 +406,12 @@ def check_map(input_ids): logger.debug(e) continue - edits = {} - if "sort_title" in details: - edits["titleSort.value"] = details["sort_title"] - edits["titleSort.locked"] = 1 - if "content_rating" in details: - edits["contentRating.value"] = details["content_rating"] - edits["contentRating.locked"] = 1 - if "summary" in details: - edits["summary.value"] = details["summary"] - edits["summary.locked"] = 1 - if len(edits) > 0: - logger.debug(edits) - plex_collection.edit(**edits) - plex_collection.reload() - logger.info("Details: have been updated") - if "collection_mode" in details: - plex_collection.modeUpdate(mode=details["collection_mode"]) - if "collection_order" in details: - plex_collection.sortUpdate(sort=details["collection_order"]) - - if library.asset_directory: - name_mapping = c - if "name_mapping" in collections[c]: - if collections[c]["name_mapping"]: name_mapping = collections[c]["name_mapping"] - else: logger.error("Collection Error: name_mapping attribute is blank") - path = os.path.join(library.asset_directory, "{}".format(name_mapping), "poster.*") - matches = glob.glob(path) - if len(matches) > 0: - for match in matches: posters_found.append(("file", os.path.abspath(match), "asset_directory")) - elif len(posters_found) == 0 and "poster" not in details: logger.warning("poster not found at: {}".format(os.path.abspath(path))) - path = os.path.join(library.asset_directory, "{}".format(name_mapping), "background.*") - matches = glob.glob(path) - if len(matches) > 0: - for match in matches: backgrounds_found.append(("file", os.path.abspath(match), "asset_directory")) - elif len(backgrounds_found) == 0 and "background" not in details: logger.warning("background not found at: {}".format(os.path.abspath(path))) + builder.update_details(plex_collection) - poster = util.choose_from_list(posters_found, "poster", list_type="tuple") - if not poster and "poster" in details: poster = details["poster"] - if poster: - if poster[0] == "url": plex_collection.uploadPoster(url=poster[1]) - else: plex_collection.uploadPoster(filepath=poster[1]) - logger.info("Detail: {} updated poster to [{}] {}".format(poster[2], poster[0], poster[1])) - - background = util.choose_from_list(backgrounds_found, "background", list_type="tuple") - if not background and "background" in details: background = details["background"] - if background: - if background[0] == "url": plex_collection.uploadArt(url=background[1]) - else: plex_collection.uploadArt(filepath=background[1]) - logger.info("Detail: {} updated background to [{}] {}".format(background[2], background[0], background[1])) - - if library.asset_directory: - path = os.path.join(library.asset_directory, "{}".format(name_mapping)) - dirs = [folder for folder in os.listdir(path) if os.path.isdir(os.path.join(path, folder))] - if len(dirs) > 0: - for item in plex_collection.items(): - folder = os.path.basename(os.path.dirname(item.locations[0])) - if folder in dirs: - files = [file for file in os.listdir(os.path.join(path, folder)) if os.path.isfile(os.path.join(path, folder, file))] - poster_path = None - background_path = None - for file in files: - if poster_path is None and file.startswith("poster."): - poster_path = os.path.join(path, folder, file) - if background_path is None and file.startswith("background."): - background_path = os.path.join(path, folder, file) - if poster_path: - item.uploadPoster(filepath=poster_path) - logger.info("Detail: asset_directory updated {}'s poster to [file] {}".format(item.title, poster_path)) - if background_path: - item.uploadArt(filepath=background_path) - logger.info("Detail: asset_directory updated {}'s background to [file] {}".format(item.title, background_path)) - if poster_path is None and background_path is None: - logger.warning("No Files Found: {}".format(os.path.join(path, folder))) - else: - logger.warning("No Folder: {}".format(os.path.join(path, folder))) except Exception as e: util.print_stacktrace() logger.error("Unknown Error: {}".format(e)) - if library.show_unmanaged is True: + if library.show_unmanaged is True and not test and not requested_collections: logger.info("") util.seperator("Unmanaged Collections in {} Library".format(library.name)) logger.info("") @@ -1110,6 +423,7 @@ def check_map(input_ids): unmanaged_count += 1 logger.info("{} Unmanaged Collections".format(unmanaged_count)) else: + logger.info("") logger.error("No collection to update") def map_guids(self, library): @@ -1121,7 +435,12 @@ def map_guids(self, library): items = library.Plex.all() for i, item in enumerate(items, 1): length = util.print_return(length, "Processing: {}/{} {}".format(i, len(items), item.title)) - id_type, main_id = self.get_id(item, library, length) + try: + id_type, main_id = self.get_id(item, library, length) + except BadRequest: + util.print_stacktrace() + util.print_end(length, "{} {:<46} | {} for {}".format("Cache | ! |" if self.Cache else "Mapping Error:", item.guid, error_message, item.title)) + continue if isinstance(main_id, list): if id_type == "movie": for m in main_id: movie_map[m] = item.ratingKey @@ -1155,13 +474,13 @@ def get_id(self, item, library, length): if item_type == "plex" and library.is_movie: for guid_tag in item.guids: url_parsed = requests.utils.urlparse(guid_tag.id) - if url_parsed.scheme == "tmdb": tmdb_id = url_parsed.netloc + if url_parsed.scheme == "tmdb": tmdb_id = int(url_parsed.netloc) elif url_parsed.scheme == "imdb": imdb_id = url_parsed.netloc elif item_type == "imdb": imdb_id = check_id - elif item_type == "thetvdb": tvdb_id = check_id - elif item_type == "themoviedb": tmdb_id = check_id + elif item_type == "thetvdb": tvdb_id = int(check_id) + elif item_type == "themoviedb": tmdb_id = int(check_id) elif item_type == "hama": - if check_id.startswith("tvdb"): tvdb_id = re.search("-(.*)", check_id).group(1) + if check_id.startswith("tvdb"): tvdb_id = int(re.search("-(.*)", check_id).group(1)) elif check_id.startswith("anidb"): anidb_id = re.search("-(.*)", check_id).group(1) else: error_message = "Hama Agent ID: {} not supported".format(check_id) elif item_type == "myanimelist": mal_id = check_id diff --git a/modules/mal.py b/modules/mal.py index 38654e967..7a9c3739b 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -17,7 +17,7 @@ def convert_tmdb_to_mal(self, tmdb_id): return self.convert_mal(tmdb def convert_mal(self, input_id, from_id, to_id): for attrs in self.ids: if from_id in attrs and int(attrs[from_id]) == int(input_id) and to_id in attrs and int(attrs[to_id]) > 0: - return attrs[to_id] + return int(attrs[to_id]) raise Failed("MyAnimeList Error: {} ID not found for {}: {}".format(util.pretty_ids[to_id], util.pretty_ids[from_id], input_id)) def find_mal_ids(self, mal_id): diff --git a/modules/plex.py b/modules/plex.py index 99ce3c8f0..21cab3021 100644 --- a/modules/plex.py +++ b/modules/plex.py @@ -1,9 +1,6 @@ import datetime, logging, os, requests from lxml import html from modules import util -from modules.radarr import RadarrAPI -from modules.sonarr import SonarrAPI -from modules.tautulli import TautulliAPI from modules.util import Failed from plexapi.exceptions import BadRequest, NotFound, Unauthorized from plexapi.library import Collections, MovieSection, ShowSection @@ -15,7 +12,7 @@ logger = logging.getLogger("Plex Meta Manager") class PlexAPI: - def __init__(self, params): + def __init__(self, params, TMDb, TVDb): try: self.PlexServer = PlexServer(params["plex"]["url"], params["plex"]["token"], timeout=600) except Unauthorized: raise Failed("Plex Error: Plex token is invalid") except ValueError as e: raise Failed("Plex Error: {}".format(e)) @@ -29,59 +26,52 @@ def __init__(self, params): try: self.data, ind, bsi = yaml.util.load_yaml_guess_indent(open(params["metadata_path"], encoding="utf-8")) except yaml.scanner.ScannerError as e: raise Failed("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - self.metadata = None - if "metadata" in self.data: - if self.data["metadata"]: self.metadata = self.data["metadata"] - else: logger.warning("Config Warning: metadata attribute is blank") - else: logger.warning("Config Warning: metadata attribute not found") + def get_dict(attribute): + if attribute in self.data: + if self.data[attribute]: + if isinstance(self.data[attribute], dict): return self.data[attribute] + else: logger.waring("Config Warning: {} must be a dictionary".format(attribute)) + else: logger.warning("Config Warning: {} attribute is blank".format(attribute)) + return None - self.collections = None - if "collections" in self.data: - if self.data["collections"]: self.collections = self.data["collections"] - else: logger.warning("Config Warning: collections attribute is blank") - else: logger.warning("Config Warning: collections attribute not found") + self.metadata = get_dict("metadata") + self.templates = get_dict("templates") + self.collections = get_dict("collections") if self.metadata is None and self.collections is None: raise Failed("YAML Error: metadata attributes or collections attribute required") if params["asset_directory"]: - logger.info("Using Asset Directory: {}".format(params["asset_directory"])) + for ad in params["asset_directory"]: + logger.info("Using Asset Directory: {}".format(ad)) + self.TMDb = TMDb + self.TVDb = TVDb self.Radarr = None - if params["tmdb"] and params["radarr"]: - logger.info("Connecting to {} library's Radarr...".format(params["name"])) - try: self.Radarr = RadarrAPI(params["tmdb"], params["radarr"]) - except Failed as e: logger.error(e) - logger.info("{} library's Radarr Connection {}".format(params["name"], "Failed" if self.Radarr is None else "Successful")) - self.Sonarr = None - if params["tvdb"] and params["sonarr"]: - logger.info("Connecting to {} library's Sonarr...".format(params["name"])) - try: self.Sonarr = SonarrAPI(params["tvdb"], params["sonarr"], self.Plex.language) - except Failed as e: logger.error(e) - logger.info("{} library's Sonarr Connection {}".format(params["name"], "Failed" if self.Sonarr is None else "Successful")) - self.Tautulli = None - if params["tautulli"]: - logger.info("Connecting to {} library's Tautulli...".format(params["name"])) - try: self.Tautulli = TautulliAPI(params["tautulli"]) - except Failed as e: logger.error(e) - logger.info("{} library's Tautulli Connection {}".format(params["name"], "Failed" if self.Tautulli is None else "Successful")) - - self.TMDb = params["tmdb"] - self.TVDb = params["tvdb"] self.name = params["name"] - self.missing_path = os.path.join(os.path.dirname(os.path.abspath(params["metadata_path"])), "{}_missing.yml".format(os.path.splitext(os.path.basename(params["metadata_path"]))[0])) self.metadata_path = params["metadata_path"] self.asset_directory = params["asset_directory"] self.sync_mode = params["sync_mode"] self.show_unmanaged = params["show_unmanaged"] self.show_filtered = params["show_filtered"] + self.show_missing = params["show_missing"] + self.save_missing = params["save_missing"] self.plex = params["plex"] - self.radarr = params["radarr"] - self.sonarr = params["sonarr"] - self.tautulli = params["tautulli"] + self.missing = {} + + def add_Radarr(self, Radarr): + self.Radarr = Radarr + + def add_Sonarr(self, Sonarr): + self.Sonarr = Sonarr + + def add_Tautulli(self, Tautulli): + self.Tautulli = Tautulli + + @retry(stop_max_attempt_number=6, wait_fixed=10000) def search(self, title, libtype=None, year=None): @@ -115,51 +105,18 @@ def validate_collections(self, collections): raise Failed("Collection Error: No valid Plex Collections in {}".format(collections[c][m])) return valid_collections - def del_collection_if_empty(self, collection): - missing_data = {} - if not os.path.exists(self.missing_path): - with open(self.missing_path, "w"): pass + def add_missing(self, collection, items, is_movie): + col_name = collection.encode("ascii", "replace").decode() + if col_name not in self.missing: + self.missing[col_name] = {} + section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)" + if section not in self.missing[col_name]: + self.missing[col_name][section] = {} + for title, item_id in items: + self.missing[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode() + with open(self.missing_path, "w"): pass try: - missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path)) - if not missing_data: - missing_data = {} - if collection in missing_data and len(missing_data[collection]) == 0: - del missing_data[collection] - yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi) - except yaml.scanner.ScannerError as e: - logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - - def clear_collection_missing(self, collection): - missing_data = {} - if not os.path.exists(self.missing_path): - with open(self.missing_path, "w"): pass - try: - missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path)) - if not missing_data: - missing_data = {} - if collection in missing_data: - missing_data[collection.encode("ascii", "replace").decode()] = {} - yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi) - except yaml.scanner.ScannerError as e: - logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) - - def save_missing(self, collection, items, is_movie): - missing_data = {} - if not os.path.exists(self.missing_path): - with open(self.missing_path, "w"): pass - try: - missing_data, ind, bsi = yaml.util.load_yaml_guess_indent(open(self.missing_path)) - if not missing_data: - missing_data = {} - col_name = collection.encode("ascii", "replace").decode() - if col_name not in missing_data: - missing_data[col_name] = {} - section = "Movies Missing (TMDb IDs)" if is_movie else "Shows Missing (TVDb IDs)" - if section not in missing_data[col_name]: - missing_data[col_name][section] = {} - for title, item_id in items: - missing_data[col_name][section][int(item_id)] = str(title).encode("ascii", "replace").decode() - yaml.round_trip_dump(missing_data, open(self.missing_path, "w"), indent=ind, block_seq_indent=bsi) + yaml.round_trip_dump(self.missing, open(self.missing_path, "w")) except yaml.scanner.ScannerError as e: logger.error("YAML Error: {}".format(str(e).replace("\n", "\n|\t "))) @@ -170,8 +127,11 @@ def add_to_collection(self, collection, items, filters, show_filtered, map, movi max_length = len(str(total)) length = 0 for i, item in enumerate(items, 1): - try: current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item)) - except BadRequest: raise Failed("Plex Error: Item {} not found".format(item)) + try: + current = self.fetchItem(item.ratingKey if isinstance(item, (Movie, Show)) else int(item)) + except (BadRequest, NotFound): + logger.error("Plex Error: Item {} not found".format(item)) + continue match = True if filters: length = util.print_return(length, "Filtering {}/{} {}".format((" " * (max_length - len(str(i)))) + str(i), total, current.title)) @@ -185,7 +145,7 @@ def add_to_collection(self, collection, items, filters, show_filtered, map, movi match = False break elif method == "original_language": - terms = f[1] if isinstance(f[1], list) else [lang.lower() for lang in str(f[1]).split(", ")] + terms = util.get_list(f[1], lower=True) tmdb_id = None movie = None for key, value in movie_map.items(): @@ -214,7 +174,7 @@ def add_to_collection(self, collection, items, filters, show_filtered, map, movi match = False break else: - terms = f[1] if isinstance(f[1], list) else str(f[1]).split(", ") + terms = util.get_list(f[1]) if method in ["video_resolution", "audio_language", "subtitle_language"]: for media in current.media: if method == "video_resolution": attrs = [media.videoResolution] @@ -241,13 +201,15 @@ def add_to_collection(self, collection, items, filters, show_filtered, map, movi def search_item(self, data, year=None): return util.choose_from_list(self.search(data, year=year), "movie" if self.is_movie else "show", str(data), exact=True) - def update_metadata(self, TMDb): + def update_metadata(self, TMDb, test): logger.info("") util.seperator("{} Library Metadata".format(self.name)) logger.info("") if not self.metadata: raise Failed("No metadata to edit") for m in self.metadata: + if test and ("test" not in self.metadata[m] or self.metadata[m]["test"] is not True): + continue logger.info("") util.seperator() logger.info("") @@ -316,10 +278,11 @@ def add_edit(name, current, group, key=None, value=None): add_edit("originally_available", str(item.originallyAvailableAt)[:-9], self.metadata[m], key="originallyAvailableAt", value=originally_available) add_edit("rating", item.rating, self.metadata[m], value=rating) add_edit("content_rating", item.contentRating, self.metadata[m], key="contentRating") - originalTitle = item.originalTitle if self.is_movie else item._data.attrib.get("originalTitle") - add_edit("original_title", originalTitle, self.metadata[m], key="originalTitle", value=original_title) + item_original_title = item.originalTitle if self.is_movie else item._data.attrib.get("originalTitle") + add_edit("original_title", item_original_title, self.metadata[m], key="originalTitle", value=original_title) add_edit("studio", item.studio, self.metadata[m], value=studio) - add_edit("tagline", item.tagline, self.metadata[m], value=tagline) + 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("summary", item.summary, self.metadata[m], value=summary) if len(edits) > 0: logger.debug("Details Update: {}".format(edits)) diff --git a/modules/radarr.py b/modules/radarr.py index a0257edf2..10591f0f4 100644 --- a/modules/radarr.py +++ b/modules/radarr.py @@ -10,8 +10,7 @@ def __init__(self, tmdb, params): self.url_params = {"apikey": "{}".format(params["token"])} self.base_url = "{}/api{}".format(params["url"], "/v3/" if params["version"] == "v3" else "/") try: - response = requests.get("{}system/status".format(self.base_url), params=self.url_params) - result = response.json() + result = requests.get("{}system/status".format(self.base_url), params=self.url_params).json() except Exception as e: util.print_stacktrace() raise Failed("Radarr Error: Could not connect to Radarr at {}".format(params["url"])) @@ -19,10 +18,9 @@ def __init__(self, tmdb, params): raise Failed("Radarr Error: Invalid API Key") if "version" not in result: raise Failed("Radarr Error: Unexpected Response Check URL") - response = requests.get("{}{}".format(self.base_url, "qualityProfile" if params["version"] == "v3" else "profile"), params=self.url_params) self.quality_profile_id = None profiles = "" - for profile in response.json(): + for profile in self.send_get("{}{}".format(self.base_url, "qualityProfile" if params["version"] == "v3" else "profile")): if len(profiles) > 0: profiles += ", " profiles += profile["name"] @@ -37,11 +35,24 @@ def __init__(self, tmdb, params): self.root_folder_path = params["root_folder_path"] self.add = params["add"] self.search = params["search"] + self.tag = params["tag"] - def add_tmdb(self, tmdb_ids): + def add_tmdb(self, tmdb_ids, tag=None): logger.info("") logger.debug("TMDb IDs: {}".format(tmdb_ids)) + tag_nums = [] add_count = 0 + if tag is None: + tag = self.tag + if tag: + tag_cache = {} + for label in tag: + self.send_post("{}tag".format(self.base_url), {"label": str(label)}) + for t in self.send_get("{}tag".format(self.base_url)): + tag_cache[t["label"]] = t["id"] + for label in tag: + if label in tag_cache: + tag_nums.append(tag_cache[label]) for tmdb_id in tmdb_ids: try: movie = self.tmdb.get_movie(tmdb_id) @@ -74,6 +85,8 @@ def add_tmdb(self, tmdb_ids): "images": [{"covertype": "poster", "url": poster}], "addOptions": {"searchForMovie": self.search} } + if tag_nums: + url_json["tags"] = tag_nums response = self.send_post("{}movie".format(self.base_url), url_json) if response.status_code < 400: logger.info("Added to Radarr | {:<6} | {}".format(tmdb_id, movie.title)) @@ -86,6 +99,10 @@ def add_tmdb(self, tmdb_ids): logger.error("Radarr Error: {}".format(response.json())) logger.info("{} Movie{} added to Radarr".format(add_count, "s" if add_count > 1 else "")) + @retry(stop_max_attempt_number=6, wait_fixed=10000) + def send_get(self, url): + return requests.get(url, params=self.url_params).json() + @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_post(self, url, url_json): return requests.post(url, json=url_json, params=self.url_params) diff --git a/modules/sonarr.py b/modules/sonarr.py index 1351ebcb1..9b76f261c 100644 --- a/modules/sonarr.py +++ b/modules/sonarr.py @@ -10,8 +10,7 @@ def __init__(self, tvdb, params, language): self.url_params = {"apikey": "{}".format(params["token"])} self.base_url = "{}/api{}".format(params["url"], "/v3/" if params["version"] == "v3" else "/") try: - response = requests.get("{}system/status".format(self.base_url), params=self.url_params) - result = response.json() + result = requests.get("{}system/status".format(self.base_url), params=self.url_params).json() except Exception as e: util.print_stacktrace() raise Failed("Sonarr Error: Could not connect to Sonarr at {}".format(params["url"])) @@ -19,10 +18,9 @@ def __init__(self, tvdb, params, language): raise Failed("Sonarr Error: Invalid API Key") if "version" not in result: raise Failed("Sonarr Error: Unexpected Response Check URL") - response = requests.get("{}{}".format(self.base_url, "qualityProfile" if params["version"] == "v3" else "profile"), params=self.url_params) self.quality_profile_id = None profiles = "" - for profile in response.json(): + for profile in self.send_get("{}{}".format(self.base_url, "qualityProfile" if params["version"] == "v3" else "profile")): if len(profiles) > 0: profiles += ", " profiles += profile["name"] @@ -38,11 +36,24 @@ def __init__(self, tvdb, params, language): self.root_folder_path = params["root_folder_path"] self.add = params["add"] self.search = params["search"] + self.tag = params["tag"] - def add_tvdb(self, tvdb_ids): + def add_tvdb(self, tvdb_ids, tag=None): logger.info("") logger.debug("TVDb IDs: {}".format(tvdb_ids)) + tag_nums = [] add_count = 0 + if tag is None: + tag = self.tag + if tag: + tag_cache = {} + for label in tag: + self.send_post("{}tag".format(self.base_url), {"label": str(label)}) + for t in self.send_get("{}tag".format(self.base_url)): + tag_cache[t["label"]] = t["id"] + for label in tag: + if label in tag_cache: + tag_nums.append(tag_cache[label]) for tvdb_id in tvdb_ids: try: show = self.tvdb.get_series(self.language, tvdb_id=tvdb_id) @@ -65,6 +76,8 @@ def add_tvdb(self, tvdb_ids): "images": [{"covertype": "poster", "url": show.poster_path}], "addOptions": {"searchForMissingEpisodes": self.search} } + if tag_nums: + url_json["tags"] = tag_nums response = self.send_post("{}series".format(self.base_url), url_json) if response.status_code < 400: logger.info("Added to Sonarr | {:<6} | {}".format(tvdb_id, show.title)) @@ -77,6 +90,10 @@ def add_tvdb(self, tvdb_ids): logger.error("Sonarr Error: {}".format(response.json())) logger.info("{} Show{} added to Sonarr".format(add_count, "s" if add_count > 1 else "")) + @retry(stop_max_attempt_number=6, wait_fixed=10000) + def send_get(self, url): + return requests.get(url, params=self.url_params).json() + @retry(stop_max_attempt_number=6, wait_fixed=10000) def send_post(self, url, url_json): return requests.post(url, json=url_json, params=self.url_params) diff --git a/modules/tests.py b/modules/tests.py index 156d0c9a0..cf7ed4eff 100644 --- a/modules/tests.py +++ b/modules/tests.py @@ -10,10 +10,8 @@ def run_tests(default_dir): config = Config(default_dir) logger.info("") util.seperator("Mapping Tests") - - config.map_guids(config.libraries[0]) - config.map_guids(config.libraries[1]) - config.map_guids(config.libraries[2]) + for library in config.libraries: + config.map_guids(library) anidb_tests(config) imdb_tests(config) mal_tests(config) diff --git a/modules/tmdb.py b/modules/tmdb.py index 78732687b..403a727cc 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -38,7 +38,7 @@ def convert_from_tmdb(self, tmdb_id, convert_to, is_movie): def convert_to_tmdb(self, external_id, external_source, is_movie): search_results = self.Movie.external(external_id=external_id, external_source=external_source) search = search_results["movie_results" if is_movie else "tv_results"] - if len(search) == 1: return search[0]["id"] + if len(search) == 1: return int(search[0]["id"]) else: raise Failed("TMDb Error: No TMDb ID found for {} {}".format(external_source.upper().replace("B_", "b "), external_id)) def convert_tmdb_to_imdb(self, tmdb_id, is_movie=True): return self.convert_from_tmdb(tmdb_id, "imdb_id", is_movie) diff --git a/modules/trakt.py b/modules/trakt.py index 31d5f5953..2bbcad27d 100644 --- a/modules/trakt.py +++ b/modules/trakt.py @@ -89,9 +89,9 @@ def convert_id(self, external_id, from_source, to_source, media_type): lookup = Trakt["search"].lookup(external_id, from_source, media_type) if lookup: lookup = lookup[0] if isinstance(lookup, list) else lookup - return lookup.get_key(to_source) - else: - raise Failed("No {} ID found for {} ID {}".format(to_source.upper().replace("B", "b"), from_source.upper().replace("B", "b"), external_id)) + if lookup.get_key(to_source): + return lookup.get_key(to_source) if to_source == "imdb" else int(lookup.get_key(to_source)) + raise Failed("No {} ID found for {} ID {}".format(to_source.upper().replace("B", "b"), from_source.upper().replace("B", "b"), external_id)) @retry(stop_max_attempt_number=6, wait_fixed=10000) def trending(self, amount, is_movie): diff --git a/modules/util.py b/modules/util.py index b707b4c99..f5777b842 100644 --- a/modules/util.py +++ b/modules/util.py @@ -249,7 +249,14 @@ def retry_if_not_failed(exception): "collection_order", "plex_collectionless", "url_poster", "tmdb_poster", "tmdb_profile", "file_poster", "url_background", "file_background", - "name_mapping" + "name_mapping", "label", "label_sync_mode" +] +other_attributes = [ + "schedule", + "sync_mode", + "template", + "test", + "tmdb_person" ] dictionary_lists = [ "filters", @@ -374,18 +381,25 @@ def retry_if_not_failed(exception): "audio_language", "audio_language.not", "country", "country.not", "director", "director.not", - "original_language", "original_language.not", + "original_language", "original_language.not", "subtitle_language", "subtitle_language.not", "video_resolution", "video_resolution.not", "writer", "writer.not" ] +boolean_details = [ + "add_to_arr", + "show_filtered", + "show_missing", + "save_missing" +] all_details = [ "sort_title", "content_rating", "summary", "tmdb_summary", "tmdb_description", "tmdb_biography", "collection_mode", "collection_order", "url_poster", "tmdb_poster", "tmdb_profile", "file_poster", "url_background", "file_background", - "name_mapping", "add_to_arr" + "name_mapping", "add_to_arr", "arr_tag", "label", + "show_filtered", "show_missing", "save_missing" ] discover_movie = [ "language", "with_original_language", "region", "sort_by", @@ -475,10 +489,12 @@ def choose_from_list(datalist, description, data=None, list_type="title", exact= else: return None -def get_list(data): +def get_list(data, lower=False, split=True): if isinstance(data, list): return data elif isinstance(data, dict): return [data] - else: return str(data).split(", ") + elif split is False: return [str(data)] + elif lower is True: return [d.strip().lower() for d in str(data).split(",")] + else: return [d.strip() for d in str(data).split(",")] def get_int_list(data, id_type): values = get_list(data) @@ -570,7 +586,7 @@ def windows_input(prompt, timeout=5): def print_multiline(lines, info=False, warning=False, error=False, critical=False): - for i, line in enumerate(lines.split("\n")): + for i, line in enumerate(str(lines).split("\n")): if critical: logger.critical(line) elif error: logger.error(line) elif warning: logger.warning(line) diff --git a/plex_meta_manager.py b/plex_meta_manager.py index 4e19b5953..e5c9df960 100644 --- a/plex_meta_manager.py +++ b/plex_meta_manager.py @@ -3,10 +3,13 @@ from modules.config import Config parser = argparse.ArgumentParser() -parser.add_argument("--test", dest="test", help=argparse.SUPPRESS, action="store_true", default=False) +parser.add_argument("--mytests", dest="tests", help=argparse.SUPPRESS, action="store_true", default=False) +parser.add_argument("--debug", dest="debug", help=argparse.SUPPRESS, action="store_true", default=False) parser.add_argument("-c", "--config", dest="config", help="Run with desired *.yml file", type=str) parser.add_argument("-t", "--time", dest="time", help="Time to update each day use format HH:MM (Default: 03:00)", default="03:00", type=str) parser.add_argument("-r", "--run", dest="run", help="Run without the scheduler", action="store_true", default=False) +parser.add_argument("-rt", "--test", "--tests", "--run-test", "--run-tests", dest="test", help="Run in debug mode with only collections that have test: true", action="store_true", default=False) +parser.add_argument("-cl", "--collection", "--collections", dest="collections", help="Process only specified collections (comma-separated list)", type=str, default="") parser.add_argument("-d", "--divider", dest="divider", help="Character that divides the sections (Default: '=')", default="=", type=str) parser.add_argument("-w", "--width", dest="width", help="Screen Width (Default: 100)", default=100, type=int) args = parser.parse_args() @@ -41,7 +44,7 @@ def fmt_filter(record): cmd_handler = logging.StreamHandler() cmd_handler.setFormatter(logging.Formatter("| %(message)-100s |")) -cmd_handler.setLevel(logging.INFO) +cmd_handler.setLevel(logging.DEBUG if args.tests or args.test or args.debug else logging.INFO) logger.addHandler(cmd_handler) logger.addHandler(file_handler) @@ -56,30 +59,34 @@ def fmt_filter(record): 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.1.0 ")) +logger.info(util.get_centered_text(" Version: 1.2.0 ")) util.seperator() -if args.test: +if args.tests: tests.run_tests(default_dir) sys.exit(0) -def start(config_path): +def start(config_path, test, daily, collections): + if daily: type = "Daily " + elif test: type = "Test " + elif collections: type = "Collections " + else: type = "" + util.seperator("Starting {}Run".format(type)) try: - util.seperator("Starting Daily Run") config = Config(default_dir, config_path) - config.update_libraries() + config.update_libraries(test, collections) except Exception as e: util.print_stacktrace() logger.critical(e) logger.info("") - util.seperator("Finished Daily Run") + util.seperator("Finished {}Run".format(type)) try: - if args.run: - start(args.config) + if args.run or args.test or args.collections: + start(args.config, args.test, False, args.collections) else: length = 0 - schedule.every().day.at(args.time).do(start, args.config) + schedule.every().day.at(args.time).do(start, args.config, False, True, None) while True: schedule.run_pending() current = datetime.datetime.now().strftime("%H:%M")