[1] Add Letterboxd Dynamic Collections (#2098)

pull/2103/head
meisnate12 6 months ago committed by GitHub Action
parent de2e1852c5
commit e4c5ef60ab

@ -1,32 +1,14 @@
# Requirements Update (requirements will need to be reinstalled) # 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 # Removed Features
# New Features # New Features
Checks requirement versions to print a message if one needs to be updated Added [`letterboxd_user_lists`](https://kometa.wiki/en/latest/files/dynamic_types/#letterboxd-user-lists) Dynamic Collection Type
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`
# Updates # 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 # 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 # 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 Various other Minor Fixes

@ -1 +1 @@
2.0.2 2.0.2-build1

@ -222,6 +222,64 @@ requirements of creating the collection.
ending: latest ending: latest
``` ```
??? blank "`letterboxd_user_lists` - Collections based on the Lists of Letterboxd Users.<a class="headerlink" href="#letterboxd-user-lists" title="Permanent link"></a>"
<div id="letterboxd-user-lists" />Creates collections for each of the Letterboxd lists that the user has created.
<hr style="margin: 0px;">
**`type` Value:** `letterboxd_user_lists`
**`data` Value:** [Dictionary](../kometa/yaml.md#dictionaries) of Attributes
??? blank "`username` - Determines the Usernames to scan for lists.<a class="headerlink" href="#letterboxd-user-lists-username" title="Permanent link"></a>"
<div id="letterboxd-user-lists-username" />This determines which Usernames are scanned.
**Allowed Values:** Username or list of Usernames
??? blank "`sort_by` - Determines the sort that the lists are returned.<a class="headerlink" href="#letterboxd-user-lists-sort-by" title="Permanent link"></a>"
<div id="letterboxd-user-lists-sort-by" />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.<a class="headerlink" href="#letterboxd-user-lists-limit" title="Permanent link"></a>"
<div id="letterboxd-user-lists-limit" />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`:** `<<key_name>>`
??? tip "Default Template (click to expand)"
```yaml
default_template:
letterboxd_list_details: <<value>>
```
???+ 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.<a class="headerlink" href="#trakt-user-lists" title="Permanent link"></a>" ??? blank "`trakt_user_lists` - Collections based on Trakt Lists by users.<a class="headerlink" href="#trakt-user-lists" title="Permanent link"></a>"
<div id="trakt-user-lists" />Creates collections for each of the Trakt lists for the specified users. Use `me` to <div id="trakt-user-lists" />Creates collections for each of the Trakt lists for the specified users. Use `me` to

@ -4,19 +4,30 @@ from modules.util import Failed
logger = util.logger logger = util.logger
sort_options = {
"name": "by/name/",
"popularity": "by/popular/",
"newest": "by/newest/",
"oldest": "by/oldest/",
"updated": ""
}
builders = ["letterboxd_list", "letterboxd_list_details"] builders = ["letterboxd_list", "letterboxd_list_details"]
base_url = "https://letterboxd.com" base_url = "https://letterboxd.com"
class Letterboxd: class Letterboxd:
def __init__(self, requests, cache): def __init__(self, requests, cache=None):
self.requests = requests self.requests = requests
self.cache = cache 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): def _parse_page(self, list_url, language):
if "ajax" not in list_url: if "ajax" not in list_url:
list_url = list_url.replace("https://letterboxd.com/films", "https://letterboxd.com/films/ajax") list_url = list_url.replace("https://letterboxd.com/films", "https://letterboxd.com/films/ajax")
logger.trace(f"URL: {list_url}") response = self._request(list_url, language)
response = self.requests.get_html(list_url, language=language)
letterboxd_ids = response.xpath("//li[contains(@class, 'poster-container') or contains(@class, 'film-detail')]/div/@data-film-id") letterboxd_ids = response.xpath("//li[contains(@class, 'poster-container') or contains(@class, 'film-detail')]/div/@data-film-id")
items = [] items = []
for letterboxd_id in letterboxd_ids: for letterboxd_id in letterboxd_ids:
@ -44,19 +55,25 @@ class Letterboxd:
return items return items
def _tmdb(self, letterboxd_url, language): def _tmdb(self, letterboxd_url, language):
logger.trace(f"URL: {letterboxd_url}") ids = self._request(letterboxd_url, language, "//a[@data-track-action='TMDb']/@href")
response = self.requests.get_html(letterboxd_url, language=language)
ids = response.xpath("//a[@data-track-action='TMDb']/@href")
if len(ids) > 0 and ids[0]: if len(ids) > 0 and ids[0]:
if "themoviedb.org/movie" in ids[0]: if "themoviedb.org/movie" in ids[0]:
return util.regex_first_int(ids[0], "TMDb Movie ID") 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 in {ids[0]}")
raise Failed(f"Letterboxd Error: TMDb Movie ID not found at {letterboxd_url}") 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): def get_list_description(self, list_url, language):
logger.trace(f"URL: {list_url}") descriptions = self._request(f"{list_url}", language, xpath="//meta[@name='description']/@content")
response = self.requests.get_html(list_url, language=language)
descriptions = response.xpath("//meta[@name='description']/@content")
if len(descriptions) > 0 and len(descriptions[0]) > 0 and "About this list: " in descriptions[0]: 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 str(descriptions[0]).split("About this list: ")[1]
return None return None

@ -1,6 +1,6 @@
import math, operator, os, re import math, operator, os, re
from datetime import datetime from datetime import datetime
from modules import plex, ergast, util from modules import plex, ergast, util, letterboxd
from modules.request import quote from modules.request import quote
from modules.util import Failed, NotScheduled from modules.util import Failed, NotScheduled
from plexapi.exceptions import NotFound, BadRequest 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" "trakt_liked_lists", "trakt_people_list", "subtitle_language", "audio_language", "resolution", "decade", "imdb_awards"
] ]
auto = { 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, "Show": ["network", "origin_country", "episode_year"] + all_auto + ms_auto,
"Artist": ["mood", "style", "country", "album_genre", "album_mood", "album_style", "track_mood"] + all_auto, "Artist": ["mood", "style", "country", "album_genre", "album_mood", "album_style", "track_mood"] + all_auto,
"Video": ["country", "content_rating"] + all_auto "Video": ["country", "content_rating"] + all_auto
@ -33,6 +33,7 @@ default_templates = {
"tmdb_collection": {"tmdb_collection_details": "<<value>>", "minimum_items": 2}, "tmdb_collection": {"tmdb_collection_details": "<<value>>", "minimum_items": 2},
"trakt_user_lists": {"trakt_list_details": "<<value>>"}, "trakt_user_lists": {"trakt_list_details": "<<value>>"},
"trakt_liked_lists": {"trakt_list_details": "<<value>>"}, "trakt_liked_lists": {"trakt_list_details": "<<value>>"},
"letterboxd_user_lists": {"letterboxd_list_details": "<<value>>"},
"tmdb_popular_people": {"tmdb_person": "<<value>>", "plex_search": {"all": {"actor": "tmdb"}}}, "tmdb_popular_people": {"tmdb_person": "<<value>>", "plex_search": {"all": {"actor": "tmdb"}}},
"trakt_people_list": {"tmdb_person": "<<value>>", "plex_search": {"all": {"actor": "tmdb"}}} "trakt_people_list": {"tmdb_person": "<<value>>", "plex_search": {"all": {"actor": "tmdb"}}}
} }
@ -1096,6 +1097,24 @@ class MetadataFile(DataFile):
auto_list[k] = v auto_list[k] = v
elif auto_type == "trakt_liked_lists": elif auto_type == "trakt_liked_lists":
_check_dict(self.config.Trakt.all_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": elif auto_type == "tmdb_popular_people":
if "data" in self.temp_vars: if "data" in self.temp_vars:
dynamic_data = util.parse("Config", "data", self.temp_vars["data"], datatype="int", minimum=1) dynamic_data = util.parse("Config", "data", self.temp_vars["data"], datatype="int", minimum=1)

Loading…
Cancel
Save