diff --git a/VERSION b/VERSION index 64de7d11..873dd82e 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.20.0-develop10 +1.20.0-develop11 diff --git a/docs/assets/icons/boxofficemojo.png b/docs/assets/icons/boxofficemojo.png new file mode 100644 index 00000000..b623b67c Binary files /dev/null and b/docs/assets/icons/boxofficemojo.png differ diff --git a/docs/files/builders/mojo.md b/docs/files/builders/mojo.md new file mode 100644 index 00000000..f638a509 --- /dev/null +++ b/docs/files/builders/mojo.md @@ -0,0 +1,394 @@ +# BoxOfficeMojo Builders + +You can find items using the lists on [boxofficemojo.com](https://www.boxofficemojo.com/) (BoxOfficeMojo). + +No configuration is required for these builders. + +??? blank "`mojo_domestic` - Uses the Domestic Box Office." + +
Uses the Domestic Box Office to collection items. + +
+ + **Works With:** Movies, Playlists, and Custom Sort + + **Builder Attribute:** `mojo_domestic` + + **Builder Value:** [Dictionary](../../pmm/yaml.md#dictionaries) of Attributes + + ??? blank "`range` - Determines the type of time range of the Box Office" + + Determines the type of the time range of the Box Office. + + **Allowed Values:** `daily`, `weekend`, `weekly`, `monthly`, `quarterly`, `yearly`, `season`, or `holiday` + + ??? blank "`year` - Determines the year of the Box Office" + + Determines the year of the Box Office. This attribute is ignored for the `daily` range. + + **Default Value:** `current` + + **Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`; where + `#` is the number of year before the current) + + ??? blank "`range_data` - Determines the actual time range of the Box Office" + + Determines the actual time range of the Box Office. The input for this value changes depending on the value + of `range`. This attribute is required for all ranges expect the `yearly` range. + + **Daily Allowed Values:** Date in the format `MM-DD-YYYY`, `current`, or relative current (`current-#`; where + `#` is the number of days before the current) + + **Weekend Allowed Values:** Week Number between 1-53, `current`, or relative current (`current-#`; where `#` + is the number of days before the current) + + **Weekly Allowed Values:** Week Number between 1-53, `current`, or relative current (`current-#`; where `#` + is the number of days before the current) + + **Monthly Allowed Values:** `january`, `february`, `march`, `april`, `may`, `june`, `july`, `august`, + `september`, `october`, `november`, `december`, `current`, or relative current (`current-#`; where `#` is the + number of days before the current) + + **Quarterly Allowed Values:** `q1`, `q2`, `q3`, `q4`, `current`, or relative current (`current-#`; where `#` + is the number of days before the current) + + **Season Allowed Values:** `winter`, `spring`, `summer`, `fall`, `holiday`, or `current` + + **Holiday Allowed Values:** `new_years_day`, `new_year_weekend`, `mlk_day`, `mlk_day_weekend`, + `presidents_day`, `presidents_day_weekend`, `easter`, `easter_weekend`, `memorial_day`, `memorial_day_weekend`, + `independence_day`, `independence_day_weekend`, `labor_day`, `labor_day_weekend`, `indigenous_day`, + `indigenous_day_weekend`, `halloween`, `thanksgiving`, `thanksgiving_3`, `thanksgiving_4`, `thanksgiving_5`, + `post_thanksgiving_weekend`, `christmas_day`, `christmas_weekend`, or `new_years_eve` + + ??? blank "`limit` - The maximum number of result to return" + + This determines the maximum number of results to return. If there are less results then the limit then all will + be returned. + + **Default Value:** Returns all results + + **Allowed Values:** Number greter than 0 + + ???+ example "Example" + + ```yaml + collections: + + Current Domestic Box Office: + mojo_domestic: + range: yearly + year: current + + Last Year's Domestic Box Office: + mojo_domestic: + range: yearly + year: current-1 + + Last Months Top 10 Domestic Box Office: + mojo_domestic: + range: monthly + year: current-1 + limit: 10 + ``` + +??? blank "`mojo_international` - Uses the International Box Office." + +
Uses the International Box Office to collection items. + +
+ + **Works With:** Movies, Playlists, and Custom Sort + + **Builder Attribute:** `mojo_international` + + **Builder Value:** [Dictionary](../../pmm/yaml.md#dictionaries) of Attributes + + ??? blank "`range` - Determines the type of time range of the Box Office" + + Determines the type of the time range of the Box Office. + + **Allowed Values:** `weekend`, `monthly`, `quarterly`, or `yearly` + + ??? blank "`chart` - Determines the chart you want to use" + + Determines the chart you want to use. + + **Default Value:** `international` + + **Allowed Values:** Item in the drop down found [here](https://www.boxofficemojo.com/intl/) + + ??? blank "`year` - Determines the year of the Box Office" + + Determines the year of the Box Office. + + **Default Value:** `current` + + **Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`; where + `#` is the number of year before the current) + + ??? blank "`range_data` - Determines the actual time range of the Box Office" + + Determines the actual time range of the Box Office. The input for this value changes depending on the value + of `range`. This attribute is required for all ranges expect the `yearly` range. + + **Weekend Allowed Values:** Week Number between 1-53, `current`, or relative current (`current-#`; where `#` + is the number of days before the current) + + **Monthly Allowed Values:** `january`, `february`, `march`, `april`, `may`, `june`, `july`, `august`, + `september`, `october`, `november`, `december`, `current`, or relative current (`current-#`; where `#` is the + number of days before the current) + + **Quarterly Allowed Values:** `q1`, `q2`, `q3`, `q4`, `current`, or relative current (`current-#`; where `#` + is the number of days before the current) + + ??? blank "`limit` - The maximum number of result to return" + + This determines the maximum number of results to return. If there are less results then the limit then all will + be returned. + + **Default Value:** Returns all results + + **Allowed Values:** Number greter than 0 + + ???+ example "Example" + + ```yaml + collections: + + Current International Box Office: + mojo_international: + range: yearly + year: current + + Last Year's International Box Office: + mojo_international: + range: yearly + year: current-1 + + Last Months Top 10 German Box Office: + mojo_international: + range: monthly + chart: germany + year: current-1 + limit: 10 + ``` + +??? blank "`mojo_world` - Uses the Worldwide Box Office." + +
Uses the [Worldwide Box Office](https://www.boxofficemojo.com/year/world/) to collection items. + +
+ + **Works With:** Movies, Playlists, and Custom Sort + + **Builder Attribute:** `mojo_world` + + **Builder Value:** [Dictionary](../../pmm/yaml.md#dictionaries) of Attributes + + ??? blank "`year` - The year of the Worldwide Box Office" + + This determines the year of the [Worldwide Box Office](https://www.boxofficemojo.com/year/world/) to pull. + + **Allowed Values:** Number between 1977 and the current year, `current`, or relative current (`current-#`; where + `#` is the number of year before the current) + + ??? blank "`limit` - The maximum number of result to return" + + This determines the maximum number of results to return. If there are less results then the limit then all will + be returned. + + **Default Value:** Returns all results + + **Allowed Values:** Number greter than 0 + + ???+ example "Example" + + ```yaml + collections: + + Current Worlwide Box Office: + mojo_world: + year: current + + Last Year's Worlwide Box Office: + mojo_world: + year: current-1 + + 2020 Top 10 Worlwide Box Office: + mojo_world: + year: 2020 + limit: 10 + ``` + +??? blank "`mojo_all_time` - Uses the All Time Lists." + +
Uses the [All Time Lists](https://www.boxofficemojo.com/charts/overall/) to collection items. + +
+ + **Works With:** Movies, Playlists, and Custom Sort + + **Builder Attribute:** `mojo_all_time` + + **Builder Value:** [Dictionary](../../pmm/yaml.md#dictionaries) of Attributes + + ??? blank "`chart` - Determines the chart you want to use" + + Determines the chart you want to use. + + **Allowed Values:** `domestic` or `worldwide` + + ??? blank "`content_rating_filter` - Determines the content rating chart to use" + + Determines the content rating chart to use. + + **Allowed Values:** `g`, `g/pg`, `pg`, `pg-13`, `r` or `nc-17` + + ??? blank "`limit` - The maximum number of result to return" + + This determines the maximum number of results to return. If there are less results then the limit then all will + be returned. + + **Default Value:** Returns all results + + **Allowed Values:** Number greter than 0 + + ???+ example "Example" + + ```yaml + collections: + + Top 100 Domestic All Time Grosses: + mojo_all_time: + chart: domestic + limit: 100 + + Top 100 Worldwide All Time Grosses: + mojo_all_time: + chart: worldwide + limit: 100 + + Top 10 Domestic All Time G Movie Grosses: + mojo_world: + chart: domestic + content_rating_filter: g + limit: 10 + ``` + +??? blank "`mojo_never` - Uses the Never Hit Lists." + +
Uses the [Never Hit Lists](https://www.boxofficemojo.com/charts/overall/) (Bottom Section) to + collection items. + +
+ + **Works With:** Movies, Playlists, and Custom Sort + + **Builder Attribute:** `mojo_never` + + **Builder Value:** [Dictionary](../../pmm/yaml.md#dictionaries) of Attributes + + ??? blank "`chart` - Determines the chart you want to use" + + Determines the chart you want to use. + + **Allowed Values:** Item in the drop down found [here](https://www.boxofficemojo.com/charts/overall/) + + ??? blank "`never` - Determines the never filter to use" + + Determines the never filter to use. + + **Default Value:** `1` + + **Allowed Values:** `1`, `5`, or `10` + + ??? blank "`limit` - The maximum number of result to return" + + This determines the maximum number of results to return. If there are less results then the limit then all will + be returned. + + **Default Value:** Returns all results + + **Allowed Values:** Number greter than 0 + + ???+ example "Example" + + ```yaml + collections: + + Top 100 Domestic Never #1: + mojo_never: + chart: domestic + limit: 100 + + Top 100 Domestic Never #10: + mojo_never: + chart: domestic + never: 10 + limit: 100 + + Top 100 German Never #1: + mojo_never: + chart: germany + limit: 100 + ``` + +??? blank "`mojo_record` - Uses other Record Lists." + +
Uses the [Weekend Records](https://www.boxofficemojo.com/charts/weekend/), + [Daily Records](https://www.boxofficemojo.com/charts/daily/), and + [Miscellaneous Records](https://www.boxofficemojo.com/charts/misc/) to collection items. + +
+ + **Works With:** Movies, Playlists, and Custom Sort + + **Builder Attribute:** `mojo_record` + + **Builder Value:** [Dictionary](../../pmm/yaml.md#dictionaries) of Attributes + + ??? blank "`chart` - Determines the record you want to use" + + Determines the chart you want to use. + + **Allowed Values:** `second_weekend_drop`, `post_thanksgiving_weekend_drop`, `top_opening_weekend`, + `worst_opening_weekend_theater_avg`, `mlk_opening`, `easter_opening`, `memorial_opening`, `labor_opening`, + `president_opening`, `thanksgiving_3_opening`, `thanksgiving_5_opening`, `mlk`, `easter`, `4th`, `memorial`, + `labor`, `president`, `thanksgiving_3`, `thanksgiving_5`, `january`, `february`, `march`, `april`, `may`, + `june`, `july`, `august`, `september`, `october`, `november`, `december`, `spring`, `summer`, `fall`, + `holiday_season`, `winter`, `g`, `g/pg`, `pg`, `pg-13`, `r`, `nc-17`, `top_opening_weekend_theater_avg_all`, + `top_opening_weekend_theater_avg_wide`, `opening_day`, `single_day_grosses`, `christmas_day_gross`, + `new_years_day_gross`, `friday`, `saturday`, `sunday`, `monday`, `tuesday`, `wednesday`, `thursday`, + `friday_non_opening`, `saturday_non_opening`, `sunday_non_opening`, `monday_non_opening`, `tuesday_non_opening`, + `wednesday_non_opening`, `thursday_non_opening`, `biggest_theater_drop`, or `opening_week` + + + ??? blank "`limit` - The maximum number of result to return" + + This determines the maximum number of results to return. If there are less results then the limit then all will + be returned. + + **Default Value:** Returns all results + + **Allowed Values:** Number greter than 0 + + ???+ example "Example" + + ```yaml + collections: + + Top 10 Biggest Opening Weekends: + mojo_record: + chart: top_opening_weekend + limit: 10 + + Top 10 Biggest Opening Day: + mojo_record: + chart: opening_day + limit: 10 + + Top 10 Biggest Opening Weeks: + mojo_record: + chart: opening_week + limit: 10 + ``` diff --git a/docs/files/builders/overview.md b/docs/files/builders/overview.md index d77300fb..aad70d65 100644 --- a/docs/files/builders/overview.md +++ b/docs/files/builders/overview.md @@ -30,6 +30,11 @@ Builders use third-party services to source items to be added to the collection. image: ../../../assets/icons/imdb.png url: "../imdb" +- title: Box Office Mojo + content: Grabs items based on metadata and lists on Boxofficemojo.com + image: ../../../assets/icons/boxofficemojo.png + url: "../boxofficemojo" + - title: Trakt content: Grabs items based on metadata and lists on Trakt.tv image: ../../../assets/icons/trakt.png diff --git a/mkdocs.yml b/mkdocs.yml index 01aea07f..7828c387 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -319,6 +319,7 @@ nav: - Letterboxd Builders: files/builders/letterboxd.md - ICheckMovies Builders: files/builders/icheckmovies.md - FlixPatrol Builders: files/builders/flixpatrol.md + - BoxOfficeMojo Builders: files/builders/mojo.md - Reciperr Builders: files/builders/reciperr.md - StevenLu Builders: files/builders/stevenlu.md - AniDB Builders: files/builders/anidb.md diff --git a/modules/builder.py b/modules/builder.py index 7e16fffa..e1ee2ca3 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -1,7 +1,8 @@ import os, re, time from arrapi import ArrException -from datetime import datetime -from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, plex, radarr, reciperr, sonarr, tautulli, tmdb, trakt, tvdb, mdblist, util +from datetime import datetime, timedelta +from dateutil.relativedelta import relativedelta +from modules import anidb, anilist, flixpatrol, icheckmovies, imdb, letterboxd, mal, mojo, plex, radarr, reciperr, sonarr, tautulli, tmdb, trakt, tvdb, mdblist, util from modules.util import Failed, FilterFailed, NonExisting, NotScheduled, NotScheduledRange, Deleted from modules.overlay import Overlay from modules.poster import PMMImage @@ -16,7 +17,7 @@ logger = util.logger advance_new_agent = ["item_metadata_language", "item_use_original_title"] advance_show = ["item_episode_sorting", "item_keep_episodes", "item_delete_episodes", "item_season_display", "item_episode_sorting"] all_builders = anidb.builders + anilist.builders + flixpatrol.builders + icheckmovies.builders + imdb.builders + \ - letterboxd.builders + mal.builders + plex.builders + reciperr.builders + tautulli.builders + \ + letterboxd.builders + mal.builders + mojo.builders + plex.builders + reciperr.builders + tautulli.builders + \ tmdb.builders + trakt.builders + tvdb.builders + mdblist.builders + radarr.builders + sonarr.builders show_only_builders = [ "tmdb_network", "tmdb_show", "tmdb_show_details", "tvdb_show", "tvdb_show_details", "tmdb_airing_today", @@ -25,7 +26,8 @@ show_only_builders = [ movie_only_builders = [ "letterboxd_list", "letterboxd_list_details", "icheckmovies_list", "icheckmovies_list_details", "stevenlu_popular", "tmdb_collection", "tmdb_collection_details", "tmdb_movie", "tmdb_movie_details", "tmdb_now_playing", "item_edition", - "tvdb_movie", "tvdb_movie_details", "tmdb_upcoming", "trakt_boxoffice", "reciperr_list", "radarr_all", "radarr_taglist" + "tvdb_movie", "tvdb_movie_details", "tmdb_upcoming", "trakt_boxoffice", "reciperr_list", "radarr_all", "radarr_taglist", + "mojo_world", "mojo_domestic", "mojo_international", "mojo_record", "mojo_all_time", "mojo_never" ] music_only_builders = ["item_album_sorting"] summary_details = [ @@ -153,7 +155,8 @@ custom_sort_builders = [ "tautulli_popular", "tautulli_watched", "mdblist_list", "letterboxd_list", "icheckmovies_list", "flixpatrol_top", "anilist_top_rated", "anilist_popular", "anilist_trending", "anilist_search", "anilist_userlist", "mal_all", "mal_airing", "mal_upcoming", "mal_tv", "mal_movie", "mal_ova", "mal_special", "mal_search", - "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio" + "mal_popular", "mal_favorite", "mal_suggested", "mal_userlist", "mal_season", "mal_genre", "mal_studio", + "mojo_world", "mojo_domestic", "mojo_international", "mojo_record", "mojo_all_time", "mojo_never" ] episode_parts_only = ["plex_pilots"] overlay_only = ["overlay", "suppress_overlays"] @@ -1037,6 +1040,8 @@ class CollectionBuilder: self._imdb(method_name, method_data) elif method_name in mal.builders: self._mal(method_name, method_data) + elif method_name in mojo.builders: + self._mojo(method_name, method_data) elif method_name in plex.builders or method_final in plex.searches: self._plex(method_name, method_data) elif method_name in reciperr.builders: @@ -1800,6 +1805,127 @@ class CollectionBuilder: final_text = f"MyAnimeList Search\n{method_name[4:].capitalize()}: {' or '.join([str(all_items[i]) for i in final_items])}" self.builders.append(("mal_search", ({"genres" if method_name == "mal_genre" else "producers": ",".join(final_items)}, final_text, 0))) + def _mojo(self, method_name, method_data): + for dict_data in util.parse(self.Type, method_name, method_data, datatype="listdict"): + dict_methods = {dm.lower(): dm for dm in dict_data} + final = {} + if method_name == "mojo_record": + final["chart"] = util.parse(self.Type, "chart", dict_data, methods=dict_methods, parent=method_name, options=mojo.top_options) + elif method_name == "mojo_world": + if "year" not in dict_methods: + raise Failed(f"{self.Type} Error: {method_name} year attribute not found") + og_year = dict_data[dict_methods["year"]] + if not og_year: + raise Failed(f"{self.Type} Error: {method_name} year attribute is blank") + if og_year == "current": + final["year"] = str(self.current_year) # noqa + elif str(og_year).startswith("current-"): + try: + final["year"] = str(self.current_year - int(og_year.split("-")[1])) # noqa + if final["year"] not in mojo.year_options: + raise Failed(f"{self.Type} Error: {method_name} year attribute final value must be 1977 or greater: {og_year}") + except ValueError: + raise Failed(f"{self.Type} Error: {method_name} year attribute invalid: {og_year}") + else: + final["year"] = util.parse(self.Type, "year", dict_data, methods=dict_methods, parent=method_name, options=mojo.year_options) + elif method_name == "mojo_all_time": + final["chart"] = util.parse(self.Type, "chart", dict_data, methods=dict_methods, parent=method_name, options=mojo.chart_options) + final["content_rating_filter"] = util.parse(self.Type, "content_rating_filter", dict_data, methods=dict_methods, parent=method_name, options=mojo.content_rating_options) if "content_rating_filter" in dict_methods else None + elif method_name == "mojo_never": + final["chart"] = util.parse(self.Type, "chart", dict_data, methods=dict_methods, parent=method_name, default="domestic", options=self.config.BoxOfficeMojo.never_options) + final["never"] = str(util.parse(self.Type, "never", dict_data, methods=dict_methods, parent=method_name, default="1", options=mojo.never_in_options)) if "never" in dict_methods else "1" + elif method_name in ["mojo_domestic", "mojo_international"]: + dome = method_name == "mojo_domestic" + final["range"] = util.parse(self.Type, "range", dict_data, methods=dict_methods, parent=method_name, options=mojo.dome_range_options if dome else mojo.intl_range_options) + if not dome: + final["chart"] = util.parse(self.Type, "chart", dict_data, methods=dict_methods, parent=method_name, default="international", options=self.config.BoxOfficeMojo.intl_options) + chart_date = self.current_time + if final["range"] != "daily": + _m = "range_data" if final["range"] == "yearly" and "year" not in dict_methods and "range_data" in dict_methods else "year" + if _m not in dict_methods: + raise Failed(f"{self.Type} Error: {method_name} {_m} attribute not found") + og_year = dict_data[dict_methods[_m]] + if not og_year: + raise Failed(f"{self.Type} Error: {method_name} {_m} attribute is blank") + if str(og_year).startswith("current-"): + try: + chart_date = self.current_time - relativedelta(years=int(og_year.split("-")[1])) + except ValueError: + raise Failed(f"{self.Type} Error: {method_name} {_m} attribute invalid: {og_year}") + else: + _y = util.parse(self.Type, _m, dict_data, methods=dict_methods, parent=method_name, default="current", options=mojo.year_options) + if _y != "current": + chart_date = self.current_time - relativedelta(years=self.current_time.year - _y) + if final["range"] != "yearly": + if "range_data" not in dict_methods: + raise Failed(f"{self.Type} Error: {method_name} range_data attribute not found") + og_data = dict_data[dict_methods["range_data"]] + if not og_data: + raise Failed(f"{self.Type} Error: {method_name} range_data attribute is blank") + + if final["range"] == "holiday": + final["range_data"] = util.parse(self.Type, "range_data", dict_data, methods=dict_methods, parent=method_name, options=mojo.holiday_options) + elif final["range"] == "daily": + if og_data == "current": + final["range_data"] = datetime.strftime(self.current_time, "%Y-%m-%d") # noqa + elif str(og_data).startswith("current-"): + try: + final["range_data"] = datetime.strftime(self.current_time - timedelta(days=int(og_data.split("-")[1])), "%Y-%m-%d") # noqa + except ValueError: + raise Failed(f"{self.Type} Error: {method_name} range_data attribute invalid: {og_data}") + else: + final["range_data"] = util.parse(self.Type, "range_data", dict_data, methods=dict_methods, parent=method_name, default="current", datatype="date", date_return="%Y-%m-%d") + if final["range_data"] == "current": + final["range_data"] = datetime.strftime(self.current_time, "%Y-%m-%d") # noqa + elif final["range"] in ["weekend", "weekly"]: + if str(og_data).startswith("current-"): + try: + final_date = chart_date - timedelta(weeks=int(og_data.split("-")[1])) + final_iso = final_date.isocalendar() + final["range_data"] = final_iso.week + final["year"] = final_iso.year + except ValueError: + raise Failed(f"{self.Type} Error: {method_name} range_data attribute invalid: {og_data}") + else: + _v = util.parse(self.Type, "range_data", dict_data, methods=dict_methods, parent=method_name, default="current", options=["current"] + [str(i) for i in range(1, 54)]) + current_iso = chart_date.isocalendar() + final["range_data"] = current_iso.week if _v == "current" else _v + final["year"] = current_iso.year + elif final["range"] == "monthly": + if str(og_data).startswith("current-"): + try: + final_date = chart_date - relativedelta(months=int(og_data.split("-")[1])) + final["range_data"] = final_date.month + final["year"] = final_date.year + except ValueError: + raise Failed(f"{self.Type} Error: {method_name} range_data attribute invalid: {og_data}") + else: + _v = util.parse(self.Type, "range_data", dict_data, methods=dict_methods, parent=method_name, default="current", options=["current"] + util.lower_months) + final["range_data"] = chart_date.month if _v == "current" else util.lower_months[_v] + elif final["range"] == "quarterly": + if str(og_data).startswith("current-"): + try: + final_date = chart_date - relativedelta(months=int(og_data.split("-")[1]) * 3) + final["range_data"] = mojo.quarters[final_date.month] + final["year"] = final_date.year + except ValueError: + raise Failed(f"{self.Type} Error: {method_name} range_data attribute invalid: {og_data}") + else: + _v = util.parse(self.Type, "range_data", dict_data, methods=dict_methods, parent=method_name, default="current", options=mojo.quarter_options) + final["range_data"] = mojo.quarters[chart_date.month] if _v == "current" else _v + elif final["range"] == "season": + _v = util.parse(self.Type, "range_data", dict_data, methods=dict_methods, parent=method_name, default="current", options=mojo.season_options) + final["range_data"] = mojo.seasons[chart_date.month] if _v == "current" else _v + else: + final["range_data"] = chart_date.year + if "year" not in final: + final["year"] = chart_date.year + if final["year"] < 1977: + raise Failed(f"{self.Type} Error: {method_name} attribute final date value must be on year 1977 or greater: {final['year']}") + + final["limit"] = util.parse(self.Type, "limit", dict_data, methods=dict_methods, parent=method_name, default=0, datatype="int", maximum=1000) if "limit" in dict_methods else 0 + self.builders.append((method_name, final)) + def _plex(self, method_name, method_data): if method_name in ["plex_all", "plex_pilots"]: self.builders.append((method_name, self.builder_level)) @@ -2088,6 +2214,8 @@ class CollectionBuilder: ids = self.config.Letterboxd.get_tmdb_ids(method, value, self.language) elif "reciperr" in method or "stevenlu" in method: ids = self.config.Reciperr.get_imdb_ids(method, value) + elif "mojo" in method: + ids = self.config.BoxOfficeMojo.get_imdb_ids(method, value) elif "mdblist" in method: ids = self.config.Mdblist.get_tmdb_ids(method, value, self.library.is_movie if not self.playlist else None) elif "tmdb" in method: diff --git a/modules/cache.py b/modules/cache.py index f0b373ff..ae33f779 100644 --- a/modules/cache.py +++ b/modules/cache.py @@ -72,6 +72,13 @@ class Cache: tmdb_id TEXT, expiration_date TEXT)""" ) + cursor.execute( + """CREATE TABLE IF NOT EXISTS mojo_map ( + key INTEGER PRIMARY KEY, + mojo_url TEXT UNIQUE, + imdb_id TEXT, + expiration_date TEXT)""" + ) cursor.execute( """CREATE TABLE IF NOT EXISTS omdb_data3 ( key INTEGER PRIMARY KEY, @@ -377,6 +384,12 @@ class Cache: def update_letterboxd_map(self, expired, letterboxd_id, tmdb_id): self._update_map("letterboxd_map", "letterboxd_id", letterboxd_id, "tmdb_id", tmdb_id, expired) + def query_mojo_map(self, mojo_url): + return self._query_map("mojo_map", mojo_url, "mojo_url", "imdb_id") + + def update_mojo_map(self, expired, mojo_url, imdb_id): + self._update_map("mojo_map", "mojo_url", mojo_url, "imdb_id", imdb_id, expired) + def _query_map(self, map_name, _id, from_id, to_id, media_type=None, return_type=False): id_to_return = None expired = None diff --git a/modules/config.py b/modules/config.py index 4f235e58..d25fe645 100644 --- a/modules/config.py +++ b/modules/config.py @@ -14,6 +14,7 @@ from modules.github import GitHub from modules.letterboxd import Letterboxd from modules.mal import MyAnimeList from modules.meta import PlaylistFile +from modules.mojo import BoxOfficeMojo from modules.notifiarr import Notifiarr from modules.omdb import OMDb from modules.overlays import Overlays @@ -704,6 +705,7 @@ class ConfigFile: self.FlixPatrol = FlixPatrol(self) self.ICheckMovies = ICheckMovies(self) self.Letterboxd = Letterboxd(self) + self.BoxOfficeMojo = BoxOfficeMojo(self) self.Reciperr = Reciperr(self) self.Ergast = Ergast(self) diff --git a/modules/mojo.py b/modules/mojo.py new file mode 100644 index 00000000..76a2b4c7 --- /dev/null +++ b/modules/mojo.py @@ -0,0 +1,273 @@ +from datetime import datetime +from modules import util +from modules.util import Failed +from num2words import num2words +from urllib.parse import urlparse, parse_qs + +logger = util.logger + +builders = ["mojo_world", "mojo_domestic", "mojo_international", "mojo_record", "mojo_all_time", "mojo_never"] +top_options = { + "second_weekend_drop": ("Biggest Second Weekend Drops", "/chart/biggest_second_weekend_gross_drop/", None), + "post_thanksgiving_weekend_drop": ("Largest Post-Thanksgiving Weekend Drops", "/chart/post_thanksgiving_weekend_drop/", None), + "top_opening_weekend": ("Top Opening Weekends", "/chart/top_opening_weekend/", None), + "worst_opening_weekend_theater_avg": ("Worst Opening Weekend Per-Theater Averages", "/chart/btm_wide_opening_weekend_theater_avg/", None), + "top_opening_weekend_theater_avg_all": ("Top Opening Theater Averages", "/chart/top_opening_weekend_theater_avg/", {"by_release_scale": "all"}), + "top_opening_weekend_theater_avg_wide": ("Top Wide Opening Theater Averages", "/chart/top_opening_weekend_theater_avg/", {"by_release_scale": "wide"}), + "january": ("Top Opening Weekend in January", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "january"}), + "february": ("Top Opening Weekend in February", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "february"}), + "march": ("Top Opening Weekend in March", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "march"}), + "april": ("Top Opening Weekend in April", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "april"}), + "may": ("Top Opening Weekend in May", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "may"}), + "june": ("Top Opening Weekend in June", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "june"}), + "july": ("Top Opening Weekend in July", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "july"}), + "august": ("Top Opening Weekend in August", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "august"}), + "september": ("Top Opening Weekend in September", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "september"}), + "october": ("Top Opening Weekend in October", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "october"}), + "november": ("Top Opening Weekend in November", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "november"}), + "december": ("Top Opening Weekend in December", "/chart/release_top_opn_wkd_in_month/", {"in_occasion": "december"}), + "spring": ("Top Opening Weekend in Spring", "/chart/release_top_opn_wkd_in_season/", {"in_occasion": "spring"}), + "summer": ("Top Opening Weekend in Summer", "/chart/release_top_opn_wkd_in_season/", {"in_occasion": "summer"}), + "fall": ("Top Opening Weekend in Fall", "/chart/release_top_opn_wkd_in_season/", {"in_occasion": "fall"}), + "holiday_season": ("Top Opening Weekend in The Holiday Season", "/chart/release_top_opn_wkd_in_season/", {"in_occasion": "holiday_season"}), + "winter": ("Top Opening Weekend in Winter", "/chart/release_top_opn_wkd_in_season/", {"in_occasion": "winter"}), + "g": ("Top Opening Weekend for G Ratings", "/chart/top_opening_wknd_by_mpaa/", {"by_mpaa": "G"}), + "g/pg": ("Top Opening Weekend for G/PG Ratings", "/chart/top_opening_wknd_by_mpaa/", {"by_mpaa": "G%2FPG"}), + "pg": ("Top Opening Weekend for PG Ratings", "/chart/top_opening_wknd_by_mpaa/", {"by_mpaa": "PG"}), + "pg-13": ("Top Opening Weekend for PG-13 Ratings", "/chart/top_opening_wknd_by_mpaa/", {"by_mpaa": "PG-13"}), + "r": ("Top Opening Weekend for R Ratings", "/chart/top_opening_wknd_by_mpaa/", {"by_mpaa": "R"}), + "nc-17": ("Top Opening Weekend for NC-17 Ratings", "/chart/top_opening_wknd_by_mpaa/", {"by_mpaa": "NC-17"}), + "mlk": ("Top Weekend for MLK Day", "/chart/release_top_weekend_gross/", {"by_occasion", "us_mlkday_weekend"}), + "easter": ("Top Weekend for Easter", "/chart/release_top_weekend_gross/", {"by_occasion", "easter_weekend"}), + "4th": ("Top Weekend for the 4th of July", "/chart/release_top_weekend_gross/", {"by_occasion", "us_july4_weekend"}), + "memorial": ("Top Weekend for Memorial Day", "/chart/release_top_weekend_gross/", {"by_occasion", "us_memorialday_weekend"}), + "labor": ("Top Weekend for Labor Day", "/chart/release_top_weekend_gross/", {"by_occasion", "us_laborday_weekend"}), + "president": ("Top Weekend for President's Day", "/chart/release_top_weekend_gross/", {"by_occasion", "us_presidentsday_weekend"}), + "thanksgiving_3": ("Top 3 Day Weekend for Thanksgiving", "/chart/release_top_weekend_gross/", {"by_occasion", "us_thanksgiving_3"}), + "thanksgiving_5": ("Top 5 Day Weekend for Thanksgiving", "/chart/release_top_weekend_gross/", {"by_occasion", "us_thanksgiving_5"}), + "mlk_opening": ("Top Opening Weekend for MLK Day", "/chart/top_opening_holiday_weekends/", {"by_occasion", "us_mlkday_weekend"}), + "easter_opening": ("Top Opening Weekend for Easter", "/chart/top_opening_holiday_weekends/", {"by_occasion", "easter_weekend"}), + "memorial_opening": ("Top Opening Weekend for Memorial Day", "/chart/top_opening_holiday_weekends/", {"by_occasion", "us_memorialday_weekend"}), + "labor_opening": ("Top Opening Weekend for Labor Day", "/chart/top_opening_holiday_weekends/", {"by_occasion", "us_laborday_weekend"}), + "president_opening": ("Top Opening Weekend for MLK Day", "/chart/top_opening_holiday_weekends/", {"by_occasion", "us_presidentsday_weekend"}), + "thanksgiving_3_opening": ("Top 3 Day Opening Weekend for Thanksgiving", "/chart/top_thanksgiving_openings/", {"by_occasion", "us_thanksgiving_3"}), + "thanksgiving_5_opening": ("Top 5 Day Opening Weekend for Thanksgiving", "/chart/top_thanksgiving_openings/", {"by_occasion", "us_thanksgiving_5"}), + "opening_week": ("Top Opening Week", "/chart/top_opening_week/", None), + "biggest_theater_drop": ("Biggest Theater Drops", "/chart/biggest_third_weekend_num_theaters_drop/", None), + "opening_day": ("Top Opening Day", "/chart/top_opening_day/", None), + "single_day_grosses": ("Top Day", "/chart/release_top_daily_gross/", None), + "christmas_day_gross": ("Top Christmas Day", "/chart/release_top_holiday_gross/", {"by_occasion": "christmas_day"}), + "new_years_day_gross": ("Top New Years Day", "/chart/release_top_holiday_gross/", {"by_occasion": "newyearsday"}), + "friday": ("Top Friday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "friday"}), + "saturday": ("Top Saturday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "saturday"}), + "sunday": ("Top Sunday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "sunday"}), + "monday": ("Top Monday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "monday"}), + "tuesday": ("Top Tuesday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "tuesday"}), + "wednesday": ("Top Wednesday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "wednesday"}), + "thursday": ("Top Thursday", "/chart/release_top_daily_gross_by_dow/", {"by_occasion": "thursday"}), + "friday_non_opening": ("Top Friday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "friday"}), + "saturday_non_opening": ("Top Saturday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "saturday"}), + "sunday_non_opening": ("Top Sunday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "sunday"}), + "monday_non_opening": ("Top Monday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "monday"}), + "tuesday_non_opening": ("Top Tuesday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "tuesday"}), + "wednesday_non_opening": ("Top Wednesday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "wednesday"}), + "thursday_non_opening": ("Top Thursday Non-Opening", "/chart/top_non_opening_by_dow/", {"by_occasion": "thursday"}), +} +chart_options = ["domestic", "worldwide"] +content_rating_options = { + "g": "G", + "g/pg": "G%2FPG", + "pg": "PG", + "pg-13": "PG-13", + "r": "R", + "nc-17": "NC-17", +} +never_in_options = { + "1": ("#1", "never_1"), + "5": ("the Top 5", "never_5"), + "10": ("the Top 10", "never_10"), +} +intl_range_options = ["weekend", "monthly", "quarterly", "yearly"] +dome_range_options = intl_range_options + ["daily", "weekly", "season", "holiday"] +year_options = ["current"] + [str(t) for t in range(1977, datetime.now().year + 1)] +quarter_options = ["current", "q1", "q2", "q3", "q4"] +quarters = {1: "q1", 2: "q1", 3: "q1", 4: "q2", 5: "q2", 6: "q2", 7: "q3", 8: "q3", 9: "q3", 10: "q4", 11: "q4", 12: "q4"} +season_options = ["current", "winter", "spring", "summer", "fall", "holiday"] +seasons = {1: "winter", 2: "winter", 3: "spring", 4: "spring", 5: "summer", 6: "summer", 7: "summer", 8: "summer", 9: "fall", 10: "fall", 11: "holiday", 12: "holiday"} +holiday_options = { + "new_years_day": ("New Year's Day", "newyearsday"), + "new_year_weekend": ("New Year Weekend", "us_newyear_weekend"), + "mlk_day": ("MLK Day", "us_mlkday"), + "mlk_day_weekend": ("MLK Day Weekend", "us_mlkday_weekend"), + "presidents_day": ("President's Day", "us_presidentsday"), + "presidents_day_weekend": ("President's Day Weekend", "us_presidentsday_weekend"), + "easter": ("Easter", "easter_sunday"), + "easter_weekend": ("Easter Weekend", "easter_weekend"), + "memorial_day": ("Memorial Day", "us_memorialday"), + "memorial_day_weekend": ("Memorial Day Weekend", "us_memorialday_weekend"), + "independence_day": ("Independence Day", "us_july4"), + "independence_day_weekend": ("Independence Day Weekend", "us_july4_weekend"), + "labor_day": ("Labor Day", "us_laborday"), + "labor_day_weekend": ("Labor Day Weekend", "us_laborday_weekend"), + "indigenous_day": ("Indigenous People's Day", "us_indig_peoples_day"), + "indigenous_day_weekend": ("", "us_indig_peoples_day_weekend"), + "halloween": ("Halloween", "halloween"), + "thanksgiving": ("Thanksgiving", "us_thanksgiving"), + "thanksgiving_3": ("Thanksgiving Weekend", "us_thanksgiving_3"), + "thanksgiving_4": ("Thanksgiving 4-Day Weekend", "us_thanksgiving_4"), + "thanksgiving_5": ("Thanksgiving 5-Day Weekend", "us_thanksgiving_5"), + "post_thanksgiving_weekend": ("Post-Thanksgiving Weekend", "us_post_thanksgiving_weekend"), + "christmas_day": ("Christmas Day", "christmas_day"), + "christmas_weekend": ("Christmas Weekend", "us_christmas_weekend"), + "new_years_eve": ("New Year's Eve", "newyearseve") +} +base_url = "https://www.boxofficemojo.com" + + +class BoxOfficeMojo: + def __init__(self, config): + self.config = config + self._never_options = None + self._intl_options = None + self._year_options = None + + def _options(self, url, nav_type="area"): + output = {} + options = self._request(url, xpath=f"//select[@id='{nav_type}-navSelector']/option") + for option in options: + query = parse_qs(urlparse(option.xpath("@value")[0]).query) + output[option.xpath("text()")[0].lower()] = query["area"][0] if "area" in query else "" + return output + + @property + def never_options(self): + if self._never_options is None: + self._never_options = self._options("/chart/never_in_top/") + return self._never_options + + @property + def intl_options(self): + if self._intl_options is None: + self._intl_options = self._options("/intl/") + return self._intl_options + + @property + def year_options(self): + if self._year_options is None: + self._year_options = [y for y in self._options("/year/world/", nav_type="year")] + return self._year_options + + def _request(self, url, xpath=None, params=None): + logger.trace(f"URL: {base_url}{url}") + if params: + logger.trace(f"Params: {params}") + response = self.config.get_html(f"{base_url}{url}", headers=util.header(), params=params) + return response.xpath(xpath) if xpath else response + + def _parse_list(self, url, params, limit): + response = self._request(url, params=params) + total_html = response.xpath("//li[contains(@class, 'mojo-pagination-button-center')]/a/text()") + total = int(total_html[0].replace(",", "").split(" ")[2]) if total_html else 0 + if total and (limit < 1 or total < limit): + limit = total + pages = int((limit - 1) / 200) + 1 if total else 0 + for field_name in ["release ", "title", "release_group"]: + output = response.xpath(f"//td[contains(@class, 'mojo-field-type-{field_name}')]/a/@href") + if output: + break + for i in range(1, pages): + response = self._request(url, params={"offset": 200 * i}) + output.extend(response.xpath(f"//td[contains(@class, 'mojo-field-type-{field_name}')]/a/@href")) + if not limit or len(output) < limit: + limit = len(output) + return [i[:i.index("?")] for i in output[:limit]] + + def _imdb(self, url): + response = self._request(url) + imdb_url = response.xpath("//select[@id='releasegroup-picker-navSelector']/option[text()='All Releases']/@value") + if not imdb_url: + raise Failed(f"Mojo Error: IMDb ID not found at {base_url}{url}") + return imdb_url[0][7:-1] + + def get_imdb_ids(self, method, data): + params = None + if method == "mojo_record": + text, url, params = top_options[data["chart"]] + elif method == "mojo_world": + text = f"{data['year']} Worldwide Box Office" + url = f"/year/world/{data['year']}/" + elif method == "mojo_all_time": + text = f"Top Lifetime {data['chart'].capitalize()}" + if data["content_rating_filter"] is None: + url = "/chart/top_lifetime_gross/" if data["chart"] == "domestic" else "/chart/ww_top_lifetime_gross/" + else: + text += f" {data['content_rating_filter'].upper()}" + url = f"/chart/mpaa_title_lifetime_gross/" + params = {"by_mpaa": content_rating_options[data['content_rating_filter']]} + text += " Grosses" + elif method == "mojo_never": + pretty, arg_key = never_in_options[data["never"]] + text = f"Top-Grossing Movies That Never Hit {pretty} {data['chart'].capitalize()}" + url = f"/chart/never_in_top/" + params = {"by_rank_threshold": data["never"]} + if data["chart"] != "domestic": + params["area"] = self.never_options[data["chart"]] + else: + chart = data["chart"].capitalize() if "chart" in data else "Domestic" + + if data["range"] == "daily": + day = datetime.strptime(data["range_data"], "%Y-%m-%d") + day = day.strftime("%b {th}, %Y").replace("{th}", num2words(day.day, to='ordinal_num')) + chart_title = f"{day}" + url = f"/date/{data['range_data']}/" + elif data["range"] == "weekend": + chart_title = f"Weekend {data['range_data']} {data['year']}" + url = f"/weekend/{data['year']}W{data['range_data']:02}/" + elif data["range"] == "weekly": + chart_title = f"Week {data['range_data']} {data['year']}" + url = f"/weekly/{data['year']}W{data['range_data']:02}/" + elif data["range"] == "monthly": + chart_title = f"{data['range_data'].capitalize()} {data['year']}" + url = f"/month/{data['range_data']}/{data['year']}/" + elif data["range"] == "quarterly": + chart_title = f"{data['range_data'].capitalize()} {data['year']}" + url = f"/quarter/{data['range_data']}/{data['year']}/" + elif data["range"] == "season": + chart_title = f"{data['range_data'].capitalize()} {data['year']}" + url = f"/season/{data['range_data']}/{data['year']}/" + elif data["range"] == "holiday": + title, slug = holiday_options[data["range_data"]] + chart_title = f"{title} {data['year']}" + url = f"/holiday/{slug}/{data['year']}/" + else: + chart_title = f"{data['year']}" + url = f"/year/{data['year']}/" + text = f"{chart} Box Office For {chart_title}" + if data["limit"]: + text += f" ({data['limit']})" + logger.info(f"Processing {method.replace('_', ' ').title()}: {text}") + items = self._parse_list(url, params, data["limit"]) + if not items: + raise Failed(f"Mojo Error: No List Items found in {method}: {data}") + ids = [] + total_items = len(items) + for i, item in enumerate(items, 1): + logger.ghost(f"Finding IMDb ID {i}/{total_items}") + if "title" in item: + imdb_id = item[7:-1] + else: + imdb_id = None + expired = None + if self.config.Cache: + imdb_id, expired = self.config.Cache.query_letterboxd_map(item) + if not imdb_id or expired is not False: + try: + imdb_id = self._imdb(item) + except Failed as e: + logger.error(e) + continue + if self.config.Cache: + self.config.Cache.update_letterboxd_map(expired, item, imdb_id) + ids.append((imdb_id, "imdb")) + logger.info(f"Processed {total_items} IMDb IDs") + return ids diff --git a/modules/util.py b/modules/util.py index 924359f5..a7df58de 100644 --- a/modules/util.py +++ b/modules/util.py @@ -76,10 +76,12 @@ mod_displays = { ".gt": "is greater than", ".gte": "is greater than or equal", ".lt": "is less than", ".lte": "is less than or equal", ".regex": "is" } pretty_days = {0: "Monday", 1: "Tuesday", 2: "Wednesday", 3: "Thursday", 4: "Friday", 5: "Saturday", 6: "Sunday"} +lower_days = {v.lower(): k for k, v in pretty_days.items()} pretty_months = { 1: "January", 2: "February", 3: "March", 4: "April", 5: "May", 6: "June", 7: "July", 8: "August", 9: "September", 10: "October", 11: "November", 12: "December" } +lower_months = {v.lower(): k for k, v in pretty_months.items()} seasons = ["current", "winter", "spring", "summer", "fall"] advance_tags_to_edit = { "Movie": ["metadata_language", "use_original_title"], @@ -782,7 +784,7 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa if options is None or (options and (v in options or (datatype == "strlist" and str(v) in options))): final_list.append(str(v) if datatype == "strlist" else v) elif options: - raise Failed(f"{error} Error: {display} {v} is invalid; Options include: {', '.join(options)}") + raise Failed(f"{error} Error: {display} {v} is invalid; Options include: {', '.join([o for o in options])}") return final_list elif datatype == "intlist": if value: @@ -861,7 +863,9 @@ def parse(error, attribute, data, datatype=None, methods=None, parent=None, defa 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) + if default in ["today", "current"]: + default = validate_date(datetime.now(), return_as=date_return) + return validate_date(datetime.now() if data in ["today", "current"] 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 \ diff --git a/requirements.txt b/requirements.txt index d862ae78..c6cd440a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,9 +7,10 @@ pillow==10.2.0 PlexAPI==4.15.7 psutil==5.9.7 python-dotenv==1.0.0 +python-dateutil==2.8.2 requests==2.31.0 retrying==1.3.4 ruamel.yaml==0.18.5 schedule==1.2.1 -tmdbapis==1.2.6 -setuptools==69.0.3 \ No newline at end of file +setuptools==69.0.3 +tmdbapis==1.2.6 \ No newline at end of file