From 910ca94e5b1ec52108cd571fc7a38b3cb20557a4 Mon Sep 17 00:00:00 2001 From: meisnate12 Date: Thu, 7 Dec 2023 14:49:32 -0500 Subject: [PATCH] [41] add imdb_search buidler --- VERSION | 2 +- docs/builders/imdb.md | 102 ++++++++++++++- docs/builders/tmdb.md | 78 ++++++------ modules/anilist.py | 5 +- modules/builder.py | 177 ++++++++++++++++++++++---- modules/imdb.py | 282 +++++++++++++++++++++++++++++++++++++++--- modules/meta.py | 5 +- modules/operations.py | 2 +- modules/tmdb.py | 9 +- modules/util.py | 24 ++-- 10 files changed, 586 insertions(+), 100 deletions(-) diff --git a/VERSION b/VERSION index 951fdaf7..75750685 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.19.1-develop40 +1.19.1-develop41 diff --git a/docs/builders/imdb.md b/docs/builders/imdb.md index dbd7a7bf..293cee3c 100644 --- a/docs/builders/imdb.md +++ b/docs/builders/imdb.md @@ -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 -``` \ No newline at end of file +``` + +## 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.
**Options:** Any Integer greater then `0`
**Default:** `100` | +| `sort_by` | Choose from one of the many available sort options.
**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`
**Default:** `popularity.asc` | +| `title` | Search by title name.
**Options:** Any String | +| `type` | Item must match at least one given type. Can be a comma-separated list.
**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.
**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.
**Options:** `today` or Date in the format `YYYY-MM-DD` | +| `release.before` | Item must have been released before the given date.
**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.
**Options:** Any Number `0.1` - `10.0`
**Example:** `7.5` | +| `rating.lte` | Item must have an IMDb Rating less then or equal to the given number.
**Options:** Any Number `0.1` - `10.0`
**Example:** `7.5` | +| `votes.gte` | Item must have a Number of Votes greater then or equal to the given number.
**Options:** Any Integer greater then `0`
**Example:** `1000` | +| `votes.lte` | Item must have a Number of Votes less then or equal to the given number.
**Options:** Any Integer greater then `0`
**Example:** `1000` | +| `genre` | Item must match all genres given. Can be a comma-separated list.
**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.
**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.
**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.
**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.
**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.
**Options:** Any Integer greater then `0` | +| `imdb_bottom` | Item must be in the bottom number of given Movies.
**Options:** Any Integer greater then `0` | +| `company` | Item must have been released by any company given. Can be a comma-separated list.
**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.
**Options:** Dictionary with two attributes `rating` and `region`
`rating`: Any String to match the content rating
`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.
**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.
**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.
**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.
**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.
**Options:** Any String | +| `keyword.any` | Item must match at least one given keyword. Can be a comma-separated list.
**Options:** Any String | +| `keyword.not` | Item must not match any given keyword. Can be a comma-separated list.
**Options:** Any String | +| `series` | Item must match with every given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | +| `series.any` | Item must match at least one given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | +| `series.not` | Item must not match any given series. Can be a comma-separated list.
**Options:** Any IMDb ID (ex. `tt0096697`) | +| `list` | Item must be on every given list. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | +| `list.any` | Item must be on at least one given lists. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | +| `list.not` | Item must not be on any given lists. Can be a comma-separated list.
**Options:** Any IMDb List ID (ex. `ls000024621`) | +| `language` | Item must match any given language. Can be a comma-separated list.
**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.
**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.
**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.
**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.
**Options:** Any Integer greater then `0`
**Example:** `1000` | +| `popularity.lte` | Item must have a Popularity less then or equal to the given number.
**Options:** Any Integer greater then `0`
**Example:** `1000` | +| `cast` | Item must have all the given cast members. Can be a comma-separated list.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | +| `cast.any` | Item must have any of the given cast members. Can be a comma-separated list.
**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.
**Options:** Any IMDb Person ID (ex. `nm0000138`) | +| `runtime.gte` | Item must have a Runtime greater then or equal to the given number.
**Options:** Any Integer greater then `0`
**Example:** `1000` | +| `runtime.lte` | Item must have a Runtime less then or equal to the given number.
**Options:** Any Integer greater then `0`
**Example:** `1000` | +| `adult` | Include adult titles in the search results.
**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 +``` diff --git a/docs/builders/tmdb.md b/docs/builders/tmdb.md index f34441d5..d6f6a42c 100644 --- a/docs/builders/tmdb.md +++ b/docs/builders/tmdb.md @@ -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.
**Type:** Integer
**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.
**Type:** `^[A-Z]{2}$` | -| `sort_by` | Choose from one of the many available sort options.
**Type:** Any [sort options](#sort-options) below
**Default:** `popularity.desc` | -| `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification.
**Type:** String | -| `certification` | Filter results with a valid certification from the `certification_country` parameter.
**Type:** String | -| `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value.
**Type:** String | -| `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value.
**Type:** String | -| `include_adult` | A filter and include or exclude adult movies.
**Type:** Boolean | -| `include_video` | A filter and include or exclude videos.
**Type:** Boolean | -| `primary_release_year` | A filter to limit the results to a specific primary release year.
**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.
**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.
**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.
**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.
**Type:** Date: `MM/DD/YYYY` | -| `with_release_type` | Specify a comma (AND) or pipe (OR) separated value to filter release types by.
**Type:** String
**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).
**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.
**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.
**Type:** Integer | -| `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value.
**Type:** Number | -| `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value.
**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.
**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.
**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.
**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.
**Type:** String | -| `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.
**Type:** String | -| `with_genres` | Comma-separated value of genre ids that you want to include in the results.
**Type:** String | -| `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.
**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.
**Type:** String | -| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.
**Type:** String | -| `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value.
**Type:** Integer | -| `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value.
**Type:** Integer | -| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.
**Type:** String | -| `with_title_translation` | Specify a language/country string to filter the results by if the item has a type of title translation.
**Type:** String
**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.
**Type:** String
**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.
**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.
**Type:** String
**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.
**Type:** String
**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` | +| Movie Parameters | Description | +|:--------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `limit` | Specify how many movies you want returned by the query.
**Type:** Integer
**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.
**Type:** `^[A-Z]{2}$` | +| `sort_by` | Choose from one of the many available sort options.
**Type:** Any [sort options](#sort-options) below
**Default:** `popularity.desc` | +| `certification_country` | Used in conjunction with the certification parameter, use this to specify a country with a valid certification.
**Type:** String | +| `certification` | Filter results with a valid certification from the `certification_country` parameter.
**Type:** String | +| `certification.lte` | Filter and only include movies that have a certification that is less than or equal to the specified value.
**Type:** String | +| `certification.gte` | Filter and only include movies that have a certification that is greater than or equal to the specified value.
**Type:** String | +| `include_adult` | A filter and include or exclude adult movies.
**Type:** Boolean | +| `include_video` | A filter and include or exclude videos.
**Type:** Boolean | +| `primary_release_year` | A filter to limit the results to a specific primary release year.
**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.
**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.
**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.
**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.
**Type:** Date: `MM/DD/YYYY` | +| `with_release_type` | Specify a comma (AND) or pipe (OR) separated value to filter release types by.
**Type:** String
**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).
**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.
**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.
**Type:** Integer | +| `vote_average.gte` | Filter and only include movies that have a rating that is greater or equal to the specified value.
**Type:** Number | +| `vote_average.lte` | Filter and only include movies that have a rating that is less than or equal to the specified value.
**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.
**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.
**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.
**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.
**Type:** String | +| `without_companies` | Filter the results to exclude the specific production companies you specify here. AND / OR filters are supported.
**Type:** String | +| `with_genres` | Comma-separated value of genre ids that you want to include in the results.
**Type:** String | +| `without_genres` | Comma-separated value of genre ids that you want to exclude from the results.
**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.
**Type:** String | +| `without_keywords` | Exclude items with certain keywords. You can comma and pipe separate these values to create an 'AND' or 'OR' logic.
**Type:** String | +| `with_runtime.gte` | Filter and only include movies that have a runtime that is greater or equal to a value.
**Type:** Integer | +| `with_runtime.lte` | Filter and only include movies that have a runtime that is less than or equal to a value.
**Type:** Integer | +| `with_original_language` | Specify an ISO 639-1 string to filter results by their original language value.
**Type:** String | +| `with_title_translation` | Specify a language/country string to filter the results by if the item has a type of title translation.
**Type:** String
**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.
**Type:** String
**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.
**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.
**Type:** String
**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.
**Type:** String
**Values:** `flatrate`, `free`, `ads`, `rent`, `buy` | diff --git a/modules/anilist.py b/modules/anilist.py index 58557a39..2175a295 100644 --- a/modules/anilist.py +++ b/modules/anilist.py @@ -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"]: diff --git a/modules/builder.py b/modules/builder.py index f244f27c..90e541c7 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -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"]: diff --git a/modules/imdb.py b/modules/imdb.py index bc0d86f9..56f70c55 100644 --- a/modules/imdb.py +++ b/modules/imdb.py @@ -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") diff --git a/modules/meta.py b/modules/meta.py index be6b32cd..e912da9f 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -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: diff --git a/modules/operations.py b/modules/operations.py index 3a371909..5d986b40 100644 --- a/modules/operations.py +++ b/modules/operations.py @@ -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: diff --git a/modules/tmdb.py b/modules/tmdb.py index 6fb0f193..c56f9ae1 100644 --- a/modules/tmdb.py +++ b/modules/tmdb.py @@ -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}") diff --git a/modules/util.py b/modules/util.py index 05a084ee..27a8dae6 100644 --- a/modules/util.py +++ b/modules/util.py @@ -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}")