Revert "Merge branch 'master' into performance"

This reverts commit 5ed0af4d32, reversing
changes made to 0d269d2e6f.
This commit is contained in:
mza921 2020-12-16 23:43:56 -08:00
parent 5ed0af4d32
commit 7099a20d65
11 changed files with 554 additions and 643 deletions

1
.gitignore vendored
View file

@ -10,3 +10,4 @@ config/config-*
config/*.pickle
config/db/*
**/cache.db*
**/*.cache*

View file

@ -53,7 +53,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- [#50](https://github.com/mza921/Plex-Auto-Collections/issues/50) - Trakt access_token refresh
## [2.4.7] - 2020-11-09 - [#103](https://github.com/mza921/Plex-Auto-Collections/pull/103)
### Fixed
- [#92](https://github.com/mza921/Plex-Auto-Collections/issues/92) - fixed New Plex Movie Agent id lookup behind a proxy
@ -67,8 +66,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- [#93](https://github.com/mza921/Plex-Auto-Collections/issues/93) - actually fixed `max_age`
## [2.4.5] - 2020-11-05 - [#97](https://github.com/mza921/Plex-Auto-Collections/pull/97)
### Changed
- `max_age` no longer takes years

290
README.md
View file

@ -1,6 +1,5 @@
# Plex Auto Collections
##### Version 2.8.0
Plex Auto Collections is a Python 3 script that works off a configuration file to create/update Plex collections. Collection management with this tool can be automated in a varying degree of customizability. Supports IMDB, TMDb, and Trakt lists as well as built in Plex Searches using actors, genres, year, studio and more.
![https://i.imgur.com/iHAYFIZ.png](https://i.imgur.com/iHAYFIZ.png)
@ -19,6 +18,7 @@ Plex Auto Collections is a Python 3 script that works off a configuration file t
- [TMDb Company (List Type)](#tmdb-company-list-type)
- [TMDb Network (List Type)](#tmdb-network-list-type)
- [TMDb Popular (List Type)](#tmdb-popular-list-type)
- [TMDb Trending (List Type)](#tmdb-trending-list-type)
- [TMDb Top Rated (List Type)](#tmdb-top-rated-list-type)
- [TMDb Now Playing (List Type)](#tmdb-now-playing-list-type)
- [TMDb Discover (List Type)](#tmdb-discover-list-type)
@ -163,6 +163,7 @@ The only required attribute for each collection is the list type. There are many
- [TMDb Company](#tmdb-company-list-type)
- [TMDb Network](#tmdb-network-list-type)
- [TMDb Popular](#tmdb-popular-list-type)
- [TMDb Trending](#tmdb-trending-list-type)
- [TMDb Top Rated](#tmdb-top-rated-list-type)
- [TMDb Now Playing](#tmdb-now-playing-list-type)
- [TMDb Discover](#tmdb-discover-list-type)
@ -177,7 +178,13 @@ The only required attribute for each collection is the list type. There are many
- [Tautulli List](#tautulli-list-list-type)
Note that most list types supports multiple lists, with the following exceptions:
- TMDb Popular
- TMDb Trending
- TMDb Top Rated
- TMDb Now Playing
- TMDb Discover
- Trakt Trending Lists
- Trakt Watchlist
- Tautulli Lists
#### Plex Search (List Type)
@ -186,18 +193,19 @@ Note that most list types supports multiple lists, with the following exceptions
You can create a collection based on the Plex search feature using the `plex_search` attribute. The search will return any movie/show that matches at least one term from each search option. You can run multiple searches. The search options are listed below.
##### Search Options
- `actor` (Gets every movie with the specified actor) (Movie libraries only)
- `tmdb_actor` (Gets every movie with the specified actor as well as the added TMDb [metadata](#tmdb-people-list-type)) (Movie libraries only)
- `country` (Gets every movie with the specified country) (Movie libraries only)
- `decade` (Gets every movie from the specified year + the 9 that follow i.e. 1990 will get you 1990-1999) (Movie libraries only)
- `director` (Gets every movie with the specified director) (Movie libraries only)
- `tmdb_director` (Gets every movie with the specified director as well as the added TMDb [metadata](#tmdb-people-list-type)) (Movie libraries only)
- `genre` (Gets every movie/show with the specified genre)
- `studio` (Gets every movie/show with the specified studio)
- `year` (Gets every movie/show with the specified year) (Put a `-` between two years for a range i.e. `year: 1990-1999` or end with `NOW` to go till current i.e. `year: 2000-NOW`)
- `writer` (Gets every movie with the specified writer) (Movie libraries only)
- `tmdb_writer` (Gets every movie with the specified writer as well as the added TMDb [metadata](#tmdb-people-list-type)) (Movie libraries only)
| Search Option | Description | Movie<br>Libraries | Show<br>Libraries |
| :-- | :-- | :--: | :--: |
| `actor` | Gets every movie with the specified actor | :heavy_check_mark: | :x: |
| `tmdb_actor` | Gets every movie with the specified actor as well as the added TMDb [metadata](#tmdb-people-list-type) | :heavy_check_mark: | :x: |
| `country` | Gets every movie with the specified country | :heavy_check_mark: | :x: |
| `decade` | Gets every movie from the specified year + the 9 that follow i.e. 1990 will get you 1990-1999 | :heavy_check_mark: | :x: |
| `director` | Gets every movie with the specified director | :heavy_check_mark: | :x: |
| `tmdb_director` | Gets every movie with the specified director as well as the added TMDb [metadata](#tmdb-people-list-type) | :heavy_check_mark: | :x: |
| `genre` | Gets every movie/show with the specified genre | :heavy_check_mark: | :heavy_check_mark: |
| `studio` | Gets every movie/show with the specified studio | :heavy_check_mark: | :heavy_check_mark: |
| `year` | Gets every movie/show with the specified year (Put a `-` between two years for a range i.e. `year: 1990-1999` or end with `NOW` to go till current i.e. `year: 2000-NOW`) | :heavy_check_mark: | :heavy_check_mark: |
| `writer` | Gets every movie with the specified writer | :heavy_check_mark: | :x: |
| `tmdb_writer` | Gets every movie with the specified writer as well as the added TMDb [metadata](#tmdb-people-list-type) | :heavy_check_mark: | :x: |
Here's some high-level ideas:
@ -436,11 +444,30 @@ You can build a collection using TMDb's most popular movies/shows by using `tmdb
```yaml
collections:
TMDb Trending:
TMDb Popular:
tmdb_popular: 30
sync_mode: sync
```
#### TMDb Trending (List Type)
###### Works with Movie and TV Show Libraries
You can build a collection using TMDb's daily or weekly trending movies/shows by using `tmdb_trending_daily` or `tmdb_trending_weekly`. Both attributes only support a single integer value. The `sync_mode: sync` option is recommended since the lists are continuously updated.
```yaml
collections:
TMDb Daily Trending:
tmdb_trending_daily: 30
sync_mode: sync
```
```yaml
collections:
TMDb Weekly Trending:
tmdb_trending_weekly: 30
sync_mode: sync
```
#### TMDb Top Rated (List Type)
###### Works with Movie and TV Show Libraries
@ -473,63 +500,96 @@ collections:
You can use [TMDb's discover engine](https://www.themoviedb.org/documentation/api/discover) to create a collection based on the search for movies/shows using all different sorts of parameters shown below. The parameters are directly from [TMDb Movie Discover](https://developers.themoviedb.org/3/discover/movie-discover) and [TMDb TV Discover](https://developers.themoviedb.org/3/discover/tv-discover)
##### TMDb Discover Parameters For Movies
- `limit` (Specify how many movies you want returned by the query. Value must be an integer greater then 0. default: 100)
- `language` (Specify a language to query translatable fields with. pattern: `([a-z]{2})-([A-Z]{2})` default: en-US)
- `region` (Specify a [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) to filter release dates. Must be uppercase. pattern: `^[A-Z]{2}$`)
- `sort_by` (Choose from one of the many available sort options. Allowed Values: `popularity.asc`, `popularity.desc`, `release_date.asc`, `release_date.desc`, `revenue.asc`, `revenue.desc`, `primary_release_date.asc`, `primary_release_date.desc`, `original_title.asc`, `original_title.desc`, `vote_average.asc`, `vote_average.desc`, `vote_count.asc`, `vote_count.desc` default: `popularity.desc`)
- `certification_country` (Used in conjunction with the certification parameter, use this to specify a country with a valid certification.)
- `certification` (Filter results with a valid certification from the `certification_country` parameter.)
- `certification.lte` (Filter and only include movies that have a certification that is less than or equal to the specified value.)
- `certification.gte` (Filter and only include movies that have a certification that is greater than or equal to the specified value.)
- `include_adult` (A filter and include or exclude adult movies. Must be `true` or `false`)
- `primary_release_year` (A filter to limit the results to a specific primary release year. Year must be a 4 digit integer i.e. 1990.)
- `primary_release_date.gte` (Filter and only include movies that have a primary release date that is greater or equal to the specified value. Date must be in the MM/DD/YYYY Format.)
- `primary_release_date.lte` (Filter and only include movies that have a primary release date that is less than or equal to the specified value. Date must be in the MM/DD/YYYY Format.)
- `release_date.gte` (Filter and only include movies that have a release date (looking at all release dates) that is greater or equal to the specified value. Date must be in the MM/DD/YYYY Format.)
- `release_date.lte` (Filter and only include movies that have a release date (looking at all release dates) that is less than or equal to the specified value. Date must be in the MM/DD/YYYY Format.)
- `year` (A filter to limit the results to a specific year (looking at all release dates). Year must be a 4 digit integer i.e. 1990.)
- `vote_count.gte` (Filter and only include movies that have a vote count that is greater or equal to the specified value. Value must be an integer greater then 0.)
- `vote_count.lte` (Filter and only include movies that have a vote count that is less than or equal to the specified value. Value must be an integer greater then 0.)
- `vote_average.gte` (Filter and only include movies that have a rating that is greater or equal to the specified value. Value must be a number greater then 0.)
- `vote_average.lte` (Filter and only include movies that have a rating that is less than or equal to the specified value. Value must be an number greater then 0.)
- `with_cast` (A comma separated list of person ID's. Only include movies that have one of the ID's added as an actor.)
- `with_crew` (A comma separated list of person ID's. Only include movies that have one of the ID's added as a crew member.)
- `with_people` (A comma separated list of person ID's. Only include movies that have one of the ID's added as a either a actor or a crew member.)
- `with_companies` (A comma separated list of production company ID's. Only include movies that have one of the ID's added as a production company.)
- `with_genres` (Comma separated value of genre ids that you want to include in the results.)
- `without_genres` (Comma separated value of genre ids that you want to exclude from the results.)
- `with_keywords` (A comma separated list of keyword ID's. Only includes movies that have one of the ID's added as a keyword.)
- `without_keywords` (Exclude items with certain keywords. You can comma and pipe seperate these values to create an 'AND' or 'OR' logic.)
- `with_runtime.gte` (Filter and only include movies that have a runtime that is greater or equal to a value. Value must be an integer greater then 0.)
- `with_runtime.lte` (Filter and only include movies that have a runtime that is less than or equal to a value. Value must be an integer greater then 0.)
- `with_original_language` (Specify an ISO 639-1 string to filter results by their original language value.)
| Type | Description |
| :-- | :-- |
| String | Any number of alphanumeric characters |
| Integer | Any whole number greater then zero i.e. 2, 10, 50 |
| Number | Any number greater then zero i.e. 2.5, 7.4, 9 |
| Boolean | Must be `true` or `false` |
| Date: `MM/DD/YYYY` | Date that fits the specified format |
| Year: `YYYY` | Year must be a 4 digit integer i.e. 1990 |
##### TMDb Discover Parameters For Shows
- `limit` (Specify how many movies you want returned by the query. Value must be an integer greater then 0. default: 100)
- `language` (Specify a language to query translatable fields with. pattern: `([a-z]{2})-([A-Z]{2})` default: en-US)
- `sort_by` (Choose from one of the many available sort options. Allowed Values: `vote_average.desc`, `vote_average.asc`, `first_air_date.desc`, `first_air_date.asc`, `popularity.desc`, `popularity.asc` default: `popularity.desc`)
- `air_date.gte` (Filter and only include TV shows that have a air date (by looking at all episodes) that is greater or equal to the specified value. Date must be in the MM/DD/YYYY Format.)
- `air_date.lte` (Filter and only include TV shows that have a air date (by looking at all episodes) that is less than or equal to the specified value. Date must be in the MM/DD/YYYY Format.)
- `first_air_date.gte` (Filter and only include TV shows that have a original air date that is greater or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date. Date must be in the MM/DD/YYYY Format.)
- `first_air_date.lte` (Filter and only include TV shows that have a original air date that is less than or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date. Date must be in the MM/DD/YYYY Format.)
- `first_air_date_year` (Filter and only include TV shows that have a original air date year that equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date. Year must be a 4 digit integer i.e. 1990.)
- `include_null_first_air_dates` (Use this filter to include TV shows that don't have an air date while using any of the `first_air_date` filters. Must be `true` or `false`.)
"- `timezone` (Used in conjunction with the `air_date.gte/lte` filter to calculate the proper UTC offset. default: America/New_York)
- `vote_count.gte` (Filter and only include TV that have a vote count that is greater or equal to the specified value. Value must be an integer greater then 0.)
- `vote_count.lte` (Filter and only include TV that have a vote count that is less than or equal to the specified value. Value must be an integer greater then 0.)
- `vote_average.gte` (Filter and only include TV that have a rating that is greater or equal to the specified value. Value must be a number greater then 0.)
- `vote_average.lte` (Filter and only include TV that have a rating that is less than or equal to the specified value. Value must be an number greater then 0.)
- `with_networks` (Comma separated value of network ids that you want to include in the results.)
- `with_companies` (A comma separated list of production company ID's. Only include movies that have one of the ID's added as a production company.)
- `with_genres` (Comma separated value of genre ids that you want to include in the results.)
- `without_genres` (Comma separated value of genre ids that you want to exclude from the results.)
- `with_keywords` (A comma separated list of keyword ID's. Only includes TV shows that have one of the ID's added as a keyword.)
- `without_keywords` (Exclude items with certain keywords. You can comma and pipe seperate these values to create an 'AND' or 'OR' logic.)
- `with_runtime.gte` (Filter and only include TV shows with an episode runtime that is greater than or equal to a value.)
- `with_runtime.lte` (Filter and only include TV shows with an episode runtime that is less than or equal to a value.)
- `with_original_language` (Specify an ISO 639-1 string to filter results by their original language value.)
- `screened_theatrically` (Filter results to include items that have been screened theatrically. Must be `true` or `false`.)
### Discover Movies
| Movie Parameters | Description | Type |
| :-- | :-- | :--: |
| `limit` | Specify how many movies you want returned by the query. (default: 100) | Integer |
| `language` | Specify a language to query translatable fields with. (default: en-US) | `([a-z]{2})-([A-Z]{2})` |
| `region` | Specify a [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) to filter release dates. Must be uppercase. | `^[A-Z]{2}$` |
| `sort_by` | Choose from one of the many available sort options. (default: `popularity.desc`) | See [sort options](#sort-options) below |
| `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification. | String |
| `certification` | Filter results with a valid certification from the `certification_country` parameter. | String |
| `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value. | String |
| `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value. | String |
| `include_adult` | A filter and include or exclude adult movies. | Boolean |
| `primary_release_year` | A filter to limit the results to a specific primary release year. | Year: YYYY |
| `primary_release_date.gte` | Filter and only include movies that have a primary release date that is greater or equal to the specified value. | Date: `MM/DD/YYYY` |
| `primary_release_date.lte` | Filter and only include movies that have a primary release date that is less than or equal to the specified value. | Date: `MM/DD/YYYY` |
| `release_date.gte` | Filter and only include movies that have a release date (looking at all release dates) that is greater or equal to the specified value. | Date: `MM/DD/YYYY` |
| `release_date.lte` | Filter and only include movies that have a release date (looking at all release dates) that is less than or equal to the specified value. | Date: `MM/DD/YYYY` |
| `year` | A filter to limit the results to a specific year (looking at all release dates). | Year: YYYY |
| `vote_count.gte` | Filter and only include movies that have a vote count that is greater or equal to the specified value. | Integer |
| `vote_count.lte` | Filter and only include movies that have a vote count that is less than or equal to the specified value. | Integer |
| `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value. | Number |
| `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value. | Number |
| `with_cast` | A comma separated list of person ID's. Only include movies that have one of the ID's added as an actor. | String |
| `with_crew` | A comma separated list of person ID's. Only include movies that have one of the ID's added as a crew member. | String |
| `with_people` | A comma separated list of person ID's. Only include movies that have one of the ID's added as a either a actor or a crew member. | String |
| `with_companies` | A comma separated list of production company ID's. Only include movies that have one of the ID's added as a production company. | String |
| `with_genres` | Comma separated value of genre ids that you want to include in the results. | String |
| `without_genres` | Comma separated value of genre ids that you want to exclude from the results. | String |
| `with_keywords` | A comma separated list of keyword ID's. Only includes movies that have one of the ID's added as a keyword. | String |
| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic. | String |
| `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value. | Integer |
| `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value. | Integer |
| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value. | String |
### Discover Shows
| Show Parameters | Description | Type |
| :-- | :-- | :--: |
| `limit` | Specify how many movies you want returned by the query. (default: 100) | Integer |
| `language` | Specify a language to query translatable fields with. (default: en-US) | `([a-z]{2})-([A-Z]{2})` |
| `sort_by` | Choose from one of the many available sort options. (default: `popularity.desc`) | See [sort options](#sort-options) below |
| `air_date.gte` | Filter and only include TV shows that have a air date (by looking at all episodes) that is greater or equal to the specified value. | Date: `MM/DD/YYYY` |
| `air_date.lte` | Filter and only include TV shows that have a air date (by looking at all episodes) that is less than or equal to the specified value. | Date: `MM/DD/YYYY` |
| `first_air_date.gte` | Filter and only include TV shows that have a original air date that is greater or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date. | Date: `MM/DD/YYYY` |
| `first_air_date.lte` | Filter and only include TV shows that have a original air date that is less than or equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date. | Date: `MM/DD/YYYY` |
| `first_air_date_year` | Filter and only include TV shows that have a original air date year that equal to the specified value. Can be used in conjunction with the `include_null_first_air_dates` filter if you want to include items with no air date. | Year: YYYY |
| `include_null_first_air_dates` | Use this filter to include TV shows that don't have an air date while using any of the `first_air_date` filters. | Boolean |
| `timezone` | Used in conjunction with the `air_date.gte/lte` filter to calculate the proper UTC offset. (default: America/New_York) | String |
| `vote_count.gte` | Filter and only include TV that have a vote count that is greater or equal to the specified value. | Integer |
| `vote_count.lte` | Filter and only include TV that have a vote count that is less than or equal to the specified value. | Integer |
| `vote_average.gte` | Filter and only include TV that have a rating that is greater or equal to the specified value. | Number |
| `vote_average.lte` | Filter and only include TV that have a rating that is less than or equal to the specified value. | Number |
| `with_networks` | Comma separated value of network ids that you want to include in the results. | String |
| `with_companies` | A comma separated list of production company ID's. Only include movies that have one of the ID's added as a production company. | String |
| `with_genres` | Comma separated value of genre ids that you want to include in the results. | String |
| `without_genres` | Comma separated value of genre ids that you want to exclude from the results. | String |
| `with_keywords` | A comma separated list of keyword ID's. Only includes TV shows that have one of the ID's added as a keyword. | String |
| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic. | String |
| `with_runtime.gte` | Filter and only include TV shows with an episode runtime that is greater than or equal to a value. | Integer |
| `with_runtime.lte` | Filter and only include TV shows with an episode runtime that is less than or equal to a value. | Integer |
| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value. | String |
| `screened_theatrically` | Filter results to include items that have been screened theatrically. | Boolean |
### Sort Options
| Sort Option | Movie Sort | Show Sort |
| :-- | :--: | :--: |
| `popularity.asc` | :heavy_check_mark: | :heavy_check_mark: |
| `popularity.desc` | :heavy_check_mark: | :heavy_check_mark: |
| `original_title.asc` | :heavy_check_mark: | :x: |
| `original_title.desc` | :heavy_check_mark: | :x: |
| `revenue.asc` | :heavy_check_mark: | :x: |
| `revenue.desc` | :heavy_check_mark: | :x: |
| `release_date.asc` | :heavy_check_mark: | :x: |
| `release_date.desc` | :heavy_check_mark: | :x: |
| `primary_release_date.asc` | :heavy_check_mark: | :x: |
| `primary_release_date.desc` | :heavy_check_mark: | :x: |
| `first_air_date.asc` | :x: | :heavy_check_mark: |
| `first_air_date.desc` | :x: | :heavy_check_mark: |
| `vote_average.asc` | :heavy_check_mark: | :heavy_check_mark: |
| `vote_average.desc` | :heavy_check_mark: | :heavy_check_mark: |
| `vote_count.asc` | :heavy_check_mark: | :x: |
| `vote_count.desc` | :heavy_check_mark: | :x: |
```yaml
collections:
@ -757,7 +817,7 @@ This script can pull items from a Trakt user's Watchlist for [Movies](https://tr
```yaml
collections:
Trakt Watchlist:
trakt_watchlist:
trakt_watchlist:
- me
- friendontrakt
sync_mode: sync
@ -769,10 +829,12 @@ collections:
Tautulli has watch analytics that can show the most watched or most popular Movies/Shows in your Library. This script can easily leverage that data into making and sync collection based on those lists using the `tautulli` attribute. Unlike other lists this one has subattribute options:
- `list_type`: watched (For Most Watched Lists) or popular (For Most Popular Lists) (Required)
- `list_days`: Number of Days to look back of the list (Optional Defaults to 30)
- `list_size`: Number of Movies/Shows to add to this list (Optional Defaults to 10)
- `list_buffer`: Number of extra Movies/Shows to grab in case you have multiple show/movie Libraries (Optional Defaults to 10)
| Attribute | Description | Required | Default |
| :-- | :-- | :--: | :--: |
| `list_type` | `watched` (For Most Watched Lists)<br>`popular` (For Most Popular Lists) | :heavy_check_mark: | :x: |
| `list_days` | Number of Days to look back of the list | :x: | 30 |
| `list_size` | Number of Movies/Shows to add to this list | :x: | 10 |
| `list_buffer` | Number of extra Movies/Shows to grab in case you have multiple show/movie Libraries. | :x: | 10 |
```yaml
collections:
@ -804,29 +866,34 @@ Note that if you have multiple movie Libraries or multiple show Libraries Tautul
###### Works with Movie and TV Show Libraries
The next optional attribute for any collection is the `filters` attribute. Collection filters allows for you to filter every movie/show added to the collection from every List Type. All collection filter options are listed below.
In addition you can also use the `.not` at the end of any standard collection filter to do an inverse search matching everything that doesn't have the value specified. You can use `all: true` to start you filter from your entire library.
In addition you can also use the `.not` at the end of any standard collection filter to do an inverse search matching everything that doesn't have the value specified. You can use `all: true` to start your filter from your entire library.
##### Standard Collection Filters Options
- `actor` (Matches every movie/show with the specified actor)
- `content_rating` (Matches every movie/show with the specified content rating)
- `country` (Matches every movie with the specified country) (Movie libraries only)
- `director` (Matches every movie with the specified director) (Movie libraries only)
- `genre` (Matches every movie/show with the specified genre)
- `studio` (Matches every movie/show with the specified studio)
- `year` (Matches every movie/show with the specified year)
- `writer` (Matches every movie with the specified writer) (Movie libraries only)
- `video_resolution` (Matches every movie with the specified video resolution) (Movie libraries only)
- `audio_language` (Matches every movie with the specified audio language) (Movie libraries only)
- `subtitle_language` (Matches every movie with the specified subtitle language) (Movie libraries only)
| Standard Filters | Description | Movie<br>Libraries | Show<br>Libraries |
| :-- | :-- | :--: | :--: |
| `actor` | Matches every movie/show with the specified actor | :heavy_check_mark: | :heavy_check_mark: |
| `content_rating` | Matches every movie/show with the specified content rating | :heavy_check_mark: | :heavy_check_mark: |
| `country` | Matches every movie with the specified country | :heavy_check_mark: | :x: |
| `director` | Matches every movie with the specified director | :heavy_check_mark: | :x: |
| `genre` | Matches every movie/show with the specified genre | :heavy_check_mark: | :heavy_check_mark: |
| `studio` | Matches every movie/show with the specified studio | :heavy_check_mark: | :heavy_check_mark: |
| `year` | Matches every movie/show with the specified year | :heavy_check_mark: | :heavy_check_mark: |
| `writer` | Matches every movie with the specified writer | :heavy_check_mark: | :x: |
| `video_resolution` | Matches every movie with the specified video resolution | :heavy_check_mark: | :x: |
| `audio_language` | Matches every movie with the specified audio language | :heavy_check_mark: | :x: |
| `subtitle_language` | Matches every movie with the specified subtitle language | :heavy_check_mark: | :x: |
| `plex_collection` | Matches every movie/show with the specified plex collection | :heavy_check_mark: | :heavy_check_mark: |
##### Special Collection Filters Options (These options can only take one value each)
- `max_age` (Matches any movie/show whose Originally Available date is within the last specified number of days)
- `year.gte` (Matches any movie/show whose year is greater then or equal to the specified year)
- `year.lte` (Matches any movie/show whose year is less then or equal to the specified year)
- `rating.gte` (Matches any movie/show whose rating is greater then or equal to the specified rating)
- `rating.lte` (Matches any movie/show whose rating is less then or equal to the specified rating)
- `originally_available.gte` (Matches any movie/show whose originally_available date is greater then or equal to the specified originally_available date) (Date must be in the MM/DD/YYYY Format)
- `originally_available.lte` (Matches any movie/show whose originally_available date is less then or equal to the specified originally_available date) (Date must be in the MM/DD/YYYY Format)
| Advanced Filters | Description | Movie<br>Libraries | Show<br>Libraries |
| :-- | :-- | :--: | :--: |
| `max_age` | Matches any movie/show whose Originally Available date is within the last specified number of days | :heavy_check_mark: | :heavy_check_mark: |
| `year.gte` | Matches any movie/show whose year is greater then or equal to the specified year | :heavy_check_mark: | :heavy_check_mark: |
| `year.lte` | Matches any movie/show whose year is less then or equal to the specified year | :heavy_check_mark: | :heavy_check_mark: |
| `rating.gte` | Matches any movie/show whose rating is greater then or equal to the specified rating | :heavy_check_mark: | :heavy_check_mark: |
| `rating.lte` | Matches any movie/show whose rating is less then or equal to the specified rating | :heavy_check_mark: | :heavy_check_mark: |
| `originally_available.gte` | Matches any movie/show whose originally_available date is greater then or equal to the specified originally_available date (Date must be in the MM/DD/YYYY Format) | :heavy_check_mark: | :heavy_check_mark: |
| `originally_available.lte` | Matches any movie/show whose originally_available date is less then or equal to the specified originally_available date (Date must be in the MM/DD/YYYY Format) | :heavy_check_mark: | :heavy_check_mark: |
Note only standard filters can take multiple values
```yaml
collections:
@ -895,9 +962,10 @@ Note that multiple collection filters are supported but a movie must match at le
### Sync Mode (Collection Attribute)
You can specify how collections sync using `sync_mode`. Set it to `append` to only add movies/shows to the collection or set it to `sync` to add movies/shows to the collection and remove movies/shows from a collection.
##### Options
- `append` (Only Add Items to the Collection)
- `sync` (Add & Remove Items from the Collection)
| Sync Options | Description |
| :-- | :-- |
| `append` | Only Add Items to the Collection |
| `sync` | Add & Remove Items from the Collection |
```yaml
collections:
@ -979,11 +1047,12 @@ collections:
Plex allows for four different types of collection modes: library default, hide items in this collection, show this collection and its items, and hide collection (more details can be found in [Plex's Collection support article](https://support.plex.tv/articles/201273953-collections/#toc-2)). These options can be set with `default`, `hide_items`, `show_items`, and `hide`.
##### Options
- `default` (Library default)
- `hide` (Hide Collection)
- `hide_items` (Hide Items in this Collection)
- `show_items` (Show this Collection and its Items)
| Collection Mode Options | Description |
| :-- | :-- |
| `default` | Library default |
| `hide` | Hide Collection |
| `hide_items` | Hide Items in this Collection |
| `show_items` | Show this Collection and its Items |
```yaml
collections:
@ -999,9 +1068,10 @@ collections:
Lastly, Plex allows collections to be sorted by the media's release dates or alphabetically by title. These options can be set with `release` or `alpha`. Plex defaults all collections to `release`, but `alpha` can be helpful for rearranging collections. For example, with collections where the chronology does not follow the release dates, you could create custom sort titles for each media item and then sort the collection alphabetically.
##### Options
- `release` (Order Collection by Release Dates)
- `alpha` (Order Collection Alphabetically)
| Collection Sort Options | Description |
| :-- | :-- |
| `release` | Order Collection by Release Dates |
| `alpha` | Order Collection Alphabetically |
```yaml
collections:
@ -1221,7 +1291,7 @@ radarr: # Opt
token: ##### # Req - User's Radarr API key
quality_profile_id: 4 # Req - See below
root_folder_path: /mnt/movies # Req - See below
add_movie: false # Opt - Add missing movies to Radarr
add_to_radarr: false # Opt - Add missing movies to Radarr
search_movie: false # Opt - Search while adding missing movies
```
@ -1244,9 +1314,7 @@ If you were to add two more profiles, the `id` would be as follows:
In this example, to set any added movies to the `Ultra-HD` profile, set `quality_profile_id` to `5`. To set any added movies to `HD-1080p`, set `quality_profile_id` to `4`.
The `add_movie` key allows missing to movies to be added to Radarr. If this key is missing, the script will prompt the user to add missing movies or not. If you'd like to add movies but not had Radarr search, then set `search_movie` to `false`.
Note that Radarr support has not been tested with extensively Trakt lists and Sonarr support has not yet been implemented.
The `add_to_radarr` key allows missing to movies to be added to Radarr. If this key is missing, the script will prompt the user to add missing movies or not. If you'd like to add movies but not had Radarr search, then set `search_movie` to `false`. If you want to override this attribute per collection you can add the `add_to_radarr` attribute under a collection and set it to true or false to override any global choice.
# Acknowledgements
- [vladimir-tutin](https://github.com/vladimir-tutin) for writing substantially all of the code in this fork

View file

@ -4,6 +4,7 @@ import sys
import yaml
import ruamel.yaml
import requests
from yaml.scanner import ScannerError
from urllib.parse import urlparse
from tmdbv3api import Collection
from plexapi.exceptions import Unauthorized
@ -14,7 +15,6 @@ from plexapi.library import MovieSection
from plexapi.library import ShowSection
from trakt import Trakt
import trakt_helpers
import trakt
def check_for_attribute(config, attribute, parent=None, test_list=None, options="", default=None, do_print=True, default_is_none=False, var_type="str", throw=False, save=True):
@ -76,8 +76,11 @@ class Config:
Config.headless = headless
Config.config_path = config_path
self.config_path = config_path
with open(self.config_path, 'rt', encoding='utf-8') as yml:
self.data = yaml.load(yml, Loader=yaml.FullLoader)
try:
with open(self.config_path, 'rt', encoding='utf-8') as yml:
self.data = yaml.load(yml, Loader=yaml.FullLoader)
except ScannerError as e:
sys.exit("| Scan Error: {}".format(str(e).replace('\n', '\n|\t ')))
if Config.valid == True:
self.collections = check_for_attribute(self.data, "collections", default={}, do_print=False)
self.plex = self.data['plex']
@ -188,7 +191,7 @@ class Radarr:
self.token = check_for_attribute(config, "token", parent="radarr")
self.quality_profile_id = check_for_attribute(config, "quality_profile_id", parent="radarr", var_type="int")
self.root_folder_path = check_for_attribute(config, "root_folder_path", parent="radarr")
self.add_movie = check_for_attribute(config, "add_movie", parent="radarr", var_type="bool", default_is_none=True, do_print=False)
self.add_to_radarr = check_for_attribute(config, "add_to_radarr", parent="radarr", var_type="bool", default_is_none=True, do_print=False)
self.search_movie = check_for_attribute(config, "search_movie", parent="radarr", var_type="bool", default=False, do_print=False)
elif Radarr.valid is None:
if TMDB.valid:
@ -216,9 +219,15 @@ class Radarr:
except SystemExit as e:
fatal_message = fatal_message + "\n" + str(e) if len(fatal_message) > 0 else str(e)
try:
self.add_movie = check_for_attribute(config, "add_movie", parent="radarr", options="| \ttrue (Add missing movies to Radarr)\n| \tfalse (Do not add missing movies to Radarr)", var_type="bool", default_is_none=True, throw=True)
self.add_to_radarr = check_for_attribute(config, "add_to_radarr", parent="radarr", options="| \ttrue (Add missing movies to Radarr)\n| \tfalse (Do not add missing movies to Radarr)", var_type="bool", default_is_none=True, throw=True)
except SystemExit as e:
message = message + "\n" + str(e) if len(message) > 0 else str(e)
if "add_movie" in config:
try:
self.add_to_radarr = check_for_attribute(config, "add_movie", parent="radarr", var_type="bool", throw=True, save=False)
print("| Config Warning: replace add_movie with add_to_radarr")
except SystemExit as e:
pass
try:
self.search_movie = check_for_attribute(config, "search_movie", parent="radarr", options="| \ttrue (Have Radarr seach the added movies)\n| \tfalse (Do not have Radarr seach the added movies)", var_type="bool", default=False, throw=True)
except SystemExit as e:
@ -314,7 +323,7 @@ class Tautulli:
except:
print("| Config Error: Invalid url")
Tautulli.valid = False
print("| tautulli connection {}".format("scuccessful" if Tautulli.valid else "failed"))
print("| tautulli Connection {}".format("Successful" if Tautulli.valid else "failed"))
class TraktClient:
@ -362,7 +371,7 @@ class TraktClient:
else:
self.refreshed_authorization = None
if trakt_helpers.check_trakt(self.refreshed_authorization):
# Save the refreshed authorization
# Save the refreshed authorization
trakt_helpers.save_authorization(Config(config_path).config_path, self.refreshed_authorization)
TraktClient.valid = True
else:

View file

@ -3,27 +3,23 @@ import requests
import math
import sys
import os
from urllib.parse import urlparse
from lxml import html
from tmdbv3api import TMDb
from tmdbv3api import Movie
from tmdbv3api import List
from tmdbv3api import TV
from tmdbv3api import Discover
from tmdbv3api import Collection
from tmdbv3api import Company
#from tmdbv3api import Network #TURNON:Trending
from tmdbv3api import Person
#from tmdbv3api import Trending #TURNON:Trending
import config_tools
import plex_tools
import trakt
def adjust_space(old_length, display_title):
space_length = old_length - len(display_title)
if space_length > 0:
display_title += " " * space_length
return display_title
try:
from urllib.parse import urlparse
from lxml import html
from tmdbv3api import TMDb
from tmdbv3api import Movie
from tmdbv3api import List
from tmdbv3api import TV
from tmdbv3api import Discover
from tmdbv3api import Collection
from tmdbv3api import Company
from tmdbv3api import Network
from tmdbv3api import Person
from tmdbv3api import Trending
except ImportError:
print('|\n| Requirements Error: Please update requirements using "pip install -r requirements.txt"\n|')
sys.exit(0)
def imdb_get_ids(plex, imdb_url):
imdb_url = imdb_url.strip()
@ -70,11 +66,7 @@ def imdb_get_ids(plex, imdb_url):
print("| Config Error {} must begin with either:\n| https://www.imdb.com/list/ls (For Lists)\n| https://www.imdb.com/search/title/? (For Searches)")
return None
def imdb_get_movies(config_path, plex, data):
title_ids = data[1]
print("| {} Movies found on IMDb".format(len(title_ids)))
tmdb = TMDb()
tmdb.api_key = config_tools.TMDB(config_path).apikey
def tmdb_get_imdb(config_path, tmdb_id):
movie = Movie()
movie.api_key = config_tools.TMDB(config_path).apikey
return str(movie.external_ids(tmdb_id)['imdb_id'])
@ -116,15 +108,14 @@ def imdb_get_movies(config_path, plex, plex_map, data):
matched = []
missing = []
for imdb_id in title_ids:
movie = imdb_map.pop(imdb_id, None)
if movie:
matched_imdb_movies.append(plex.Server.fetchItem(movie.ratingKey))
tmdb_id = imdb_get_tmdb(config_path, imdb_id)
if tmdb_id in plex_map:
matched.append(plex.Server.fetchItem(plex_map[tmdb_id]))
else:
missing_imdb_movies.append(imdb_id)
missing.append(tmdb_id)
return matched, missing
return matched_imdb_movies, missing_imdb_movies
def tmdb_get_movies(config_path, plex, data, method):
def tmdb_get_movies(config_path, plex, plex_map, data, method):
t_movs = []
t_movie = Movie()
t_movie.api_key = config_tools.TMDB(config_path).apikey # Set TMDb api key for Movie
@ -133,19 +124,20 @@ def tmdb_get_movies(config_path, plex, data, method):
count = 0
if method == "tmdb_discover":
attrs = data.copy()
discover = Discover()
discover.api_key = t_movie.api_key
discover.discover_movies(data)
discover.discover_movies(attrs)
total_pages = int(os.environ["total_pages"])
total_results = int(os.environ["total_results"])
limit = int(data.pop('limit'))
amount = total_results if total_results < limit else limit
print("| Processing {}: {} items".format(method, amount))
for attr, value in data.items():
limit = int(attrs.pop('limit'))
amount = total_results if limit == 0 or total_results < limit else limit
print("| Processing {}: {} movies".format(method, amount))
for attr, value in attrs.items():
print("| {}: {}".format(attr, value))
for x in range(total_pages):
data["page"] = x + 1
tmdb_movies = discover.discover_movies(data)
attrs["page"] = x + 1
tmdb_movies = discover.discover_movies(attrs)
for tmovie in tmdb_movies:
count += 1
t_movs.append(tmovie.id)
@ -163,10 +155,10 @@ def tmdb_get_movies(config_path, plex, data, method):
tmdb_movies = t_movie.top_rated(x + 1)
elif method == "tmdb_now_playing":
tmdb_movies = t_movie.now_playing(x + 1)
#elif method == "tmdb_trending_daily": #TURNON:Trending
# tmdb_movies = trending.movie_day(x + 1) #TURNON:Trending
#elif method == "tmdb_trending_weekly": #TURNON:Trending
# tmdb_movies = trending.movie_week(x + 1) #TURNON:Trending
elif method == "tmdb_trending_daily":
tmdb_movies = trending.movie_day(x + 1)
elif method == "tmdb_trending_weekly":
tmdb_movies = trending.movie_week(x + 1)
for tmovie in tmdb_movies:
count += 1
t_movs.append(tmovie.id)
@ -212,23 +204,6 @@ def tmdb_get_movies(config_path, plex, data, method):
raise ValueError("| Config Error: TMDb ID: {} not found".format(tmdb_id))
print("| Processing {}: ({}) {}".format(method, tmdb_id, tmdb_name))
# Create dictionary of movies and their guid
# GUIDs reference from which source Plex has pulled the metadata
p_m_map = {}
p_movies = plex.Library.all()
for m in p_movies:
guid = m.guid
if "themoviedb://" in guid:
guid = guid.split('themoviedb://')[1].split('?')[0]
elif "imdb://" in guid:
guid = guid.split('imdb://')[1].split('?')[0]
elif "plex://" in guid:
guid = guid.split('plex://')[1].split('?')[0]
else:
guid = "None"
p_m_map[m] = guid
matched = []
missing = []
for mid in t_movs:
@ -236,8 +211,7 @@ def tmdb_get_movies(config_path, plex, data, method):
if mid in plex_map:
matched.append(plex.Server.fetchItem(plex_map[mid]))
else:
# Duplicate TMDb call?
missing.append(t_movie.details(mid).imdb_id)
missing.append(mid)
return matched, missing
@ -274,38 +248,43 @@ def get_tautulli(config_path, plex, data):
return matched, missing
def get_tvdb_id_from_tmdb_id(id):
lookup = trakt.Trakt['search'].lookup(id, 'tmdb', 'show')
if lookup:
lookup = lookup[0] if isinstance(lookup, list) else lookup
return lookup.get_key('tvdb')
else:
return None
def tmdb_get_shows(config_path, plex, data, method):
def tmdb_get_shows(config_path, plex, plex_map, data, method):
config_tools.TraktClient(config_path)
t_tvs = []
t_tv = TV()
t_tv.api_key = config_tools.TMDB(config_path).apikey # Set TMDb api key for Movie
t_tv.api_key = config_tools.TMDB(config_path).apikey
if t_tv.api_key == "None":
raise KeyError("Invalid TMDb API Key")
discover = Discover()
discover.api_key = t_tv.api_key
count = 0
if method == "tmdb_discover":
discover.discover_tv_shows(data)
if method in ["tmdb_discover", "tmdb_company", "tmdb_network"]:
if method in ["tmdb_company", "tmdb_network"]:
tmdb = Company() if method == "tmdb_company" else Network()
tmdb.api_key = t_tv.api_key
tmdb_id = int(data)
tmdb_name = str(tmdb.details(tmdb_id))
discover_method = "with_companies" if method == "tmdb_company" else "with_networks"
attrs = {discover_method: tmdb_id}
limit = 0
else:
attrs = data.copy()
limit = int(attrs.pop('limit'))
discover = Discover()
discover.api_key = t_tv.api_key
discover.discover_tv_shows(attrs)
total_pages = int(os.environ["total_pages"])
total_results = int(os.environ["total_results"])
limit = int(data.pop('limit'))
amount = total_results if total_results < limit else limit
print("| Processing {}: {} items".format(method, amount))
for attr, value in data.items():
print("| {}: {}".format(attr, value))
amount = total_results if limit == 0 or total_results < limit else limit
if method in ["tmdb_company", "tmdb_network"]:
print("| Processing {}: {} ({} {} shows)".format(method, tmdb_id, amount, tmdb_name))
else:
print("| Processing {}: {} shows".format(method, amount))
for attr, value in attrs.items():
print("| {}: {}".format(attr, value))
for x in range(total_pages):
data["page"] = x + 1
tmdb_shows = discover.discover_tv_shows(data)
attrs["page"] = x + 1
tmdb_shows = discover.discover_tv_shows(attrs)
for tshow in tmdb_shows:
count += 1
t_tvs.append(tshow.id)
@ -313,7 +292,6 @@ def tmdb_get_shows(config_path, plex, data, method):
break
if count == amount:
break
run_discover(data)
elif method in ["tmdb_popular", "tmdb_top_rated", "tmdb_trending_daily", "tmdb_trending_weekly"]:
trending = Trending()
trending.api_key = t_movie.api_key
@ -322,10 +300,10 @@ def tmdb_get_shows(config_path, plex, data, method):
tmdb_shows = t_tv.popular(x + 1)
elif method == "tmdb_top_rated":
tmdb_shows = t_tv.top_rated(x + 1)
#elif method == "tmdb_trending_daily": #TURNON:Trending
# tmdb_shows = trending.tv_day(x + 1) #TURNON:Trending
#elif method == "tmdb_trending_weekly": #TURNON:Trending
# tmdb_shows = trending.tv_week(x + 1) #TURNON:Trending
elif method == "tmdb_trending_daily":
tmdb_shows = trending.tv_day(x + 1)
elif method == "tmdb_trending_weekly":
tmdb_shows = trending.tv_week(x + 1)
for tshow in tmdb_shows:
count += 1
t_tvs.append(tshow.id)
@ -347,19 +325,6 @@ def tmdb_get_shows(config_path, plex, data, method):
t_tvs.append(ttv.id)
except:
raise ValueError("| Config Error: TMDb List: {} not found".format(tmdb_id))
elif method in ["tmdb_company", "tmdb_network"]:
if method == "tmdb_company":
tmdb = Company()
tmdb.api_key = t_tv.api_key
tmdb_name = str(tmdb.details(tmdb_id))
else:
#tmdb = Network() #TURNON:Trending
#tmdb.api_key = t_tv.api_key #TURNON:Trending
tmdb_name = ""#str(tmdb.details(tmdb_id)) #TURNON:Trending
discover_method = "with_companies" if method == "tmdb_company" else "with_networks"
tmdb_shows = discover.discover_tv_shows({discover_method: tmdb_id})
for tshow in tmdb_shows:
t_tvs.append(tshow.id)
else:
try:
t_tv.details(tmdb_id).number_of_seasons
@ -369,33 +334,14 @@ def tmdb_get_shows(config_path, plex, data, method):
raise ValueError("| Config Error: TMDb ID: {} not found".format(tmdb_id))
print("| Processing {}: ({}) {}".format(method, tmdb_id, tmdb_name))
p_tv_map = {}
for item in plex.Library.all():
guid = urlparse(item.guid)
item_type = guid.scheme.split('.')[-1]
if item_type == 'thetvdb':
tvdb_id = guid.netloc
elif item_type == 'themoviedb':
tvdb_id = get_tvdb_id_from_tmdb_id(guid.netloc)
else:
tvdb_id = None
p_tv_map[item] = tvdb_id
matched = []
missing = []
for mid in t_tvs:
match = False
tvdb_id = get_tvdb_id_from_tmdb_id(mid)
tvdb_id = tmdb_get_tvdb(config_path, mid)
if tvdb_id is None:
print("| Trakt Error: tmbd_id: {} could not converted to tvdb_id try just using tvdb_id instead".format(mid))
else:
for t in p_tv_map:
if p_tv_map[t] and "tt" not in p_tv_map[t] != "None":
if p_tv_map[t] is not None and int(p_tv_map[t]) == int(tvdb_id):
match = True
break
if match:
matched.append(t)
print("| TMDb Error: tmdb_id: {} ({}) has no associated tvdb_id try just using tvdb_id instead".format(mid, t_tv.details(mid).name))
elif tvdb_id in plex_map:
matched.append(plex.Server.fetchItem(plex_map[tvdb_id]))
else:
missing.append(tvdb_id)
@ -405,64 +351,64 @@ def tvdb_get_shows(config_path, plex, plex_map, data):
tvdb_id = str(data)
matched = []
missing = []
match = False
for t in p_tv_map:
if p_tv_map[t] and "tt" not in p_tv_map[t] != "None":
if p_tv_map[t] is not None and int(p_tv_map[t]) == int(id):
match = True
break
if match:
matched.append(t)
if tvdb_id in plex_map:
matched.append(plex.Server.fetchItem(plex_map[tvdb_id]))
else:
missing.append(id)
missing.append(tvdb_id)
return matched, missing
def tmdb_get_metadata(config_path, data, type):
# Instantiate TMDB objects
id = int(data)
tmdb_id = int(data)
tmdb_url_prefix = "https://image.tmdb.org/t/p/original"
api_key = config_tools.TMDB(config_path).apikey
language = config_tools.TMDB(config_path).language
is_movie = config_tools.Plex(config_path).library_type == "movie"
if type in ["overview", "poster_path", "backdrop_path"]:
if type in ["overview", "poster", "backdrop"]:
collection = Collection()
collection.api_key = api_key
collection.language = language
try:
if type == "overview":
return collection.details(id).overview
elif type == "poster_path":
return tmdb_url_prefix + collection.details(id).poster_path
elif type == "backdrop_path":
return tmdb_url_prefix + collection.details(id).backdrop_path
meta = collection.details(tmdb_id).overview
elif type == "poster":
meta = collection.details(tmdb_id).poster_path
elif type == "backdrop":
meta = collection.details(tmdb_id).backdrop_path
except AttributeError:
media = Movie() if is_movie else TV()
media.api_key = api_key
media.language = language
try:
if type == "overview":
return media.details(id).overview
elif type == "poster_path":
return tmdb_url_prefix + media.details(id).poster_path
elif type == "backdrop_path":
return tmdb_url_prefix + media.details(id).backdrop_path
meta = media.details(tmdb_id).overview
elif type == "poster":
meta = media.details(tmdb_id).poster_path
elif type == "backdrop":
meta = media.details(tmdb_id).backdrop_path
except AttributeError:
raise ValueError("| Config Error: TMBd {} ID: {} not found".format("Movie/Collection" if is_movie else "Show", id))
elif type in ["biography", "profile_path", "name"]:
raise ValueError("| TMDb Error: TMBd {} ID: {} not found".format("Movie/Collection" if is_movie else "Show", tmdb_id))
elif type in ["biography", "profile", "name"]:
person = Person()
person.api_key = api_key
person.language = language
try:
if type == "biography":
return person.details(id).biography
elif type == "profile_path":
return tmdb_url_prefix + person.details(id).profile_path
meta = person.details(tmdb_id).biography
elif type == "profile":
meta = person.details(tmdb_id).profile_path
elif type == "name":
return person.details(id).name
meta = person.details(tmdb_id).name
except AttributeError:
raise ValueError("| Config Error: TMBd Actor ID: {} not found".format(id))
raise ValueError("| TMDb Error: TMBd Actor ID: {} not found".format(tmdb_id))
else:
raise RuntimeError("type {} not yet supported in tmdb_get_metadata".format(type))
if meta is None:
raise ValueError("| TMDb Error: TMDB ID {} has no {}".format(tmdb_id, type))
elif type in ["profile", "poster", "backdrop"]:
return "https://image.tmdb.org/t/p/original" + meta
else:
return meta

View file

@ -1,34 +1,39 @@
import os
import argparse
import re
import sys
import threading
import glob
import datetime
from plexapi.server import PlexServer
from plexapi.video import Movie
from plexapi.video import Show
from plexapi.library import MovieSection
from plexapi.library import ShowSection
from plexapi.library import Collections
from plex_tools import add_to_collection
from plex_tools import delete_collection
from plex_tools import get_actor_rkey
from plex_tools import get_collection
from plex_tools import get_movie
from imdb_tools import tmdb_get_metadata
from imdb_tools import imdb_get_ids
from config_tools import Config
from config_tools import Plex
from config_tools import Radarr
from config_tools import TMDB
from config_tools import Tautulli
from config_tools import TraktClient
from config_tools import ImageServer
from config_tools import modify_config
from config_tools import check_for_attribute
from radarr_tools import add_to_radarr
from urllib.parse import urlparse
try:
import os
import argparse
import re
import sys
import threading
import glob
import datetime
from plexapi.server import PlexServer
from plexapi.video import Movie
from plexapi.video import Show
from plexapi.library import MovieSection
from plexapi.library import ShowSection
from plexapi.library import Collections
from plex_tools import get_map
from plex_tools import add_to_collection
from plex_tools import delete_collection
from plex_tools import get_actor_rkey
from plex_tools import get_collection
from plex_tools import get_item
from imdb_tools import tmdb_get_metadata
from imdb_tools import imdb_get_ids
from config_tools import Config
from config_tools import Plex
from config_tools import Radarr
from config_tools import TMDB
from config_tools import Tautulli
from config_tools import TraktClient
from config_tools import ImageServer
from config_tools import modify_config
from config_tools import check_for_attribute
from radarr_tools import add_to_radarr
from urllib.parse import urlparse
except ModuleNotFoundError:
print('|\n| Requirements Error: Please install requirements using "pip install -r requirements.txt"\n|')
sys.exit(0)
def regex_first_int(data, method, id_type="number", default=None):
try:
@ -104,12 +109,15 @@ def get_method_pair_year(method_to_parse, values_to_parse):
def update_from_config(config_path, plex, headless=False, no_meta=False, no_images=False):
config = Config(config_path)
collections = config.collections
if isinstance(plex.Library, MovieSection):
libtype = "movie"
elif isinstance(plex.Library, ShowSection):
libtype = "show"
if not headless:
print("|\n|===================================================================================================|")
if isinstance(plex.Library, MovieSection):
radarr = Radarr(config_path) if Radarr.valid else None
libtype = "movie"
elif isinstance(plex.Library, ShowSection):
radarr = None
libtype = "show"
plex_map = get_map(config_path, plex)
alias = {
"actors": "actor", "role": "actor", "roles": "actor",
"content_ratings": "content_rating", "contentRating": "content_rating", "contentRatings": "content_rating",
@ -130,6 +138,26 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
"subfilters": "filters",
"collection_sort": "collection_order"
}
pretty_names = {
"tmdb_collection": "TMDb Collection",
"tmdb_id": "TMDb ID",
"tmdb_company": "TMDb Company",
"tmdb_network": "TMDb Network",
"tmdb_discover": "TMDb Discover",
"tmdb_popular": "TMDb Popular",
"tmdb_top_rated": "TMDb Top Rated",
"tmdb_now_playing": "TMDb Now Playing",
"tmdb_trending_daily": "TMDb Trending Daily",
"tmdb_trending_weekly": "TMDb Trending Weekly",
"tmdb_list": "TMDb List",
"tmdb_movie": "TMDb Movie",
"tmdb_show": "TMDb Show",
"tvdb_show": "TVDb Show",
"imdb_list": "IMDb List",
"trakt_list": "Trakt List",
"trakt_trending": "Trakt Trending",
"trakt_watchlist": "Trakt Watchlist"
}
all_lists = [
"plex_search",
"plex_collection",
@ -144,8 +172,8 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
"tmdb_popular",
"tmdb_top_rated",
"tmdb_now_playing",
#"tmdb_trending_daily", #TURNON:Trending
#"tmdb_trending_weekly", #TURNON:Trending
"tmdb_trending_daily",
"tmdb_trending_weekly",
"tmdb_list",
"tmdb_movie",
"tmdb_show",
@ -169,8 +197,8 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
]
show_only_lists = [
"tmdb_show",
"tvdb_show"
"tmdb_network",
"tvdb_show",
"tmdb_network"
]
movie_only_lists = [
"tmdb_collection",
@ -205,7 +233,8 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
"originally_available.gte", "originally_available.lte",
"video_resolution", "video_resolution.not",
"audio_language", "audio_language.not",
"subtitle_language", "subtitle_language.not"
"subtitle_language", "subtitle_language.not",
"plex_collection", "plex_collection.not"
]
movie_only_filters = [
"country", "country.not",
@ -221,7 +250,7 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
"collection_mode", "collection_order",
"poster", "tmdb_poster", "tmdb_profile", "file_poster",
"background", "file_background",
"name_mapping"
"name_mapping", "add_to_radarr"
]
discover_movie = [
"language", "with_original_language", "region", "sort_by",
@ -308,7 +337,7 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
if ("tmdb" in m or "imdb" in m) and not TMDB.valid:
print("| Config Error: {} skipped. tmdb incorrectly configured".format(m))
map = {}
elif ("trakt" in m or (("tmdb" in m or "tvdb" in m) and plex.library_type == "show")) and not TraktClient.valid:
elif "trakt" in m and not TraktClient.valid:
print("| Config Error: {} skipped. trakt incorrectly configured".format(m))
map = {}
elif m == "tautulli" and not Tautulli.valid:
@ -349,12 +378,12 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
posters_found.append(["url", check_value, check_name])
elif check_name == "tmdb_poster":
try:
posters_found.append(["url", tmdb_get_metadata(config_path, check_value, "poster_path"), check_name])
posters_found.append(["url", tmdb_get_metadata(config_path, check_value, "poster"), check_name])
except ValueError as e:
print(e)
elif check_name == "tmdb_profile":
try:
posters_found.append(["url", tmdb_get_metadata(config_path, check_value, "profile_path"), check_name])
posters_found.append(["url", tmdb_get_metadata(config_path, check_value, "profile"), check_name])
except ValueError as e:
print(e)
elif check_name == "file_poster":
@ -366,7 +395,7 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
backgrounds_found.append(["url", check_value, check_name])
elif check_name == "tmdb_background":
try:
backgrounds_found.append(["url", tmdb_get_metadata(config_path, check_value, "backdrop_path"), check_name])
backgrounds_found.append(["url", tmdb_get_metadata(config_path, check_value, "backdrop"), check_name])
except ValueError as e:
print(e)
elif check_name == "file_background":
@ -374,9 +403,18 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
backgrounds_found.append(["file", os.path.abspath(check_value), check_name])
else:
print("| Config Error: Background Path Does Not Exist: {}".format(os.path.abspath(check_value)))
elif check_name == "add_to_radarr":
if check_value == True or check_value == False:
details[check_name] = check_value
else:
print("| Config Error: add_to_radarr must be either true or false")
else:
details[check_name] = check_value
if method_name == "details":
if method_name in show_only_lists and libtype == "movie":
print("| Config Error: {} attribute only works for show libraries".format(method_name))
elif (method_name in movie_only_filters or method_name in movie_only_lists) and libtype == "show":
print("| Config Error: {} attribute only works for movie libraries".format(method_name))
elif method_name == "details":
print("| Config Error: Please remove the details attribute all its old sub-attributes should be one level higher")
for detail_m in collections[c][m]:
if detail_m in alias:
@ -405,7 +443,7 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
if person_id is None:
if "summary" not in details:
details["summary"] = tmdb_get_metadata(config_path, id, "biography")
details["poster"] = ["url", tmdb_get_metadata(config_path, id, "profile_path"), method_name]
details["poster"] = ["url", tmdb_get_metadata(config_path, id, "profile"), method_name]
person_id = id
person_method = method_name
if method_name == "tmdb_actor":
@ -421,9 +459,9 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
final_collections = []
for new_collection in collection_list:
try:
final_collections.append(get_collection(plex, new_collection, headless))
final_collections.append(get_collection(plex, str(new_collection), headless))
except ValueError as e:
print("| Config Error: {} {}".format(method_name, new_collection))
print("| Config Error: {} {} Not Found".format(method_name, new_collection))
if len(final_collections) > 0:
methods.append(("plex_collection", final_collections))
elif method_name == "tmdb_collection":
@ -434,9 +472,18 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
id = get_method_pair_tmdb(method_name, collections[c][m], "TMDb ID")
if tmdb_id is None:
if "summary" not in details:
details["summary"] = tmdb_get_metadata(config_path, id[1][0], "overview")
details["poster"] = ["url", tmdb_get_metadata(config_path, id[1][0], "poster_path"), method_name]
details["poster"] = ["url", tmdb_get_metadata(config_path, id[1][0], "backdrop_path"), method_name]
try:
details["summary"] = tmdb_get_metadata(config_path, id[1][0], "overview")
except ValueError as e:
print(e)
try:
details["poster"] = ["url", tmdb_get_metadata(config_path, id[1][0], "poster"), method_name]
except ValueError as e:
print(e)
try:
details["background"] = ["url", tmdb_get_metadata(config_path, id[1][0], "backdrop"), method_name]
except ValueError as e:
print(e)
tmdb_id = id[1][0]
methods.append(id)
elif method_name in ["tmdb_popular", "tmdb_top_rated", "tmdb_now_playing", "tmdb_trending_daily", "tmdb_trending_weekly"]:
@ -602,6 +649,12 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
first_filter = False
print("| Collection Filter {}: {}".format(f[0], f[1]))
do_radarr = False
if radarr:
do_radarr = radarr.add_to_radarr
if "add_to_radarr" in details:
do_radarr = details["add_to_radarr"]
# Loops though and actually processes the methods
for m, values in methods:
for v in values:
@ -612,39 +665,26 @@ def update_from_config(config_path, plex, headless=False, no_meta=False, no_imag
elif m not in ["plex_search", "tmdb_list", "tmdb_id", "tmdb_movie", "tmdb_collection", "tmdb_company", "tmdb_network", "tmdb_discover", "tmdb_show"]:
print("| \n| Processing {}: {}".format(m, v))
try:
missing, map = add_to_collection(config_path, plex, m, v, c, map, filters)
missing, map = add_to_collection(config_path, plex, m, v, c, plex_map, map, filters)
except (KeyError, ValueError, SystemExit) as e:
print(e)
missing = False
if missing:
if libtype == "movie":
method_name = "IMDb" if "imdb" in m else "Trakt" if "trakt" in m else "TMDb"
if m in ["trakt_list", "trakt_watchlist", "tmdb_list"]:
print("| {} missing movie{} from {} List: {}".format(len(missing), "s" if len(missing) > 1 else "", method_name, v))
elif m == "imdb_list":
print("| {} missing movie{} from {} List: {}".format(len(missing), "s" if len(missing) > 1 else "", method_name, v[0]))
elif m == "tmdb_collection":
print("| {} missing movie{} from {} Collection: {}".format(len(missing), "s" if len(missing) > 1 else "", method_name, v))
elif m == "trakt_trending":
print("| {} missing movie{} from {} List: Trending (top {})".format(len(missing), "s" if len(missing) > 1 else "", method_name, v))
else:
print("| {} ID: {} missing".format(method_name, v))
if Radarr.valid:
radarr = Radarr(config_path)
if radarr.add_movie:
print("| Adding missing movies to Radarr")
add_to_radarr(config_path, missing)
elif not headless and radarr.add_movie is None and input("| Add missing movies to Radarr? (y/n): ").upper() == "Y":
add_to_radarr(config_path, missing)
elif libtype == "show":
method_name = "Trakt" if "trakt" in m else "TVDb" if "tvdb" in m else "TMDb"
if m in ["trakt_list", "trakt_watchlist", "tmdb_list"]:
print("| {} missing show{} from {} List: {}".format(len(missing), "s" if len(missing) > 1 else "", method_name, v))
elif m == "trakt_trending":
print("| {} missing show{} from {} List: Trending (top {})".format(len(missing), "s" if len(missing) > 1 else "", method_name, v))
else:
print("| {} ID: {} missing".format(method_name, v))
def missing_print(display_value):
print("| {} missing {}{} from {}: {}".format(len(missing), libtype, "s" if len(missing) > 1 else "", pretty_names[m], display_value))
if m in ["tmdb_popular", "tmdb_top_rated", "tmdb_now_playing", "tmdb_trending_daily", "tmdb_trending_weekly", "trakt_trending"]:
missing_print("Top {}".format(v))
elif m == "imdb_list":
missing_print(v[0])
else:
missing_print(v)
if do_radarr:
print("| Adding missing movies to Radarr")
add_to_radarr(config_path, missing)
elif do_radarr is None and not headless and input("| Add missing movies to Radarr? (y/n): ").upper() == "Y":
add_to_radarr(config_path, missing)
# if not skip_sonarr:
# if input("Add missing shows to Sonarr? (y/n): ").upper() == "Y":
# add_to_radarr(missing_shows)
@ -799,12 +839,12 @@ def append_collection(config_path, config_update=None):
method = "movie"
value = input("| Enter Movie (Name or Rating Key): ")
if value is int:
plex_movie = get_movie(plex, int(value))
plex_movie = get_item(plex, int(value))
print('| +++ Adding %s to collection %s' % (
plex_movie.title, selected_collection.title))
plex_movie.addCollection(selected_collection.title)
else:
results = get_movie(plex, value)
results = get_item(plex, value)
if len(results) > 1:
while True:
i = 1

View file

@ -14,6 +14,8 @@ from bs4 import BeautifulSoup
from urllib.request import Request
from urllib.request import urlopen
from urllib.parse import urlparse
from tmdbv3api import TMDb
from tmdbv3api import Movie as TMDb_Movie
import os
import sqlite3
import tempfile
@ -22,42 +24,25 @@ import shutil
from contextlib import closing
def get_movie(plex, data):
# If an int is passed as data, assume it is a movie's rating key
if isinstance(data, int):
try:
return plex.Server.fetchItem(data)
except PlexExceptions.BadRequest:
print("| Nothing found")
return None
elif isinstance(data, Movie):
return data
else:
movie_list = plex.Library.search(title=data)
if movie_list:
return movie_list
else:
print("| Movie: {} not found".format(data))
return None
def adjust_space(old_length, display_title):
display_title = str(display_title)
space_length = old_length - len(display_title)
if space_length > 0:
display_title += " " * space_length
return display_title
def get_item(plex, data):
# If an int is passed as data, assume it is a movie's rating key
if isinstance(data, int):
if isinstance(data, int) or isinstance(data, Movie) or isinstance(data, Show):
try:
return plex.Server.fetchItem(data)
return plex.Server.fetchItem(data.ratingKey if isinstance(data, Movie) or isinstance(data, Show) else data)
except PlexExceptions.BadRequest:
return "Nothing found"
elif isinstance(data, Movie):
return data
elif isinstance(data, Show):
return data
else:
print(data)
item_list = plex.Library.search(title=data)
if item_list:
return item_list
else:
return "Item: " + data + " not found"
return "Item: {} not found".format(data)
def get_actor_rkey(plex, data):
"""Takes in actors name as str and returns as Plex's corresponding rating key ID"""
@ -211,17 +196,24 @@ def get_collection(plex, data, exact=None, subtype=None):
print("| Invalid entry")
except (IndexError, ValueError) as E:
print("| Invalid entry")
elif len(collection_list) == 1 and (exact is None or (exact and collection_list[0].title == data)):
elif len(collection_list) == 1 and (exact is None or collection_list[0].title == data):
return collection_list[0]
else:
raise ValueError("Collection {} not found".format(data))
raise ValueError("Collection {} Not Found".format(data))
def add_to_collection(config_path, plex, method, value, c, map, filters=None):
movies = []
shows = []
def add_to_collection(config_path, plex, method, value, c, plex_map=None, map=None, filters=None):
if plex_map is None and ("imdb" in method or "tvdb" in method or "tmdb" in method or "trakt" in method):
plex_map = get_map(config_path, plex)
if map is None:
map = {}
items = []
missing = []
def search_plex():
if method == "all":
items = plex.Library.all()
elif method == "plex_collection":
items = value.children
elif method == "plex_search":
search_terms = {}
output = ""
for attr_pair in value:
@ -240,48 +232,32 @@ def add_to_collection(config_path, plex, method, value, c, map, filters=None):
ors = ors + (" OR " if len(ors) > 0 else attr_pair[0] + "(") + str(param)
output = output + ("\n|\t\t AND " if len(output) > 0 else "| Processing Plex Search: ") + ors + ")"
print(output)
return plex.Library.search(**search_terms)
if ("trakt" in method or (("tmdb" in method or "tvdb" in method) and plex.library_type == "show")) and not TraktClient.valid:
raise KeyError("| trakt connection required for {}",format(method))
elif ("imdb" in method or "tmdb" in method) and not TMDB.valid:
raise KeyError("| tmdb connection required for {}",format(method))
elif method == "tautulli" and not Tautulli.valid:
raise KeyError("| tautulli connection required for {}",format(method))
elif plex.library_type == "movie":
if method == "plex_collection":
movies = value.children
elif method == "imdb_list":
movies, missing = imdb_tools.imdb_get_movies(config_path, plex, value)
elif "tmdb" in method:
movies, missing = imdb_tools.tmdb_get_movies(config_path, plex, value, method)
elif "trakt" in method:
movies, missing = trakt_tools.trakt_get_movies(config_path, plex, value, method)
elif method == "tautulli":
movies, missing = imdb_tools.get_tautulli(config_path, plex, value)
elif method == "all":
movies = plex.Library.all()
elif method == "plex_search":
movies = search_plex()
items = plex.Library.search(**search_terms)
elif method == "tvdb_show" and plex.library_type == "show":
items, missing = imdb_tools.tvdb_get_shows(config_path, plex, plex_map, value)
elif "imdb" in method or "tmdb" in method:
if not TMDB.valid:
raise KeyError("| tmdb connection required for {}",format(method))
elif method == "imdb_list" and plex.library_type == "movie":
items, missing = imdb_tools.imdb_get_movies(config_path, plex, plex_map, value)
elif "tmdb" in method and plex.library_type == "movie":
items, missing = imdb_tools.tmdb_get_movies(config_path, plex, plex_map, value, method)
elif "tmdb" in method and plex.library_type == "show":
items, missing = imdb_tools.tmdb_get_shows(config_path, plex, plex_map, value, method)
elif "trakt" in method:
if not TraktClient.valid:
raise KeyError("| trakt connection required for {}",format(method))
elif plex.library_type == "movie":
items, missing = trakt_tools.trakt_get_movies(config_path, plex, plex_map, value, method)
elif plex.library_type == "show":
items, missing = trakt_tools.trakt_get_shows(config_path, plex, plex_map, value, method)
elif method == "tautulli":
if not Tautulli.valid:
raise KeyError("| tautulli connection required for {}",format(method))
else:
print("| Config Error: {} method not supported".format(method))
elif plex.library_type == "show":
if method == "plex_collection":
shows = value.children
elif "tmdb" in method:
shows, missing = imdb_tools.tmdb_get_shows(config_path, plex, value, method)
elif method == "tvdb_show":
shows, missing = imdb_tools.tvdb_get_shows(config_path, plex, value)
elif "trakt" in method:
shows, missing = trakt_tools.trakt_get_shows(config_path, plex, value, method)
elif method == "tautulli":
shows, missing = imdb_tools.get_tautulli(config_path, plex, value)
elif method == "all":
shows = plex.Library.all()
elif method == "plex_search":
shows = search_plex()
else:
print("| Config Error: {} method not supported".format(method))
items, missing = imdb_tools.get_tautulli(config_path, plex, value)
else:
print("| Config Error: {} method not supported".format(method))
filter_alias = {
"actor": "actors",
@ -298,161 +274,83 @@ def add_to_collection(config_path, plex, method, value, c, map, filters=None):
"video_resolution": "video_resolution",
"audio_language": "audio_language",
"subtitle_language": "subtitle_language",
"plex_collection": "collections",
}
if movies:
if items:
# Check if already in collection
cols = plex.Library.search(title=c, libtype="collection")
try:
fs = cols[0].children
except IndexError:
fs = []
movie_count = 0
movie_max = len(movies)
max_str_len = len(str(movie_max))
item_count = 0
item_max = len(items)
max_str_len = len(str(item_max))
current_length = 0
for rk in movies:
current_m = get_movie(plex, rk)
current_m.reload()
movie_count += 1
count_str_len = len(str(movie_count))
display_count = (" " * (max_str_len - count_str_len)) + str(movie_count)
for rk in items:
current_item = get_item(plex, rk)
item_count += 1
match = True
if filters:
display_count = (" " * (max_str_len - len(str(item_count)))) + str(item_count)
print_display = "| Filtering {}/{} {}".format(display_count, item_max, current_item.title)
print(adjust_space(current_length, print_display), end = "\r")
current_length = len(print_display)
for f in filters:
print_display = "| Filtering {}/{} {}".format(display_count, movie_max, current_m.title)
print(imdb_tools.adjust_space(current_length, print_display), end = "\r")
current_length = len(print_display)
modifier = f[0][-4:]
method = filter_alias[f[0][:-4]] if modifier in [".not", ".lte", ".gte"] else filter_alias[f[0]]
if method == "max_age":
threshold_date = datetime.now() - timedelta(days=f[1])
attr = getattr(current_m, "originallyAvailableAt")
attr = getattr(current_item, "originallyAvailableAt")
if attr is None or attr < threshold_date:
match = False
break
elif modifier in [".gte", ".lte"]:
if method == "originallyAvailableAt":
threshold_date = datetime.strptime(f[1], "%m/%d/%y")
attr = getattr(current_m, "originallyAvailableAt")
attr = getattr(current_item, "originallyAvailableAt")
if (modifier == ".lte" and attr > threshold_date) or (modifier == ".gte" and attr < threshold_date):
match = False
break
elif method in ["year", "rating"]:
attr = getattr(current_m, method)
attr = getattr(current_item, method)
if (modifier == ".lte" and attr > f[1]) or (modifier == ".gte" and attr < f[1]):
match = False
break
else:
terms = f[1] if isinstance(f[1], list) else str(f[1]).split(", ")
if method in ["video_resolution", "audio_language", "subtitle_language"]:
for media in current_m.media:
for media in current_item.media:
if method == "video_resolution":
mv_attrs = [media.videoResolution]
attrs = [media.videoResolution]
for part in media.parts:
if method == "audio_language":
mv_attrs = ([audio_stream.language for audio_stream in part.audioStreams()])
attrs = ([audio_stream.language for audio_stream in part.audioStreams()])
if method == "subtitle_language":
mv_attrs = ([subtitle_stream.language for subtitle_stream in part.subtitleStreams()])
attrs = ([subtitle_stream.language for subtitle_stream in part.subtitleStreams()])
elif method in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]: # Otherwise, it's a string. Make it a list.
mv_attrs = [str(getattr(current_m, method))]
elif method in ["actors", "countries", "directors", "genres", "writers"]:
mv_attrs = [getattr(x, 'tag') for x in getattr(current_m, method)]
attrs = [str(getattr(current_item, method))]
elif method in ["actors", "countries", "directors", "genres", "writers", "collections"]:
attrs = [getattr(x, 'tag') for x in getattr(current_item, method)]
# Get the intersection of the user's terms and movie's terms
# Get the intersection of the user's terms and item's terms
# If it's empty and modifier is not .not, it's not a match
# If it's not empty and modifier is .not, it's not a match
if (not list(set(terms) & set(mv_attrs)) and modifier != ".not") or (list(set(terms) & set(mv_attrs)) and modifier == ".not"):
if (not list(set(terms) & set(attrs)) and modifier != ".not") or (list(set(terms) & set(attrs)) and modifier == ".not"):
match = False
break
if match:
if current_m in fs:
map[current_m.ratingKey] = None
if current_item in fs:
map[current_item.ratingKey] = None
else:
current_m.addCollection(c)
print(imdb_tools.adjust_space(current_length, "| {} Collection | {} | {}".format(c, "=" if current_m in fs else "+", current_m.title)))
print(imdb_tools.adjust_space(current_length, "| Processed {} Movies".format(movie_max)))
elif plex.library_type == "movie":
print("| No movies found")
if shows:
# Check if already in collection
cols = plex.Library.search(title=c, libtype="collection")
try:
fs = cols[0].children
except IndexError:
fs = []
show_count = 0
show_max = len(shows)
current_length = 0
for rk in shows:
current_s = get_item(plex, rk)
current_s.reload()
show_count += 1
match = True
if filters:
for f in filters:
print_display = "| Filtering {}/{} {}".format(show_count, show_max, current_s.title)
print(imdb_tools.adjust_space(current_length, print_display), end = "\r")
current_length = len(print_display)
modifier = f[0][-4:]
method = filter_alias[f[0][:-4]] if modifier in [".not", ".lte", ".gte"] else filter_alias[f[0]]
if method == "max_age":
threshold_date = datetime.now() - timedelta(days=f[1])
attr = getattr(current_s, "originallyAvailableAt")
if attr is None or attr < threshold_date:
match = False
break
elif modifier in [".gte", ".lte"]:
if method == "originallyAvailableAt":
threshold_date = datetime.strptime(f[1], "%m/%d/%y")
attr = getattr(current_s, "originallyAvailableAt")
if (modifier == ".lte" and attr > threshold_date) or (modifier == ".gte" and attr < threshold_date):
match = False
break
elif method in ["year", "rating"]:
attr = getattr(current_s, method)
if (modifier == ".lte" and attr > f[1]) or (modifier == ".gte" and attr < f[1]):
match = False
break
else:
terms = f[1] if isinstance(f[1], list) else str(f[1]).split(", ")
# if method in ["video_resolution", "audio_language", "subtitle_language"]:
# for media in current_s.media:
# if method == "video_resolution":
# show_attrs = [media.videoResolution]
# for part in media.parts:
# if method == "audio_language":
# show_attrs = ([audio_stream.language for audio_stream in part.audioStreams()])
# if method == "subtitle_language":
# show_attrs = ([subtitle_stream.language for subtitle_stream in part.subtitleStreams()])
if method in ["contentRating", "studio", "year", "rating", "originallyAvailableAt"]:
mv_attrs = [str(getattr(current_s, method))]
elif method in ["actors", "genres"]:
mv_attrs = [getattr(x, 'tag') for x in getattr(current_s, method)]
# Get the intersection of the user's terms and show's terms
# If it's empty and modifier is not .not, it's not a match
# If it's not empty and modifier is .not, it's not a match
if (not list(set(terms) & set(show_attrs)) and modifier != ".not") or (list(set(terms) & set(show_attrs)) and modifier == ".not"):
match = False
break
if match:
if current_s in fs:
map[current_s.ratingKey] = None
else:
current_s.addCollection(c)
print(imdb_tools.adjust_space(current_length, "| {} Collection | {} | {}".format(c, "=" if current_s in fs else "+", current_s.title)))
print(imdb_tools.adjust_space(current_length, "| Processed {} Shows".format(show_max)))
elif plex.library_type == "show":
print("| No shows found")
try:
missing
except UnboundLocalError:
return
current_item.addCollection(c)
print(adjust_space(current_length, "| {} Collection | {} | {}".format(c, "=" if current_item in fs else "+", current_item.title)))
print(adjust_space(current_length, "| Processed {} {}".format(item_max, "Movies" if plex.library_type == "movie" else "Shows")))
else:
return missing, map
print("| No {} Found".format("Movies" if plex.library_type == "movie" else "Shows"))
return missing, map
def delete_collection(data):
confirm = input("| {} selected. Confirm deletion (y/n):".format(data.title))
@ -553,4 +451,4 @@ def update_guid_map(config_path, plex_guid, **kwargs):
tmdb_id = kwargs['tmdb_id']
cursor.execute('INSERT OR IGNORE INTO guids(plex_guid, tmdb_id, updated) VALUES(?, ?, ?)', (plex_guid, tmdb_id, datetime.now()))
cursor.execute('UPDATE guids SET tmdb_id = ?, updated = ? WHERE plex_guid = ?', (tmdb_id, datetime.now(), plex_guid))
connection.commit()
connection.commit()

View file

@ -12,28 +12,19 @@ def add_to_radarr(config_path, missing):
tmdb.language = config_tmdb.language
movie = Movie()
for m in missing:
# Get TMDb ID data from IMDb ID
search = movie.external(external_id=str(m), external_source="imdb_id")['movie_results']
if len(search) == 1:
tmdb_details = search[0]
else:
print("| --- Unable to match TMDb ID for IMDb ID {}, skipping".format(m))
continue
# Validate TMDb information (very few TMDb entries don't yet have basic information)
for tmdb_id in missing:
try:
tmdb_title = tmdb_details['title']
tmdb_id = tmdb_details['id']
except IndexError:
tmovie = movie.details(tmdb_id)
tmdb_title = tmovie.title
except AttributeError:
print("| --- Unable to fetch necessary TMDb information for IMDb ID {}, skipping".format(m))
continue
# Validate TMDb year (several TMDb entries don't yet have release dates)
try:
# This might be overly punitive
tmdb_year = tmdb_details['release_date'].split("-")[0]
except KeyError:
tmdb_year = tmovie.release_date.split("-")[0]
except AttributeError:
print("| --- {} does not have a release date on TMDb yet, skipping".format(tmdb_title))
continue
@ -41,7 +32,7 @@ def add_to_radarr(config_path, missing):
print("| --- {} does not have a release date on TMDb yet, skipping".format(tmdb_title))
continue
tmdb_poster = "https://image.tmdb.org/t/p/original{}".format(tmdb_details['poster_path'])
tmdb_poster = "https://image.tmdb.org/t/p/original{}".format(tmovie.poster_path)
titleslug = "{} {}".format(tmdb_title, tmdb_year)
titleslug = re.sub(r'([^\s\w]|_)+', '', titleslug)

View file

@ -1,11 +1,37 @@
import config_tools
from urllib.parse import urlparse
import plex_tools
import trakt
import os
def trakt_get_movies(config_path, plex, data, method):
def trakt_tmdb_to_imdb(config_path, tmdb_id):
config_tools.TraktClient(config_path)
lookup = trakt.Trakt['search'].lookup(tmdb_id, 'tmdb', 'movie')
if lookup:
lookup = lookup[0] if isinstance(lookup, list) else lookup
return lookup.get_key('imdb')
else:
return None
def trakt_imdb_to_tmdb(config_path, imdb_id):
config_tools.TraktClient(config_path)
lookup = trakt.Trakt['search'].lookup(imdb_id, 'imdb', 'movie')
if lookup:
lookup = lookup[0] if isinstance(lookup, list) else lookup
return lookup.get_key('tmdb')
else:
return None
def trakt_tmdb_to_tvdb(config_path, tmdb_id):
config_tools.TraktClient(config_path)
lookup = trakt.Trakt['search'].lookup(id, 'tmdb', 'show')
if lookup:
lookup = lookup[0] if isinstance(lookup, list) else lookup
return lookup.get_key('tvdb')
else:
return None
def trakt_get_movies(config_path, plex, plex_map, data, method):
config_tools.TraktClient(config_path)
if method == "trakt_trending":
max_items = int(data)
@ -24,52 +50,17 @@ def trakt_get_movies(config_path, plex, data, method):
trakt_list_items = trakt.Trakt[trakt_list_path].items()
title_ids = [str(m.get_key('tmdb')) for m in trakt_list_items if isinstance(m, trakt.objects.movie.Movie)]
imdb_map = {}
plex_tools.create_cache(config_path)
if title_ids:
for item in plex.Library.all():
item_type = urlparse(item.guid).scheme.split('.')[-1]
if item_type == 'plex':
# Check cache for imdb_id
imdb_id = plex_tools.query_cache(config_path, item.guid, 'imdb_id')
if not imdb_id:
imdb_id, tmdb_id = plex_tools.alt_id_lookup(plex, item)
print("| Cache | + | {} | {} | {} | {}".format(item.guid, imdb_id, tmdb_id, item.title))
plex_tools.update_cache(config_path, item.guid, imdb_id=imdb_id, tmdb_id=tmdb_id)
elif item_type == 'imdb':
imdb_id = urlparse(item.guid).netloc
elif item_type == 'themoviedb':
tmdb_id = urlparse(item.guid).netloc
# lookup can sometimes return a list
lookup = trakt.Trakt['search'].lookup(tmdb_id, 'tmdb', 'movie')
if lookup:
lookup = lookup[0] if isinstance(lookup, list) else lookup
imdb_id = lookup.get_key('imdb')
else:
imdb_id = None
else:
imdb_id = None
print("| {} Movies found on Trakt".format(len(title_ids)))
matched = []
missing = []
for tmdb_id in title_ids:
if tmdb_id in plex_map:
matched.append(plex.Server.fetchItem(plex_map[tmdb_id]))
else:
missing.append(tmdb_id)
return matched, missing
if imdb_id and imdb_id in title_ids:
imdb_map[imdb_id] = item
else:
imdb_map[item.ratingKey] = item
matched_imdb_movies = []
missing_imdb_movies = []
for imdb_id in title_ids:
movie = imdb_map.pop(imdb_id, None)
if movie:
matched_imdb_movies.append(plex.Server.fetchItem(movie.ratingKey))
else:
missing_imdb_movies.append(imdb_id)
return matched_imdb_movies, missing_imdb_movies
else:
# No movies
return None, None
def trakt_get_shows(config_path, plex, data, method):
def trakt_get_shows(config_path, plex, plex_map, data, method):
config_tools.TraktClient(config_path)
if method == "trakt_trending":
max_items = int(data)
@ -87,7 +78,6 @@ def trakt_get_shows(config_path, plex, data, method):
trakt_list_path = urlparse(trakt_url).path
trakt_list_items = trakt.Trakt[trakt_list_path].items()
tvdb_map = {}
title_ids = []
for m in trakt_list_items:
if isinstance(m, trakt.objects.show.Show):
@ -100,40 +90,11 @@ def trakt_get_shows(config_path, plex, data, method):
if m.show.pk[1] not in title_ids:
title_ids.append(m.show.pk[1])
if title_ids:
for item in plex.Library.all():
guid = urlparse(item.guid)
item_type = guid.scheme.split('.')[-1]
# print('item_type', item, item_type)
if item_type == 'thetvdb':
tvdb_id = guid.netloc
elif item_type == 'themoviedb':
tmdb_id = guid.netloc
lookup = trakt.Trakt['search'].lookup(tmdb_id, 'tmdb', 'show')
if lookup:
lookup = lookup[0] if isinstance(lookup, list) else lookup
tvdb_id = lookup.get_key('tvdb')
else:
tvdb_id = None
else:
tvdb_id = None
if tvdb_id and tvdb_id in title_ids:
tvdb_map[tvdb_id] = item
else:
tvdb_map[item.ratingKey] = item
matched_tvdb_shows = []
missing_tvdb_shows = []
for tvdb_id in title_ids:
show = tvdb_map.pop(tvdb_id, None)
if show:
matched_tvdb_shows.append(plex.Server.fetchItem(show.ratingKey))
else:
missing_tvdb_shows.append(tvdb_id)
return matched_tvdb_shows, missing_tvdb_shows
else:
# No shows
return None, None
matched = []
missing = []
for tvdb_id in title_ids:
if tvdb_id in plex_map:
matched.append(plex.Server.fetchItem(plex_map[tvdb_id]))
else:
missing.append(tvdb_id)
return matched, missing

View file

@ -69,7 +69,7 @@ radarr:
token: ###########################
quality_profile_id: 4
root_folder_path: /mnt/user/PlexMedia/movies
add_movie: false
add_to_radarr: false
search_movie: false
tmdb:
apikey: ############################

View file

@ -2,7 +2,7 @@
PyYAML==5.3.1
# Less common, pinned
PlexAPI==4.2.0
tmdbv3api==1.6.2
tmdbv3api==1.7.1
trakt.py==4.2.0
# More common, flexible
bs4