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)