[41] add imdb_search buidler

pull/1767/head
meisnate12 5 months ago
parent 4ef840bbab
commit 910ca94e5b

@ -1 +1 @@
1.19.1-develop40
1.19.1-develop41

@ -50,9 +50,9 @@ collections:
## IMDb List
Finds every item in an IMDb List, [Keyword Search](https://www.imdb.com/search/keyword/), [Title Search](https://www.imdb.com/search/title/), or [Topic Search](https://www.imdb.com/search/title-text/).
Finds every item in an IMDb List or [Keyword Search](https://www.imdb.com/search/keyword/).
The expected input is an IMDb List URL or IMDb Search URL. Multiple values are supported as a list only a comma-separated string will not work.
The expected input is an IMDb List URL or IMDb Keyword Search URL. Multiple values are supported as a list only a comma-separated string will not work.
The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
@ -151,4 +151,100 @@ collections:
- ur12345678
collection_order: custom
sync_mode: sync
```
```
## IMDb Search
Finds every item using an [IMDb Advance Title Search](https://www.imdb.com/search/title/)
The `sync_mode: sync` and `collection_order: custom` Details are recommended since the lists are continuously updated and in a specific order.
| Search Parameter | Description |
|:-------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `limit` | Specify how items you want returned by the query.<br>**Options:** Any Integer greater then `0`<br>**Default:** `100` |
| `sort_by` | Choose from one of the many available sort options.<br>**Options:** `popularity.asc`, `popularity.desc`, `title.asc`, `title.desc`, `rating.asc`, `rating.desc`, `votes.asc`, `votes.desc`, `box_office.asc`, `box_office.desc`, `runtime.asc`, `runtime.desc`, `year.asc`, `year.desc`, `release.asc`, `release.desc`<br>**Default:** `popularity.asc` |
| `title` | Search by title name.<br>**Options:** Any String |
| `type` | Item must match at least one given type. Can be a comma-separated list.<br>**Options:** `movie`, `tv_series`, `short`, `tv_episode`, `tv_mini_series`, `tv_movie`, `tv_special`, `tv_short`, `video_game`, `video`, `music_video`, `podcast_series`, `podcast_episode` |
| `type.not` | Item must not match any of the given types. Can be a comma-separated list.<br>**Options:** `movie`, `tv_series`, `short`, `tv_episode`, `tv_mini_series`, `tv_movie`, `tv_special`, `tv_short`, `video_game`, `video`, `music_video`, `podcast_series`, `podcast_episode` |
| `release.after` | Item must have been released after the given date.<br>**Options:** `today` or Date in the format `YYYY-MM-DD` |
| `release.before` | Item must have been released before the given date.<br>**Options:** `today` or Date in the format `YYYY-MM-DD` |
| `rating.gte` | Item must have an IMDb Rating greater then or equal to the given number.<br>**Options:** Any Number `0.1` - `10.0`<br>**Example:** `7.5` |
| `rating.lte` | Item must have an IMDb Rating less then or equal to the given number.<br>**Options:** Any Number `0.1` - `10.0`<br>**Example:** `7.5` |
| `votes.gte` | Item must have a Number of Votes greater then or equal to the given number.<br>**Options:** Any Integer greater then `0`<br>**Example:** `1000` |
| `votes.lte` | Item must have a Number of Votes less then or equal to the given number.<br>**Options:** Any Integer greater then `0`<br>**Example:** `1000` |
| `genre` | Item must match all genres given. Can be a comma-separated list.<br>**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` |
| `genre.any` | Item must match at least one given genre. Can be a comma-separated list.<br>**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` |
| `genre.not` | Item must not match any og the given genres. Can be a comma-separated list.<br>**Options:** `action`, `adventure`, `animation`, `biography`, `comedy`, `documentary`, `drama`, `crime`, `family`, `history`, `news`, `short`, `western`, `sport`, `reality-tv`, `horror`, `fantasy`, `film-noir`, `music`, `romance`, `talk-show`, `thriller`, `war`, `sci-fi`, `musical`, `mystery`, `game-show` |
| `event` | Item must have been nominated for a category at the event given. Can be a comma-separated list.<br>**Options:** `cannes`, `choice`, `spirit`, `sundance`, `bafta`, `oscar`, `emmy`, `golden`, `oscar_picture`, `oscar_director`, `national_film_board_preserved`, `razzie`, or any [IMDb Event ID](https://www.imdb.com/event/all/) (ex. `ev0050888`) |
| `event.winning` | Item must have won a category at the event given. Can be a comma-separated list.<br>**Options:** `cannes`, `choice`, `spirit`, `sundance`, `bafta`, `oscar`, `emmy`, `golden`, `oscar_picture`, `oscar_director`, `national_film_board_preserved`, `razzie`, or any [IMDb Event ID](https://www.imdb.com/event/all/) (ex. `ev0050888`) |
| `imdb_top` | Item must be in the top number of given Movies.<br>**Options:** Any Integer greater then `0` |
| `imdb_bottom` | Item must be in the bottom number of given Movies.<br>**Options:** Any Integer greater then `0` |
| `company` | Item must have been released by any company given. Can be a comma-separated list.<br>**Options:** `fox`, `dreamworks`, `mgm`, `paramount`, `sony`, `universal`, `disney`, `warner`, or any IMDb Company ID (ex. `co0023400`) |
| `content_rating` | Item must have the given content rating. Can be a list.<br>**Options:** Dictionary with two attributes `rating` and `region`<br>`rating`: Any String to match the content rating<br>`region`: [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `country` | Item must match with every given country. Can be a comma-separated list.<br>**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `country.any` | Item must match at least one given country. Can be a comma-separated list.<br>**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `country.not` | Item must not match any given country. Can be a comma-separated list.<br>**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `country.origin` | Item must match any given country as the origin country. Can be a comma-separated list.<br>**Options:** [2 Digit ISO 3166 Country Code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `keyword` | Item must match with every given keyword. Can be a comma-separated list.<br>**Options:** Any String |
| `keyword.any` | Item must match at least one given keyword. Can be a comma-separated list.<br>**Options:** Any String |
| `keyword.not` | Item must not match any given keyword. Can be a comma-separated list.<br>**Options:** Any String |
| `series` | Item must match with every given series. Can be a comma-separated list.<br>**Options:** Any IMDb ID (ex. `tt0096697`) |
| `series.any` | Item must match at least one given series. Can be a comma-separated list.<br>**Options:** Any IMDb ID (ex. `tt0096697`) |
| `series.not` | Item must not match any given series. Can be a comma-separated list.<br>**Options:** Any IMDb ID (ex. `tt0096697`) |
| `list` | Item must be on every given list. Can be a comma-separated list.<br>**Options:** Any IMDb List ID (ex. `ls000024621`) |
| `list.any` | Item must be on at least one given lists. Can be a comma-separated list.<br>**Options:** Any IMDb List ID (ex. `ls000024621`) |
| `list.not` | Item must not be on any given lists. Can be a comma-separated list.<br>**Options:** Any IMDb List ID (ex. `ls000024621`) |
| `language` | Item must match any given language. Can be a comma-separated list.<br>**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) |
| `language.any` | Item must match at least one given language. Can be a comma-separated list.<br>**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) |
| `language.not` | Item must not match any given language. Can be a comma-separated list.<br>**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) |
| `language.primary` | Item must match any given language as the primary language. Can be a comma-separated list.<br>**Options:** [ISO 639-2 Language Codes](https://en.wikipedia.org/wiki/List_of_ISO_639-2_codes) |
| `popularity.gte` | Item must have a Popularity greater then or equal to the given number.<br>**Options:** Any Integer greater then `0`<br>**Example:** `1000` |
| `popularity.lte` | Item must have a Popularity less then or equal to the given number.<br>**Options:** Any Integer greater then `0`<br>**Example:** `1000` |
| `cast` | Item must have all the given cast members. Can be a comma-separated list.<br>**Options:** Any IMDb Person ID (ex. `nm0000138`) |
| `cast.any` | Item must have any of the given cast members. Can be a comma-separated list.<br>**Options:** Any IMDb Person ID (ex. `nm0000138`) |
| `cast.not` | Item must not have any of the given cast members. Can be a comma-separated list.<br>**Options:** Any IMDb Person ID (ex. `nm0000138`) |
| `runtime.gte` | Item must have a Runtime greater then or equal to the given number.<br>**Options:** Any Integer greater then `0`<br>**Example:** `1000` |
| `runtime.lte` | Item must have a Runtime less then or equal to the given number.<br>**Options:** Any Integer greater then `0`<br>**Example:** `1000` |
| `adult` | Include adult titles in the search results.<br>**Options:** `true`/`false` |
### Examples
```yaml
collections:
IMDb Popular:
imdb_search:
type: movie
sort_by: popularity.asc
limit: 50
collection_order: custom
sync_mode: sync
```
```yaml
collections:
Top Action:
imdb_search:
type: movie
release.after: 1990-01-01
rating.gte: 5
votes.gte: 100000
genre: action
sort_by: rating.desc
limit: 100
```
You can also find episodes using `imdb_search` like so.
```yaml
collections:
The Simpsons Top 100 Episodes:
collection_order: custom
builder_level: episode
sync_mode: sync
imdb_search:
type: tv_episode
series: tt0096697
sort: rating.desc
limit: 100
summary: The top 100 Simpsons episodes by IMDb user rating
```

@ -435,45 +435,45 @@ The `sync_mode: sync` and `collection_order: custom` Details are recommended sin
### Discover Movies Parameters
| Movie Parameters | Description |
|:--------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `limit` | Specify how many movies you want returned by the query.<br>**Type:** Integer<br>**Default:** 100 |
| `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. Will use the `region` specified in the [TMDb Config](../config/tmdb.md) by default.<br>**Type:** `^[A-Z]{2}$` |
| `sort_by` | Choose from one of the many available sort options.<br>**Type:** Any [sort options](#sort-options) below<br>**Default:** `popularity.desc` |
| `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification.<br>**Type:** String |
| `certification` | Filter results with a valid certification from the `certification_country` parameter.<br>**Type:** String |
| `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value.<br>**Type:** String |
| `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value.<br>**Type:** String |
| `include_adult` | A filter and include or exclude adult movies.<br>**Type:** Boolean |
| `include_video` | A filter and include or exclude videos.<br>**Type:** Boolean |
| `primary_release_year` | A filter to limit the results to a specific primary release year.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** Date: `MM/DD/YYYY` |
| `with_release_type` | Specify a comma (AND) or pipe (OR) separated value to filter release types by.<br>**Type:** String<br>**Values:** `1`: Premiere, `2`: Theatrical (limited), `3`: Theatrical, `4`: Digital, `5`: Physical, `6`: TV |
| `year` | A filter to limit the results to a specific year (looking at all release dates).<br>**Type:** Year: `YYYY` |
| `vote_count.gte` | Filter and only include movies that have a vote count that is greater or equal to the specified value.<br>**Type:** Integer |
| `vote_count.lte` | Filter and only include movies that have a vote count that is less than or equal to the specified value.<br>**Type:** Integer |
| `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value.<br>**Type:** Number |
| `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** String |
| `with_people` | A comma-separated list of person ID's. Only include movies that have one of the ID's added as either an actor or a crew member.<br>**Type:** 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.<br>**Type:** String |
| `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.<br>**Type:** String |
| `with_genres` | Comma-separated value of genre ids that you want to include in the results.<br>**Type:** String |
| `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.<br>**Type:** 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.<br>**Type:** String |
| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.<br>**Type:** String |
| `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value.<br>**Type:** Integer |
| `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value.<br>**Type:** Integer |
| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.<br>**Type:** String |
| `with_title_translation` | Specify a language/country string to filter the results by if the item has a type of title translation.<br>**Type:** String<br>**Values:** `ar-AE`, `ar-SA`, `bg-BG`, `bn-BD`, `ca-ES`, `ch-GU`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-US`, `eo-EO`, `es-ES`, `es-MX`, `eu-ES`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ka-GE`, `kn-IN`, `ko-KR`, `lt-LT`, `ml-IN`, `nb-NO`, `nl-NL`, `no-NO`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sl-SI`, `sr-RS`, `sv-SE`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-TW` |
| `with_overview_translation` | Specify a language/country string to filter the results by if the item has a type of overview translation.<br>**Type:** String<br>**Values:** `ar-AE`, `ar-SA`, `bg-BG`, `bn-BD`, `ca-ES`, `ch-GU`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-US`, `eo-EO`, `es-ES`, `es-MX`, `eu-ES`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ka-GE`, `kn-IN`, `ko-KR`, `lt-LT`, `ml-IN`, `nb-NO`, `nl-NL`, `no-NO`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sl-SI`, `sr-RS`, `sv-SE`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-TW` |
| `with_watch_providers` | A comma or pipe separated list of watch provider ID's. Combine this filter with `watch_region` in order to filter your results by a specific watch provider in a specific region.<br>**Type:** String |
| `watch_region` | An [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). Combine this filter with `with_watch_providers` in order to filter your results by a specific watch provider in a specific region.<br>**Type:** String<br>**Values:** [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `with_watch_monetization_types` | In combination with `watch_region`, you can filter by monetization type.<br>**Type:** String<br>**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` |
| Movie Parameters | Description |
|:--------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `limit` | Specify how many movies you want returned by the query.<br>**Type:** Integer<br>**Default:** 100 |
| `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. Will use the `region` specified in the [TMDb Config](../config/tmdb.md) by default.<br>**Type:** `^[A-Z]{2}$` |
| `sort_by` | Choose from one of the many available sort options.<br>**Type:** Any [sort options](#sort-options) below<br>**Default:** `popularity.desc` |
| `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification.<br>**Type:** String |
| `certification` | Filter results with a valid certification from the `certification_country` parameter.<br>**Type:** String |
| `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value.<br>**Type:** String |
| `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value.<br>**Type:** String |
| `include_adult` | A filter and include or exclude adult movies.<br>**Type:** Boolean |
| `include_video` | A filter and include or exclude videos.<br>**Type:** Boolean |
| `primary_release_year` | A filter to limit the results to a specific primary release year.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** Date: `MM/DD/YYYY` |
| `with_release_type` | Specify a comma (AND) or pipe (OR) separated value to filter release types by.<br>**Type:** String<br>**Values:** `1`: Premiere, `2`: Theatrical (limited), `3`: Theatrical, `4`: Digital, `5`: Physical, `6`: TV |
| `year` | A filter to limit the results to a specific year (looking at all release dates).<br>**Type:** Year: `YYYY` |
| `vote_count.gte` | Filter and only include movies that have a vote count that is greater or equal to the specified value.<br>**Type:** Integer |
| `vote_count.lte` | Filter and only include movies that have a vote count that is less than or equal to the specified value.<br>**Type:** Integer |
| `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value.<br>**Type:** Number |
| `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value.<br>**Type:** 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.<br>**Type:** 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.<br>**Type:** String |
| `with_people` | A comma-separated list of person ID's. Only include movies that have one of the ID's added as either an actor or a crew member.<br>**Type:** 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.<br>**Type:** String |
| `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.<br>**Type:** String |
| `with_genres` | Comma-separated value of genre ids that you want to include in the results.<br>**Type:** String |
| `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.<br>**Type:** 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.<br>**Type:** String |
| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.<br>**Type:** String |
| `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value.<br>**Type:** Integer |
| `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value.<br>**Type:** Integer |
| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.<br>**Type:** String |
| `with_title_translation` | Specify a language/country string to filter the results by if the item has a type of title translation.<br>**Type:** String<br>**Values:** `ar-AE`, `ar-SA`, `bg-BG`, `bn-BD`, `ca-ES`, `ch-GU`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-US`, `eo-EO`, `es-ES`, `es-MX`, `eu-ES`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ka-GE`, `kn-IN`, `ko-KR`, `lt-LT`, `ml-IN`, `nb-NO`, `nl-NL`, `no-NO`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sl-SI`, `sr-RS`, `sv-SE`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-TW` |
| `with_overview_translation` | Specify a language/country string to filter the results by if the item has a type of overview translation.<br>**Type:** String<br>**Values:** `ar-AE`, `ar-SA`, `bg-BG`, `bn-BD`, `ca-ES`, `ch-GU`, `cs-CZ`, `da-DK`, `de-DE`, `el-GR`, `en-US`, `eo-EO`, `es-ES`, `es-MX`, `eu-ES`, `fa-IR`, `fi-FI`, `fr-CA`, `fr-FR`, `he-IL`, `hi-IN`, `hu-HU`, `id-ID`, `it-IT`, `ja-JP`, `ka-GE`, `kn-IN`, `ko-KR`, `lt-LT`, `ml-IN`, `nb-NO`, `nl-NL`, `no-NO`, `pl-PL`, `pt-BR`, `pt-PT`, `ro-RO`, `ru-RU`, `sk-SK`, `sl-SI`, `sr-RS`, `sv-SE`, `ta-IN`, `te-IN`, `th-TH`, `tr-TR`, `uk-UA`, `vi-VN`, `zh-CN`, `zh-TW` |
| `with_watch_providers` | A comma or pipe separated list of watch provider ID's. Combine this filter with `watch_region` in order to filter your results by a specific watch provider in a specific region.<br>**Type:** String |
| `watch_region` | An [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes). Combine this filter with `with_watch_providers` in order to filter your results by a specific watch provider in a specific region.<br>**Type:** String<br>**Values:** [ISO 3166-1 code](https://en.wikipedia.org/wiki/List_of_ISO_3166_country_codes) |
| `with_watch_monetization_types` | In combination with `watch_region`, you can filter by monetization type.<br>**Type:** String<br>**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` |

@ -137,7 +137,10 @@ class AniList:
ani_attr = attr_translation[attr] if attr in attr_translation else attr
final = ani_attr if attr in no_mod_searches else f"{ani_attr}_{mod_translation[mod]}"
if attr in ["start", "end"]:
value = int(util.validate_date(value, f"anilist_search {key}", return_as="%Y%m%d"))
try:
value = int(util.validate_date(value, return_as="%Y%m%d"))
except Failed as e:
raise Failed(f"Collection Error: anilist_search {key}: {e}")
elif attr in ["format", "status", "genre", "tag", "tag_category"]:
temp_value = [self.options[attr.replace('_', ' ').title()][v.lower().replace(' / ', '-').replace(' ', '-')] for v in value]
if attr in ["format", "status"]:

@ -1419,8 +1419,9 @@ class CollectionBuilder:
dict_methods = {dm.lower(): dm for dm in dict_data}
new_dictionary = {}
for search_method, search_data in dict_data.items():
search_attr, modifier = os.path.splitext(str(search_method).lower())
if search_method not in anilist.searches:
lower_method = str(search_method).lower()
search_attr, modifier = os.path.splitext(lower_method)
if lower_method not in anilist.searches:
raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute not supported")
elif search_attr == "season":
new_dictionary[search_attr] = util.parse(self.Type, search_attr, search_data, parent=method_name, default=current_season, options=util.seasons)
@ -1440,16 +1441,16 @@ class CollectionBuilder:
elif search_attr == "source":
new_dictionary[search_attr] = util.parse(self.Type, search_attr, search_data, options=anilist.media_source, parent=method_name)
elif search_attr in ["episodes", "duration", "score", "popularity"]:
new_dictionary[search_method] = util.parse(self.Type, search_method, search_data, datatype="int", parent=method_name)
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="int", parent=method_name)
elif search_attr in ["format", "status", "genre", "tag", "tag_category"]:
new_dictionary[search_method] = self.config.AniList.validate(search_attr.replace("_", " ").title(), util.parse(self.Type, search_method, search_data))
new_dictionary[lower_method] = self.config.AniList.validate(search_attr.replace("_", " ").title(), util.parse(self.Type, search_method, search_data))
elif search_attr in ["start", "end"]:
new_dictionary[search_method] = util.validate_date(search_data, f"{method_name} {search_method} attribute", return_as="%m/%d/%Y")
new_dictionary[search_attr] = util.parse(self.Type, search_attr, search_data, datatype="date", parent=method_name, date_return="%m/%d/%Y")
elif search_attr == "min_tag_percent":
new_dictionary[search_attr] = util.parse(self.Type, search_attr, search_data, datatype="int", parent=method_name, minimum=0, maximum=100)
elif search_attr == "search":
new_dictionary[search_attr] = str(search_data)
elif search_method not in ["sort_by", "limit"]:
elif lower_method not in ["sort_by", "limit"]:
raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute not supported")
if len(new_dictionary) == 0:
raise Failed(f"{self.Type} Error: {method_name} must have at least one valid search option")
@ -1498,6 +1499,129 @@ class CollectionBuilder:
elif method_name == "imdb_watchlist":
for imdb_user in self.config.IMDb.validate_imdb_watchlists(self.Type, method_data, self.language):
self.builders.append((method_name, imdb_user))
elif method_name == "imdb_search":
for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"):
dict_methods = {dm.lower(): dm for dm in dict_data}
new_dictionary = {"limit": util.parse(self.Type, "limit", dict_data, datatype="int", methods=dict_methods, minimum=0, default=100, parent=method_name)}
for search_method, search_data in dict_data.items():
lower_method = str(search_method).lower()
search_attr, modifier = os.path.splitext(lower_method)
if search_data is None:
raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute is blank")
elif lower_method not in imdb.imdb_search_attributes:
raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute not supported")
elif search_attr == "sort_by":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, parent=method_name, options=imdb.sort_options)
elif search_attr == "title":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, parent=method_name)
elif search_attr == "type":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name, options=imdb.title_type_options)
elif search_attr == "release":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="date", parent=method_name, date_return="%Y-%m-%d")
elif search_attr == "rating":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="float", parent=method_name, minimum=0.1, maximum=10)
elif search_attr in ["votes", "imdb_top", "imdb_bottom", "popularity", "runtime"]:
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="int", parent=method_name, minimum=0)
elif search_attr == "genre":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name, options=imdb.genre_options)
elif search_attr == "event":
events = []
for event in util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name):
if event in imdb.event_options:
events.append(event)
else:
res = re.search(r'(ev\d+)', event)
if res:
events.append(res.group(1))
else:
raise Failed(f"{method_name} {search_method} attribute: {search_data} must match pattern ev\d+ e.g. ev0000292 or be one of {', '.join([e for e in imdb.event_options])}")
if events:
new_dictionary[lower_method] = events
elif search_attr == "company":
companies = []
for company in util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name):
if company in imdb.company_options:
companies.append(company)
else:
res = re.search(r'(co\d+)', company)
if res:
companies.append(res.group(1))
else:
raise Failed(f"{method_name} {search_method} attribute: {search_data} must match pattern co\d+ e.g. co0098836 or be one of {', '.join([e for e in imdb.company_options])}")
if companies:
new_dictionary[lower_method] = companies
elif search_attr == "content_rating":
final_list = []
for content in util.get_list(search_data):
if content:
final_dict = {"region": "US", "rating": None}
if not isinstance(content, dict):
final_dict["rating"] = str(content)
else:
if "rating" not in content or not content["rating"]:
raise Failed(f"{method_name} {search_method} attribute: rating attribute is required")
final_dict["rating"] = str(content["rating"])
if "region" not in content or not content["region"]:
logger.warning(f"{method_name} {search_method} attribute: region attribute not found defaulting to 'US'")
elif len(str(content["region"])) != 2:
logger.warning(f"{method_name} {search_method} attribute: region attribute: {str(content['region'])} must be only 2 characters defaulting to 'US'")
else:
final_dict["region"] = str(content["region"]).upper()
final_list.append(final_dict)
if final_list:
new_dictionary[lower_method] = final_list
elif search_attr == "country":
countries = []
for country in util.parse(self.Type, search_method, search_data, datatype="upperlist", parent=method_name):
if country:
if len(str(country)) != 2:
raise Failed(f"{method_name} {search_method} attribute: {country} must be only 2 characters i.e. 'US'")
countries.append(str(country))
if countries:
new_dictionary[lower_method] = countries
elif search_attr == "keyword":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="strlist", parent=method_name)
elif search_attr == "language":
new_dictionary[lower_method] = util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name)
elif search_attr == "cast":
casts = []
for cast in util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name):
res = re.search(r'(nm\d+)', cast)
if res:
casts.append(res.group(1))
else:
raise Failed(f"{method_name} {search_method} attribute: {search_data} must match pattern nm\d+ e.g. nm00988366")
if casts:
new_dictionary[lower_method] = casts
elif search_attr == "series":
series = []
for show in util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name):
res = re.search(r'(tt\d+)', show)
if res:
series.append(res.group(1))
else:
raise Failed(f"{method_name} {search_method} attribute: {search_data} must match pattern tt\d+ e.g. tt00988366")
if series:
new_dictionary[lower_method] = series
elif search_attr == "list":
lists = []
for new_list in util.parse(self.Type, search_method, search_data, datatype="lowerlist", parent=method_name):
res = re.search(r'(ls\d+)', new_list)
if res:
lists.append(res.group(1))
else:
raise Failed(f"{method_name} {search_method} attribute: {search_data} must match pattern ls\d+ e.g. ls000024621")
if lists:
new_dictionary[lower_method] = lists
elif search_attr == "adult":
if util.parse(self.Type, search_method, search_data, datatype="bool", parent=method_name):
new_dictionary[lower_method] = True
else:
raise Failed(f"{self.Type} Error: {method_name} {search_method} attribute not supported")
if len(new_dictionary) > 1:
self.builders.append((method_name, new_dictionary))
else:
raise Failed(f"{self.Type} Error: {method_name} had no valid fields")
def _letterboxd(self, method_name, method_data):
if method_name.startswith("letterboxd_list"):
@ -1682,56 +1806,57 @@ class CollectionBuilder:
dict_methods = {dm.lower(): dm for dm in dict_data}
new_dictionary = {"limit": util.parse(self.Type, "limit", dict_data, datatype="int", methods=dict_methods, default=100, parent=method_name)}
for discover_method, discover_data in dict_data.items():
discover_attr, modifier = os.path.splitext(str(discover_method).lower())
lower_method = str(discover_method).lower()
discover_attr, modifier = os.path.splitext(lower_method)
if discover_data is None:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute is blank")
elif discover_method not in tmdb.discover_all:
elif discover_method.lower() not in tmdb.discover_all:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute not supported")
elif self.library.is_movie and discover_attr in tmdb.discover_tv_only:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute only works for show libraries")
elif self.library.is_show and discover_attr in tmdb.discover_movie_only:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute only works for movie libraries")
elif discover_attr == "region":
new_dictionary[discover_attr] = util.parse(self.Type, discover_attr, discover_data, parent=method_name, regex=("^[A-Z]{2}$", "US"))
new_dictionary[discover_attr] = util.parse(self.Type, discover_method, discover_data, parent=method_name, regex=("^[A-Z]{2}$", "US"))
elif discover_attr == "sort_by":
options = tmdb.discover_movie_sort if self.library.is_movie else tmdb.discover_tv_sort
new_dictionary[discover_method] = util.parse(self.Type, discover_attr, discover_data, parent=method_name, options=options)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, parent=method_name, options=options)
elif discover_attr == "certification_country":
if "certification" in dict_data or "certification.lte" in dict_data or "certification.gte" in dict_data:
new_dictionary[discover_method] = discover_data
new_dictionary[lower_method] = discover_data
else:
raise Failed(f"{self.Type} Error: {method_name} {discover_attr} attribute: must be used with either certification, certification.lte, or certification.gte")
elif discover_attr == "certification":
if "certification_country" in dict_data:
new_dictionary[discover_method] = discover_data
new_dictionary[lower_method] = discover_data
else:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute: must be used with certification_country")
elif discover_attr == "watch_region":
if "with_watch_providers" in dict_data or "without_watch_providers" in dict_data or "with_watch_monetization_types" in dict_data:
new_dictionary[discover_method] = discover_data
new_dictionary[lower_method] = discover_data
else:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute: must be used with either with_watch_providers, without_watch_providers, or with_watch_monetization_types")
elif discover_attr == "with_watch_monetization_types":
if "watch_region" in dict_data:
new_dictionary[discover_method] = util.parse(self.Type, discover_attr, discover_data, parent=method_name, options=tmdb.discover_monetization_types)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, parent=method_name, options=tmdb.discover_monetization_types)
else:
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute: must be used with watch_region")
elif discover_attr in tmdb.discover_booleans:
new_dictionary[discover_method] = util.parse(self.Type, discover_attr, discover_data, datatype="bool", parent=method_name)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="bool", parent=method_name)
elif discover_attr == "vote_average":
new_dictionary[discover_method] = util.parse(self.Type, discover_method, discover_data, datatype="float", parent=method_name)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="float", parent=method_name)
elif discover_attr == "with_status":
new_dictionary[discover_method] = util.parse(self.Type, discover_attr, discover_data, datatype="int", parent=method_name, minimum=0, maximum=5)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="int", parent=method_name, minimum=0, maximum=5)
elif discover_attr == "with_type":
new_dictionary[discover_method] = util.parse(self.Type, discover_attr, discover_data, datatype="int", parent=method_name, minimum=0, maximum=6)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="int", parent=method_name, minimum=0, maximum=6)
elif discover_attr in tmdb.discover_dates:
new_dictionary[discover_method] = util.validate_date(discover_data, f"{method_name} {discover_method} attribute", return_as="%m/%d/%Y")
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="date", parent=method_name, date_return="%m/%d/%Y")
elif discover_attr in tmdb.discover_years:
new_dictionary[discover_method] = util.parse(self.Type, discover_attr, discover_data, datatype="int", parent=method_name, minimum=1800, maximum=self.current_year + 1)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="int", parent=method_name, minimum=1800, maximum=self.current_year + 1)
elif discover_attr in tmdb.discover_ints:
new_dictionary[discover_method] = util.parse(self.Type, discover_method, discover_data, datatype="int", parent=method_name)
new_dictionary[lower_method] = util.parse(self.Type, discover_method, discover_data, datatype="int", parent=method_name)
elif discover_attr in tmdb.discover_strings:
new_dictionary[discover_method] = discover_data
new_dictionary[lower_method] = discover_data
elif discover_attr != "limit":
raise Failed(f"{self.Type} Error: {method_name} {discover_method} attribute not supported")
if len(new_dictionary) > 1:
@ -2449,10 +2574,10 @@ class CollectionBuilder:
logger.error(error)
return valid_list
elif attribute in date_attributes and modifier in [".before", ".after"]:
if data == "today":
return datetime.strftime(datetime.now(), "%Y-%m-%d")
else:
return util.validate_date(data, final, return_as="%Y-%m-%d")
try:
return util.validate_date(datetime.now() if data == "today" else data, return_as="%Y-%m-%d")
except Failed as e:
raise Failed(f"{self.Type} Error: {final}: {e}")
elif attribute in date_attributes and modifier in ["", ".not"]:
search_mod = "d"
if plex_search and data and str(data)[-1] in ["s", "m", "h", "d", "w", "o", "y"]:

@ -5,7 +5,7 @@ from urllib.parse import urlparse, parse_qs
logger = util.logger
builders = ["imdb_list", "imdb_id", "imdb_chart", "imdb_watchlist"]
builders = ["imdb_list", "imdb_id", "imdb_chart", "imdb_watchlist", "imdb_search"]
movie_charts = ["box_office", "popular_movies", "top_movies", "top_english", "top_indian", "lowest_rated"]
show_charts = ["popular_shows", "top_shows"]
charts = {
@ -18,11 +18,62 @@ charts = {
"top_indian": "Top Rated Indian Movies",
"lowest_rated": "Lowest Rated Movies"
}
imdb_search_attributes = [
"sort_by", "title", "type", "type.not", "release.after", "release.before", "rating.gte", "rating.lte",
"votes.gte", "votes.lte", "genre", "genre.any", "genre.not", "event", "event.winning", "series", "series.any", "series.not",
"imdb_top", "imdb_bottom", "company", "content_rating", "country", "country.any", "country.not", "country.origin",
"keyword", "keyword.any", "keyword.not", "language", "language.any", "language.not", "language.primary",
"popularity.gte", "popularity.lte", "cast", "cast.any", "cast.not", "runtime.gte", "runtime.lte", "adult",
]
sort_by_options = {
"popularity": "POPULARITY",
"title": "TITLE_REGIONAL",
"rating": "USER_RATING",
"votes": "USER_RATING_COUNT",
"box_office": "BOX_OFFICE_GROSS_DOMESTIC",
"runtime": "RUNTIME",
"year": "YEAR",
"release": "RELEASE_DATE",
}
sort_options = [f"{a}.{d}"for a in sort_by_options for d in ["asc", "desc"]]
title_type_options = {
"movie": "movie", "tv_series": "tvSeries", "short": "short", "tv_episode": "tvEpisode", "tv_mini_series": "tvMiniSeries",
"tv_movie": "tvMovie", "tv_special": "tvSpecial", "tv_short": "tvShort", "video_game": "videoGame", "video": "video",
"music_video": "musicVideo", "podcast_series": "podcastSeries", "podcast_episode": "podcastEpisode"
}
genre_options = {a.lower(): a for a in [
"Action", "Adventure", "Animation", "Biography", "Comedy", "Documentary", "Drama", "Crime", "Family", "History",
"News", "Short", "Western", "Sport", "Reality-TV", "Horror", "Fantasy", "Film-Noir", "Music", "Romance",
"Talk-Show", "Thriller", "War", "Sci-Fi", "Musical", "Mystery", "Game-Show"
]}
company_options = {
"fox": ["co0000756", "co0176225", "co0201557", "co0017497"],
"dreamworks": ["co0067641", "co0040938", "co0252576", "co0003158"],
"mgm": ["co0007143", "co0026841"],
"paramount": ["co0023400"],
"sony": ["co0050868", "co0026545", "co0121181"],
"universal": ["co0005073", "co0055277", "co0042399"],
"disney": ["co0008970", "co0017902", "co0098836", "co0059516", "co0092035", "co0049348"],
"warner": ["co0002663", "co0005035", "co0863266", "co0072876", "co0080422", "co0046718"],
}
event_options = {
"cannes": {"eventId": "ev0000147"},
"choice": {"eventId": "ev0000133"},
"spirit": {"eventId": "ev0000349"},
"sundance": {"eventId": "ev0000631"},
"bafta": {"eventId": "ev0000123"},
"oscar": {"eventId": "ev0000003"},
"emmy": {"eventId": "ev0000223"},
"golden": {"eventId": "ev0000292"},
"oscar_picture": {"eventId": "ev0000003", "searchAwardCategoryId": "bestPicture"},
"oscar_director": {"eventId": "ev0000003", "searchAwardCategoryId": "bestDirector"},
"national_film_board_preserved": {"eventId": "ev0000468"},
"razzie": {"eventId": "ev0000558"},
}
base_url = "https://www.imdb.com"
graphql_url = "https://api.graphql.imdb.com/"
urls = {
"lists": f"{base_url}/list/ls",
"searches": f"{base_url}/search/title/",
"title_text_searches": f"{base_url}/search/title-text/",
"keyword_searches": f"{base_url}/search/keyword/",
"filmography_searches": f"{base_url}/filmosearch/"
}
@ -42,6 +93,9 @@ class IMDb:
response = self.config.get_html(url, headers=headers, params=params)
return response.xpath(xpath) if xpath else response
def _graph_request(self, json_data):
return self.config.post_json(graphql_url, headers={"content-type": "application/json"}, json=json_data)
def validate_imdb_lists(self, err_type, imdb_lists, language):
valid_lists = []
for imdb_dict in util.get_list(imdb_lists, split=False):
@ -102,12 +156,6 @@ class IMDb:
if imdb_url.startswith(urls["lists"]):
xpath_total = "//div[@class='desc lister-total-num-results']/text()"
per_page = 100
elif imdb_url.startswith(urls["searches"]):
xpath_total = "//div[@class='desc']/span/text()"
per_page = 250
elif imdb_url.startswith(urls["title_text_searches"]):
xpath_total = "//div[@class='desc']/span/text()"
per_page = 50
else:
xpath_total = "//div[@class='desc']/text()"
per_page = 50
@ -135,7 +183,6 @@ class IMDb:
params.pop("page", None) # noqa
logger.trace(f"URL: {imdb_base}")
logger.trace(f"Params: {params}")
search_url = imdb_base.startswith(urls["searches"])
if limit < 1 or total < limit:
limit = total
remainder = limit % item_count
@ -145,15 +192,9 @@ class IMDb:
for i in range(1, num_of_pages + 1):
start_num = (i - 1) * item_count + 1
logger.ghost(f"Parsing Page {i}/{num_of_pages} {start_num}-{limit if i == num_of_pages else i * item_count}")
if search_url:
params["count"] = remainder if i == num_of_pages else item_count # noqa
params["start"] = start_num # noqa
elif imdb_base.startswith(urls["title_text_searches"]):
params["start"] = start_num # noqa
else:
params["page"] = i # noqa
params["page"] = i # noqa
ids_found = self._request(imdb_base, language=language, xpath="//div[contains(@class, 'lister-item-image')]//a/img//@data-tconst", params=params)
if not search_url and i == num_of_pages:
if i == num_of_pages:
ids_found = ids_found[:remainder]
imdb_ids.extend(ids_found)
time.sleep(2)
@ -162,6 +203,206 @@ class IMDb:
return imdb_ids
raise Failed(f"IMDb Error: No IMDb IDs Found at {imdb_url}")
def _search_json(self, data):
out = {
"locale": "en-US",
"first": data["limit"] if "limit" in data and data["limit"] < 250 else 250,
"titleTypeConstraint": {"anyTitleTypeIds": [title_type_options[t] for t in data["type"]] if "type" in data else []},
}
sort = data["sort_by"] if "sort_by" in data else "popularity.asc"
sort_by, sort_order = sort.split(".")
out["sortBy"] = sort_by_options[sort_by]
out["sortOrder"] = sort_order.upper()
if "type.not" in data:
out["titleTypeConstraint"]["excludeTitleTypeIds"] = [title_type_options[t] for t in data["type.not"]]
if "release.after" in data or "release.before" in data:
num_range = {}
if "release.after" in data:
num_range["start"] = data["release.after"]
if "release.before" in data:
num_range["end"] = data["release.before"]
out["releaseDateConstraint"] = {"releaseDateRange": num_range}
if "title" in data:
out["titleTextConstraint"] = {"searchTerm": data["title"]}
if any([a in data for a in ["rating.gte", "rating.lte", "votes.gte", "votes.lte"]]):
out["userRatingsConstraint"] = {}
num_range = {}
if "rating.gte" in data:
num_range["min"] = data["rating.gte"]
if "rating.lte" in data:
num_range["max"] = data["rating.lte"]
out["userRatingsConstraint"]["aggregateRatingRange"] = num_range
num_range = {}
if "votes.gte" in data:
num_range["min"] = data["votes.gte"]
if "votes.lte" in data:
num_range["max"] = data["votes.lte"]
out["userRatingsConstraint"]["ratingsCountRange"] = num_range
if any([a in data for a in ["genre", "genre.any", "genre.not"]]):
out["genreConstraint"] = {}
if "genre" in data:
out["genreConstraint"]["allGenreIds"] = [genre_options[g] for g in data["genre"]]
if "genre.any" in data:
out["genreConstraint"]["anyGenreIds"] = [genre_options[g] for g in data["genre.any"]]
if "genre.not" in data:
out["genreConstraint"]["excludeGenreIds"] = [genre_options[g] for g in data["genre.not"]]
if "event" in data or "event.winning" in data:
input_list = []
if "event" in data:
input_list.extend([event_options[a] if a in event_options else {"eventId": a} for a in data["event"]])
if "event.winning" in data:
for a in data["event.winning"]:
award_dict = event_options[a] if a in event_options else {"eventId": a}
award_dict["winnerFilter"] = "WINNER_ONLY"
input_list.append(award_dict)
out["awardConstraint"] = {"allEventNominations": input_list}
if any([a in data for a in ["imdb_top", "imdb_bottom", "popularity.gte", "popularity.lte"]]):
ranges = []
if "imdb_top" in data:
ranges.append({"rankRange": {"max": data["imdb_top"]}, "rankedTitleListType": "TOP_RATED_MOVIES"})
if "imdb_bottom" in data:
ranges.append({"rankRange": {"max": data["imdb_bottom"]}, "rankedTitleListType": "LOWEST_RATED_MOVIES"})
if "popularity.gte" in data or "popularity.lte" in data:
num_range = {}
if "popularity.lte" in data:
num_range["max"] = data["popularity.lte"]
if "popularity.gte" in data:
num_range["min"] = data["popularity.gte"]
ranges.append({"rankRange": num_range, "rankedTitleListType": "TITLE_METER"})
out["rankedTitleListConstraint"] = {"allRankedTitleLists": ranges}
if any([a in data for a in ["series", "series.any", "series.not"]]):
out["episodicConstraint"] = {}
if "series" in data:
out["episodicConstraint"]["allSeriesIds"] = data["series"]
if "series.any" in data:
out["episodicConstraint"]["anySeriesIds"] = data["series.any"]
if "series.not" in data:
out["episodicConstraint"]["excludeSeriesIds"] = data["series.not"]
if any([a in data for a in ["list", "list.any", "list.not"]]):
out["listConstraint"] = {}
if "list" in data:
out["listConstraint"]["inAllLists"] = data["list"]
if "list.any" in data:
out["listConstraint"]["inAnyList"] = data["list.any"]
if "list.not" in data:
out["listConstraint"]["notInAnyList"] = data["list.not"]
if "company" in data:
company_ids = []
for c in data["company"]:
if c in company_options:
company_ids.extend(company_options[c])
else:
company_ids.append(c)
out["creditedCompanyConstraint"] = {"anyCompanyIds": company_ids}
if "content_rating" in data:
out["certificateConstraint"] = {"anyRegionCertificateRatings": data["content_rating"]}
if any([a in data for a in ["country", "country.any", "country.not", "country.origin"]]):
out["originCountryConstraint"] = {}
if "country" in data:
out["originCountryConstraint"]["allCountries"] = data["country"]
if "country.any" in data:
out["originCountryConstraint"]["anyCountries"] = data["country.any"]
if "country.not" in data:
out["originCountryConstraint"]["excludeCountries"] = data["country.not"]
if "country.origin" in data:
out["originCountryConstraint"]["anyPrimaryCountries"] = data["country.origin"]
if any([a in data for a in ["keyword", "keyword.any", "keyword.not"]]):
out["keywordConstraint"] = {}
if "keyword" in data:
out["keywordConstraint"]["allKeywords"] = data["keyword"]
if "keyword.any" in data:
out["keywordConstraint"]["anyKeywords"] = data["keyword.any"]
if "keyword.not" in data:
out["keywordConstraint"]["excludeKeywords"] = data["keyword.not"]
if any([a in data for a in ["language", "language.any", "language.not", "language.primary"]]):
out["languageConstraint"] = {}
if "language" in data:
out["languageConstraint"]["allLanguages"] = data["language"]
if "language.any" in data:
out["languageConstraint"]["anyLanguages"] = data["language.any"]
if "language.not" in data:
out["languageConstraint"]["excludeLanguages"] = data["language.not"]
if "language.primary" in data:
out["languageConstraint"]["anyPrimaryLanguages"] = data["language.primary"]
if any([a in data for a in ["cast", "cast.any", "cast.not"]]):
out["creditedNameConstraint"] = {}
if "cast" in data:
out["creditedNameConstraint"]["allNameIds"] = data["cast"]
if "cast.any" in data:
out["creditedNameConstraint"]["anyNameIds"] = data["cast.any"]
if "cast.not" in data:
out["creditedNameConstraint"]["excludeNameIds"] = data["cast.not"]
if "runtime.gte" in data or "runtime.lte" in data:
num_range = {}
if "runtime.gte" in data:
num_range["min"] = data["runtime.gte"]
if "runtime.lte" in data:
num_range["max"] = data["runtime.lte"]
out["runtimeConstraint"] = {"runtimeRangeMinutes": num_range}
if "adult" in data and data["adult"]:
out["explicitContentConstraint"] = {"explicitContentFilter": "INCLUDE_ADULT"}
logger.trace(out)
return {
"operationName": "AdvancedTitleSearch",
"variables": out,
"extensions": {
"persistedQuery": {
"version": 1,
"sha256Hash": "7327d144ec84b57c93f761affe0d0609b0d495f85e8e47fdc76291679850cfda"
}
}
}
def _search(self, data):
json_obj = self._search_json(data)
item_count = 250
imdb_ids = []
logger.ghost("Parsing Page 1")
response_json = self._graph_request(json_obj)
total = response_json["data"]["advancedTitleSearch"]["total"]
limit = data["limit"] if "limit" in data else 0
if limit < 1 or total < limit:
limit = total
remainder = limit % item_count
if remainder == 0:
remainder = item_count
num_of_pages = math.ceil(int(limit) / item_count)
end_cursor = response_json["data"]["advancedTitleSearch"]["pageInfo"]["endCursor"]
imdb_ids.extend([n["node"]["title"]["id"] for n in response_json["data"]["advancedTitleSearch"]["edges"]])
if num_of_pages > 1:
for i in range(2, num_of_pages + 1):
start_num = (i - 1) * item_count + 1
logger.ghost(f"Parsing Page {i}/{num_of_pages} {start_num}-{limit if i == num_of_pages else i * item_count}")
json_obj["variables"]["after"] = end_cursor
response_json = self._graph_request(json_obj)
end_cursor = response_json["data"]["advancedTitleSearch"]["pageInfo"]["endCursor"]
ids_found = [n["node"]["title"]["id"] for n in response_json["data"]["advancedTitleSearch"]["edges"]]
if i == num_of_pages:
ids_found = ids_found[:remainder]
imdb_ids.extend(ids_found)
logger.exorcise()
if len(imdb_ids) > 0:
return imdb_ids
raise Failed("IMDb Error: No IMDb IDs Found")
def keywords(self, imdb_id, language, ignore_cache=False):
imdb_keywords = {}
expired = None
@ -238,6 +479,11 @@ class IMDb:
elif method == "imdb_watchlist":
logger.info(f"Processing IMDb Watchlist: {data}")
return [(_i, "imdb") for _i in self._watchlist(data, language)]
elif method == "imdb_search":
logger.info(f"Processing IMDb Search:")
for k, v in data.items():
logger.info(f" {k}: {v}")
return [(_i, "imdb") for _i in self._search(data)]
else:
raise Failed(f"IMDb Error: Method {method} not supported")

@ -1631,7 +1631,10 @@ class MetadataFile(DataFile):
current = str(getattr(current_item, key, ""))
final_value = None
if var_type == "date":
final_value = util.validate_date(value, name, return_as="%Y-%m-%d")
try:
final_value = util.validate_date(value, return_as="%Y-%m-%d")
except Failed as ei:
raise Failed(f"{self.type_str} Error: {name} {ei}")
current = current[:-9]
elif var_type == "float":
try:

@ -857,7 +857,7 @@ class Operations:
if self.library.radarr_remove_by_tag:
logger.info("")
logger.separator(f"Radarr Remove {len(self.library.sonarr_remove_by_tag)} Movies with Tags: {', '.join(self.library.sonarr_remove_by_tag)}", space=False, border=False)
logger.separator(f"Radarr Remove {len(self.library.radarr_remove_by_tag)} Movies with Tags: {', '.join(self.library.radarr_remove_by_tag)}", space=False, border=False)
logger.info("")
self.library.Radarr.remove_all_with_tags(self.library.radarr_remove_by_tag)
if self.library.sonarr_remove_by_tag:

@ -135,7 +135,7 @@ class TMDbMovie(TMDBObj):
raise Failed(f"TMDb Error: No Movie found for TMDb ID {self.tmdb_id}")
except TMDbException as e:
logger.stacktrace()
raise Failed(f"TMDb Error: Unexpected Error with TMDb ID {self.tmdb_id}: {e}")
raise TMDbException(f"TMDb Error: Unexpected Error with TMDb ID {self.tmdb_id}: {e}")
class TMDbShow(TMDBObj):
@ -172,7 +172,7 @@ class TMDbShow(TMDBObj):
raise Failed(f"TMDb Error: No Show found for TMDb ID {self.tmdb_id}")
except TMDbException as e:
logger.stacktrace()
raise Failed(f"TMDb Error: Unexpected Error with TMDb ID {self.tmdb_id}: {e}")
raise TMDbException(f"TMDb Error: Unexpected Error with TMDb ID {self.tmdb_id}: {e}")
class TMDb:
def __init__(self, config, params):
@ -344,7 +344,10 @@ class TMDb:
limit = int(attrs.pop("limit"))
for date_attr in date_methods:
if date_attr in attrs:
attrs[date_attr] = util.validate_date(attrs[date_attr], f"tmdb_discover attribute {date_attr}", return_as="%Y-%m-%d")
try:
attrs[date_attr] = util.validate_date(attrs[date_attr], return_as="%Y-%m-%d")
except Failed as e:
raise Failed(f"Collection Error: tmdb_discover attribute {date_attr}: {e}")
if is_movie and region and "region" not in attrs:
attrs["region"] = region
logger.trace(f"Params: {attrs}")

@ -262,14 +262,14 @@ def get_int_list(data, id_type):
except Failed as e: logger.error(e)
return int_values
def validate_date(date_text, method, return_as=None):
def validate_date(date_text, return_as=None):
if isinstance(date_text, datetime):
date_obg = date_text
else:
try:
date_obg = datetime.strptime(str(date_text), "%Y-%m-%d" if "-" in str(date_text) else "%m/%d/%Y")
except ValueError:
raise Failed(f"Collection Error: {method}: {date_text} must match pattern YYYY-MM-DD (e.g. 2020-12-25) or MM/DD/YYYY (e.g. 12/25/2020)")
raise Failed(f"{date_text} must match pattern YYYY-MM-DD (e.g. 2020-12-25) or MM/DD/YYYY (e.g. 12/25/2020)")
return datetime.strftime(date_obg, return_as) if return_as else date_obg
def validate_regex(data, col_type, validate=True):
@ -513,7 +513,10 @@ def is_date_filter(value, modifier, data, final, current_time):
or (modifier == ".not" and value and value >= threshold_date):
return True
elif modifier in [".before", ".after"]:
filter_date = validate_date(data, final)
try:
filter_date = validate_date(data)
except Failed as e:
raise Failed(f"Collection Error: {final}: {e}")
if (modifier == ".before" and value >= filter_date) or (modifier == ".after" and value <= filter_date):
return True
elif modifier == ".regex":
@ -741,21 +744,23 @@ def parse_and_or(error, attribute, data, test_list):
out += ")"
return out, final
def parse(error, attribute, data, datatype=None, methods=None, parent=None, default=None, options=None, translation=None, minimum=1, maximum=None, regex=None, range_split=None):
def parse(error, attribute, data, datatype=None, methods=None, parent=None, default=None, options=None, translation=None, minimum=1, maximum=None, regex=None, range_split=None, date_return=None):
display = f"{parent + ' ' if parent else ''}{attribute} attribute"
if options is None and translation is not None:
options = [o for o in translation]
value = data[methods[attribute]] if methods and attribute in methods else data
if datatype in ["list", "commalist", "strlist", "lowerlist"]:
if datatype in ["list", "commalist", "strlist", "lowerlist", "upperlist"]:
final_list = []
if value:
if datatype in ["commalist", "strlist"] and isinstance(value, dict):
if isinstance(value, dict):
raise Failed(f"{error} Error: {display} {value} must be a list or string")
if datatype == "commalist":
value = get_list(value)
if datatype == "lowerlist":
value = get_list(value, lower=True)
if datatype == "upperlist":
value = get_list(value, upper=True)
if not isinstance(value, list):
value = [value]
for v in value:
@ -840,11 +845,16 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa
message = f"{message} {minimum} or greater" if maximum is None else f"{message} between {minimum} and {maximum}"
if range_split:
message = f"{message} separated by a {range_split}"
elif datatype == "date":
try:
return validate_date(datetime.now() if data == "today" else data, return_as=date_return)
except Failed as e:
message = f"{e}"
elif (translation is not None and str(value).lower() not in translation) or \
(options is not None and translation is None and str(value).lower() not in options):
message = f"{display} {value} must be in {', '.join([str(o) for o in options])}"
else:
return translation[value] if translation is not None else value
return translation[str(value).lower()] if translation is not None else value
if default is None:
raise Failed(f"{error} Error: {message}")

Loading…
Cancel
Save