import contextlib
from loguru import logger
from flexget import plugin
from flexget.entry import Entry
from flexget.event import event
from flexget.utils.cached_input import cached
from flexget.utils.requests import RequestException, Session, TimedLimiter
from flexget.utils.soup import get_soup
logger = logger.bind(name='letterboxd')
requests = Session(max_retries=5)
requests.add_domain_limiter(TimedLimiter('letterboxd.com', '1 seconds'))
base_url = 'http://letterboxd.com'
SLUGS = {
'default': {'p_slug': '/%(user)s/list/%(list)s/', 'f_slug': 'data-film-slug'},
'diary': {'p_slug': '/%(user)s/films/diary/', 'f_slug': 'data-film-slug'},
'likes': {'p_slug': '/%(user)s/likes/films/', 'f_slug': 'data-film-link'},
'rated': {'p_slug': '/%(user)s/films/ratings/', 'f_slug': 'data-film-slug'},
'watched': {'p_slug': '/%(user)s/films/', 'f_slug': 'data-film-slug'},
'watchlist': {'p_slug': '/%(user)s/watchlist/', 'f_slug': 'data-film-slug'},
}
SORT_BY = {
'default': '',
'added': 'by/added/',
'length-ascending': 'by/shortest/',
'length-descending': 'by/longest/',
'name': 'by/name/',
'popularity': 'by/popular/',
'rating-ascending': 'by/rating-lowest/',
'rating-descending': 'by/rating/',
'release-ascending': 'by/release-earliest/',
'release-descending': 'by/release/',
}
[docs]
class Letterboxd:
schema = {
'type': 'object',
'properties': {
'username': {'type': 'string'},
'list': {'type': 'string'},
'sort_by': {'type': 'string', 'enum': list(SORT_BY.keys()), 'default': 'default'},
'max_results': {
'type': 'integer',
'deprecated': True,
'deprecationMessage': '`limit` plugin should be used instead of letterboxd `max_results` option',
},
},
'required': ['username', 'list'],
'additionalProperties': False,
}
[docs]
def build_config(self, config):
config['list'] = config['list'].lower().replace(' ', '-')
list_key = config['list']
if list_key not in list(SLUGS.keys()):
list_key = 'default'
config['p_slug'] = SLUGS[list_key]['p_slug'] % {
'user': config['username'],
'list': config['list'],
}
config['f_slug'] = SLUGS[list_key]['f_slug']
config['sort_by'] = SORT_BY[config['sort_by']]
return config
[docs]
def tmdb_lookup(self, search):
if not search:
logger.warning("Can't search tmdb, no tmdb_id")
return None
try:
tmdb = plugin.get('api_tmdb', self).lookup(tmdb_id=search)
except LookupError as e:
logger.warning('Error searching tmdb: {}', e)
return None
return {
'title': f'{tmdb.name} ({tmdb.year})',
'imdb_id': tmdb.imdb_id,
'tmdb_id': tmdb.id,
'movie_name': tmdb.name,
'movie_year': tmdb.year,
}
[docs]
def parse_film(self, film, config):
url = base_url + '/film/' + film.get(config['f_slug'])
soup = get_soup(requests.get(url).content)
result = self.tmdb_lookup(soup.find(attrs={'data-tmdb-id': True}).get('data-tmdb-id'))
if not result:
return None
entry = Entry(result)
entry['url'] = url
entry['letterboxd_list'] = '{} ({})'.format(config['list'], config['username'])
with contextlib.suppress(AttributeError):
entry['letterboxd_score'] = float(soup.find(itemprop='average').get('content'))
if config['list'] == 'diary':
entry['letterboxd_uscore'] = int(
film.find_next(attrs={'data-rating': True}).get('data-rating')
)
elif config['list'] == 'rated':
entry['letterboxd_uscore'] = int(film.find_next(itemprop='rating').get('content'))
return entry
@cached('letterboxd', persist='2 hours')
def on_task_input(self, task, config=None):
config = self.build_config(config)
url = base_url + config['p_slug'] + config['sort_by']
max_results = config.get('max_results', 1)
rcount = 0
next_page = ''
logger.verbose('Looking for films in Letterboxd list: {}', url)
while next_page is not None and rcount < max_results:
try:
page = requests.get(url).content
except RequestException as e:
raise plugin.PluginError(f'Error retrieving list from Letterboxd: {e}')
soup = get_soup(page)
for film in soup.find_all(attrs={config['f_slug']: True}):
if rcount < max_results:
result = self.parse_film(film, config)
if not result:
continue
yield result
if 'max_results' in config:
rcount += 1
next_page = soup.select_one('.paginate-nextprev .next')
if next_page is not None:
next_page = next_page.get('href')
if next_page is not None:
url = base_url + next_page
[docs]
@event('plugin.register')
def register_plugin():
plugin.register(Letterboxd, 'letterboxd', api_ver=2)