diff --git a/CHANGELOG b/CHANGELOG index 365807ef..2ac0ffa2 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,32 +1,14 @@ # Requirements Update (requirements will need to be reinstalled) -Updated PlexAPI requirement to 4.15.13 -Update lxml requirement to 5.2.2 -Update requests requirement to 2.32.3 -Update schedule requirement to 1.2.2 -Update setuptools requirement to 70.0.0 # Removed Features # New Features -Checks requirement versions to print a message if one needs to be updated -Added the `mass_added_at_update` operation to mass set the Added At field of Movies and Shows. -Add automated Anime Aggregations for AniDB matching -Added `total_runtime` as a special text overlay variable. -Added `top_tamil`, `top_telugu`, `top_malayalam`, `trending_india`, `trending_tamil`, and `trending_telugu` as options for `imdb_chart` -Adds the `sort_by` attribute to `imdb_list` +Added [`letterboxd_user_lists`](https://kometa.wiki/en/latest/files/dynamic_types/#letterboxd-user-lists) Dynamic Collection Type # Updates -Changed the `overlay_artwork_filetype` Setting to accept `webp_lossy` and `webp_lossless` while the old attribute `webp` will be treated as `webp_lossy`. # Defaults -Added Letterboxd Default [Collections](https://kometa.wiki/en/latest/defaults/chart/letterboxd/) and [Ribbon](https://kometa.wiki/en/latest/defaults/overlays/ribbon/) # Bug Fixes -Fixes #2034 `anilist_userlist` `score` attribute wasn't being validated correctly -Fixes #1367 Error when trying to symlink the logs folder -Fixes #2028 TMDb IDs were being ignored on the report -Fixes a bug when parsing a comma-separated string of ints -Fixes `imdb_chart` only getting 25 results -Fixes `imdb_list` not returning items Various other Minor Fixes \ No newline at end of file diff --git a/VERSION b/VERSION index e9307ca5..3072430c 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.0.2 +2.0.2-build1 diff --git a/docs/files/dynamic_types.md b/docs/files/dynamic_types.md index 7e057c39..7fd569ed 100644 --- a/docs/files/dynamic_types.md +++ b/docs/files/dynamic_types.md @@ -222,6 +222,64 @@ requirements of creating the collection. ending: latest ``` +??? blank "`letterboxd_user_lists` - Collections based on the Lists of Letterboxd Users." + +
Creates collections for each of the Letterboxd lists that the user has created. + +
+ + **`type` Value:** `letterboxd_user_lists` + + **`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes + + ??? blank "`username` - Determines the Usernames to scan for lists." + +
This determines which Usernames are scanned. + + **Allowed Values:** Username or list of Usernames + + ??? blank "`sort_by` - Determines the sort that the lists are returned." + +
Determines the sort that the lists are returned. + + **Allowed Values:** `updated`, `name`, `popularity`, `newest`, `oldest` + + **Default:** `updated` + + ??? blank "`limit` - Determines the number of lists to create collections for." + +
Determines the number of lists to create collections for. (`0` is all lists) + + **Allowed Values:** Number 0 or greater + + **Default:** `0` + + **Valid Library Types:** Movies + + **Key Values:** Letterboxd List URL + + **Key Name Value:** Letterboxd List Title + + **Default `title_format`:** `<>` + + ??? tip "Default Template (click to expand)" + + ```yaml + default_template: + letterboxd_list_details: <> + ``` + + ???+ example "Example" + + ```yaml + dynamic_collections: + Letterboxd User Lists: # This name is the mapping name + type: letterboxd_user_lists + data: + username: thebigpictures + limit: 5 + ``` + ??? blank "`trakt_user_lists` - Collections based on Trakt Lists by users."
Creates collections for each of the Trakt lists for the specified users. Use `me` to diff --git a/modules/letterboxd.py b/modules/letterboxd.py index 17a711ff..a0183660 100644 --- a/modules/letterboxd.py +++ b/modules/letterboxd.py @@ -4,19 +4,30 @@ from modules.util import Failed logger = util.logger +sort_options = { + "name": "by/name/", + "popularity": "by/popular/", + "newest": "by/newest/", + "oldest": "by/oldest/", + "updated": "" +} builders = ["letterboxd_list", "letterboxd_list_details"] base_url = "https://letterboxd.com" class Letterboxd: - def __init__(self, requests, cache): + def __init__(self, requests, cache=None): self.requests = requests self.cache = cache + def _request(self, url, language, xpath=None): + logger.trace(f"URL: {url}") + response = self.requests.get_html(url, language=language) + return response.xpath(xpath) if xpath else response + def _parse_page(self, list_url, language): if "ajax" not in list_url: list_url = list_url.replace("https://letterboxd.com/films", "https://letterboxd.com/films/ajax") - logger.trace(f"URL: {list_url}") - response = self.requests.get_html(list_url, language=language) + response = self._request(list_url, language) letterboxd_ids = response.xpath("//li[contains(@class, 'poster-container') or contains(@class, 'film-detail')]/div/@data-film-id") items = [] for letterboxd_id in letterboxd_ids: @@ -44,19 +55,25 @@ class Letterboxd: return items def _tmdb(self, letterboxd_url, language): - logger.trace(f"URL: {letterboxd_url}") - response = self.requests.get_html(letterboxd_url, language=language) - ids = response.xpath("//a[@data-track-action='TMDb']/@href") + ids = self._request(letterboxd_url, language, "//a[@data-track-action='TMDb']/@href") if len(ids) > 0 and ids[0]: if "themoviedb.org/movie" in ids[0]: return util.regex_first_int(ids[0], "TMDb Movie ID") raise Failed(f"Letterboxd Error: TMDb Movie ID not found in {ids[0]}") raise Failed(f"Letterboxd Error: TMDb Movie ID not found at {letterboxd_url}") + def get_user_lists(self, username, sort, language): + next_page = [f"/{username}/lists/{sort_options[sort]}"] + lists = [] + while next_page: + response = self._request(f"{base_url}{next_page[0]}", language) + sections = response.xpath("//div[@class='film-list-summary']/h2/a") + lists.extend([(f"{base_url}{s.xpath('@href')[0]}", s.xpath("text()")[0]) for s in sections]) + next_page = response.xpath("//div[@class='pagination']/div/a[@class='next']/@href") + return lists + def get_list_description(self, list_url, language): - logger.trace(f"URL: {list_url}") - response = self.requests.get_html(list_url, language=language) - descriptions = response.xpath("//meta[@name='description']/@content") + descriptions = self._request(f"{list_url}", language, xpath="//meta[@name='description']/@content") if len(descriptions) > 0 and len(descriptions[0]) > 0 and "About this list: " in descriptions[0]: return str(descriptions[0]).split("About this list: ")[1] return None diff --git a/modules/meta.py b/modules/meta.py index 1de5195b..ca10615a 100644 --- a/modules/meta.py +++ b/modules/meta.py @@ -1,6 +1,6 @@ import math, operator, os, re from datetime import datetime -from modules import plex, ergast, util +from modules import plex, ergast, util, letterboxd from modules.request import quote from modules.util import Failed, NotScheduled from plexapi.exceptions import NotFound, BadRequest @@ -13,7 +13,7 @@ ms_auto = [ "trakt_liked_lists", "trakt_people_list", "subtitle_language", "audio_language", "resolution", "decade", "imdb_awards" ] auto = { - "Movie": ["tmdb_collection", "edition", "country", "director", "producer", "writer"] + all_auto + ms_auto, + "Movie": ["tmdb_collection", "edition", "country", "director", "producer", "writer", "letterboxd_user_lists"] + all_auto + ms_auto, "Show": ["network", "origin_country", "episode_year"] + all_auto + ms_auto, "Artist": ["mood", "style", "country", "album_genre", "album_mood", "album_style", "track_mood"] + all_auto, "Video": ["country", "content_rating"] + all_auto @@ -33,6 +33,7 @@ default_templates = { "tmdb_collection": {"tmdb_collection_details": "<>", "minimum_items": 2}, "trakt_user_lists": {"trakt_list_details": "<>"}, "trakt_liked_lists": {"trakt_list_details": "<>"}, + "letterboxd_user_lists": {"letterboxd_list_details": "<>"}, "tmdb_popular_people": {"tmdb_person": "<>", "plex_search": {"all": {"actor": "tmdb"}}}, "trakt_people_list": {"tmdb_person": "<>", "plex_search": {"all": {"actor": "tmdb"}}} } @@ -1096,6 +1097,24 @@ class MetadataFile(DataFile): auto_list[k] = v elif auto_type == "trakt_liked_lists": _check_dict(self.config.Trakt.all_liked_lists()) + elif auto_type == "letterboxd_user_lists": + dynamic_data = util.parse("Config", "data", dynamic, parent=map_name, methods=methods, datatype="dict") + if "data" in self.temp_vars: + temp_data = util.parse("Config", "data", self.temp_vars["data"], datatype="dict") + for k, v in temp_data.items(): + dynamic_data[k] = v + letter_methods = {am.lower(): am for am in dynamic_data} + users = util.parse("Config", "username", dynamic_data, parent=f"{map_name} data", methods=letter_methods, datatype="strlist") + sort = util.parse("Config", "sort_by", dynamic_data, parent=f"{map_name} data", methods=letter_methods, options=letterboxd.sort_options, default="updated") + limit = util.parse("Config", "limit", dynamic_data, parent=f"{map_name} data", methods=letter_methods, datatype="int", minimum=0, default=0) + final = {} + for user in users: + out = self.config.Letterboxd.get_user_lists(user, sort, self.language) + if limit != 0: + out = out[:limit] + for url, name in out: + final[url] = name + _check_dict(final) elif auto_type == "tmdb_popular_people": if "data" in self.temp_vars: dynamic_data = util.parse("Config", "data", self.temp_vars["data"], datatype="int", minimum=1)