diff --git a/VERSION b/VERSION index 7f052aa5..5aca0cb7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.17.1-develop6 +1.17.1-develop7 diff --git a/docs/config/myanimelist.md b/docs/config/myanimelist.md index 1ed6ecd0..7501f785 100644 --- a/docs/config/myanimelist.md +++ b/docs/config/myanimelist.md @@ -9,6 +9,7 @@ Below is a `mal` mapping example and the full set of attributes: mal: client_id: ################################ client_secret: ################################################################ + localhost_url: authorization: access_token: token_type: @@ -20,6 +21,7 @@ mal: |:----------------|:--------------------------------------|:--------:| | `client_id` | MyAnimeList Application Client ID | ✅ | | `client_secret` | MyAnimeList Application Client Secret | ✅ | +| `localhost_url` | MyAnimeList Authorization URL | ❌ | * All other attributes will be filled in by the script. @@ -37,6 +39,20 @@ mal: 11. You should see `Successfully registered.` followed by a link that says `Return to list` click this link. 12. On this page Click the `Edit` button next to the application you just created. 13. Record the `Client ID` and `Client Secret` found on the application page. +14. Go to this URL but replace `CLIENT_ID` with your Client ID `https://myanimelist.net/v1/oauth2/authorize?response_type=code&client_id=CLIENT_ID&code_challenge=k_UHwN_eHAPQVXiceC-rYGkozKqrJmKxPUIUOBIKo1noq_4XGRVCViP_dGcwB-fkPql8f56mmWj5aWCa2HDeugf6sRvnc9Rjhbb1vKGYLY0IwWsDNXRqXdksaVGJthux` +15. You should see a page that looks like this + ![MAL Details](mal.png) + Click "Allow" +16. You will be taken to a page that will not load. That's fine and expected. + ![Localhost Failure](localhost-fail.png) +17. Copy the URL, which will be `localhost/?code=BLAH` and paste in your config file next to `localhost_url`. + NOTE: If you do not see an error as above but instead get taken to some seemingly random website, you probably have a webserver running on your local computer, probably from some sort of tutorial if you don't recall having set one up. For example, some Docker tutorials have you start up local web servers. + You will need to stop that web server while you're doing this in order to grab that localhost URL. +18. Run PMM and the auth will be completed. + +### Alternative Way Letting PMM make the URL + +You can record just your `client_id` and `client_secret` and pmm will create the url for you described below. * On the first run, the script will walk the user through the OAuth flow by producing a MyAnimeList URL for the user to follow. After following the URL login to MyAnimeList.net and authorize the application by clicking the `Allow` button which will redirect the user to `http://localhost/`. Copy the entire URL and paste it into the script and if the URL is correct then the script will populate the `authorization` sub-attributes to use in subsequent runs. diff --git a/docs/metadata/metadata.md b/docs/metadata/metadata.md index 23a69fad..f327cfb4 100644 --- a/docs/metadata/metadata.md +++ b/docs/metadata/metadata.md @@ -129,15 +129,15 @@ Each metadata requires its own section within the `metadata` attribute. Each ite ```yaml metadata: Godzilla vs. Mechagodzilla II: - # ... details to change for this itwm + # ... details to change for this item Godzilla vs. Megaguirus: - # ... details to change for this itwm + # ... details to change for this item Godzilla vs. Megalon: - # ... details to change for this itwm + # ... details to change for this item Halloween (Rob Zombie): - # ... details to change for this itwm + # ... details to change for this item etc: - # ... details to change for this itwm + # ... details to change for this item ``` ### Title & Year diff --git a/modules/builder.py b/modules/builder.py index f23c7790..7c0bb607 100644 --- a/modules/builder.py +++ b/modules/builder.py @@ -748,6 +748,9 @@ class CollectionBuilder: self.details["collection_mode"] = "hide" self.sync = True + if self.smart_url: + self.sync = False + self.do_missing = not self.config.no_missing and (self.details["show_missing"] or self.details["save_report"] or (self.library.Radarr and self.radarr_details["add_missing"]) or (self.library.Sonarr and self.sonarr_details["add_missing"])) @@ -2635,7 +2638,7 @@ class CollectionBuilder: self.library.delete_user_playlist(self.obj.title, user) except NotFound: pass - new_playlist = self.obj.copyToUser(user) + self.obj.copyToUser(user) logger.info(f"Playlist: {self.name} synced to {user}") def send_notifications(self, playlist=False): diff --git a/modules/config.py b/modules/config.py index a6763f6d..1cb7d98e 100644 --- a/modules/config.py +++ b/modules/config.py @@ -460,6 +460,7 @@ class ConfigFile: self.MyAnimeList = MyAnimeList(self, { "client_id": check_for_attribute(self.data, "client_id", parent="mal", throw=True), "client_secret": check_for_attribute(self.data, "client_secret", parent="mal", throw=True), + "localhost_url": check_for_attribute(self.data, "localhost_url", parent="mal", default_is_none=True), "config_path": self.config_path, "authorization": self.data["mal"]["authorization"] if "authorization" in self.data["mal"] else None }) diff --git a/modules/mal.py b/modules/mal.py index dde73bd1..6b2332bc 100644 --- a/modules/mal.py +++ b/modules/mal.py @@ -41,6 +41,7 @@ search_sorts = ["mal_id", "title", "type", "rating", "start_date", "end_date", " search_combos = [f"{s}.{d}" for s in search_sorts for d in ["desc", "asc"]] base_url = "https://api.myanimelist.net/v2/" jiken_base_url = "https://api.jikan.moe/v4/" +uni_code_verifier = "k_UHwN_eHAPQVXiceC-rYGkozKqrJmKxPUIUOBIKo1noq_4XGRVCViP_dGcwB-fkPql8f56mmWj5aWCa2HDeugf6sRvnc9Rjhbb1vKGYLY0IwWsDNXRqXdksaVGJthux" urls = { "oauth_token": "https://myanimelist.net/v1/oauth2/token", "oauth_authorize": "https://myanimelist.net/v1/oauth2/authorize", @@ -55,6 +56,7 @@ class MyAnimeList: self.config = config self.client_id = params["client_id"] self.client_secret = params["client_secret"] + self.localhost_url = params["localhost_url"] self.config_path = params["config_path"] self.authorization = params["authorization"] logger.secret(self.client_secret) @@ -85,17 +87,21 @@ class MyAnimeList: return self._studios def _authorization(self): - code_verifier = secrets.token_urlsafe(100)[:128] - url = f"{urls['oauth_authorize']}?response_type=code&client_id={self.client_id}&code_challenge={code_verifier}" - logger.info("") - logger.info(f"Navigate to: {url}") - logger.info("") - logger.info("Login and click the Allow option. You will then be redirected to a localhost") - logger.info("url that most likely won't load, which is fine. Copy the URL and paste it below") - webbrowser.open(url, new=2) - try: url = util.logger_input("URL").strip() - except TimeoutExpired: raise Failed("Input Timeout: URL required.") - if not url: raise Failed("MyAnimeList Error: No input MyAnimeList code required.") + if self.localhost_url: + code_verifier = uni_code_verifier + url = self.localhost_url + else: + code_verifier = secrets.token_urlsafe(100)[:128] + url = f"{urls['oauth_authorize']}?response_type=code&client_id={self.client_id}&code_challenge={code_verifier}" + logger.info("") + logger.info(f"Navigate to: {url}") + logger.info("") + logger.info("Login and click the Allow option. You will then be redirected to a localhost") + logger.info("url that most likely won't load, which is fine. Copy the URL and paste it below") + webbrowser.open(url, new=2) + try: url = util.logger_input("URL").strip() + except TimeoutExpired: raise Failed("Input Timeout: URL required.") + if not url: raise Failed("MyAnimeList Error: No input MyAnimeList code required.") match = re.search("code=([^&]+)", str(url)) if not match: raise Failed("MyAnimeList Error: Invalid URL") diff --git a/modules/meta.py b/modules/meta.py index 9bc36851..e01186ac 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -237,17 +237,19 @@ class DataFile: else: return og_txt - for i in range(2): - for option in optional: - if option not in variables and f"<<{option}>>" in str(final_data): - raise Failed - for variable, variable_data in variables.items(): - if (variable == "collection_name" or variable == "playlist_name") and _method in ["radarr_tag", "item_radarr_tag", "sonarr_tag", "item_sonarr_tag"]: - final_data = scan_text(final_data, variable, variable_data.replace(",", "")) - elif variable != "name": - final_data = scan_text(final_data, variable, variable_data) - for dm, dd in default.items(): - final_data = scan_text(final_data, dm, dd) + for i in range(4): + if i == 2: + for dm, dd in default.items(): + final_data = scan_text(final_data, dm, dd) + else: + for option in optional: + if option not in variables and f"<<{option}>>" in str(final_data): + raise Failed + for variable, variable_data in variables.items(): + if (variable == "collection_name" or variable == "playlist_name") and _method in ["radarr_tag", "item_radarr_tag", "sonarr_tag", "item_sonarr_tag"]: + final_data = scan_text(final_data, variable, variable_data.replace(",", "")) + elif variable != "name": + final_data = scan_text(final_data, variable, variable_data) return final_data for method_name, attr_data in template.items():