# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/indexers/tmdb_api.py
# © Les alKODIques

import requests
import random
import hashlib
import sys
import time
from datetime import timedelta
from threading import Thread, Event, Lock
from urllib.parse import quote_plus, urlsplit, parse_qsl
from caches.main_cache import cache_object, MainCache
from caches.meta_cache import cache_function
from modules import kodi_utils
ls = kodi_utils.local_string
from modules.settings import tmdb_api_key, get_language, image_language_query, certification_country, show_unaired_watchlist, ignore_articles, lists_sort_order, lists_sort_reverse, paginate, page_limit
from modules.utils import paginate_list, sort_for_article, jsondate_to_datetime, get_datetime, chunks, TaskPool

EXPIRES_4_HOURS, EXPIRES_2_DAYS, EXPIRES_1_WEEK, EXPIRES_1_MONTH = 4, 48, 168, 672
ls, logger, js2date = kodi_utils.local_string, kodi_utils.logger, jsondate_to_datetime
get_setting, set_setting = kodi_utils.get_setting, kodi_utils.set_setting
movies_append = 'external_ids,videos,credits,release_dates,alternative_titles,translations,images'
tvshows_append = 'external_ids,videos,credits,aggregate_credits,content_ratings,alternative_titles,translations,images'
eps_map = {1: 'Original air date', 2: 'Absolute', 3: 'DVD', 4: 'Digital', 5: 'Story arc', 6: 'Production', 7: 'TV'}
tmdb_image_base = 'https://image.tmdb.org/t/p/%s%s'
base_url = 'https://api.themoviedb.org/3'
timeout = 3.05
session = requests.Session()
retry = requests.adapters.Retry(total=None, status=1, status_forcelist=(429, 502, 503, 504))
session.mount('https://api.themoviedb.org', requests.adapters.HTTPAdapter(pool_maxsize=100, max_retries=retry))

# Cache mémoire court pour éviter plusieurs appels TMDb identiques dans la même
# navigation Kodi. Le cache persistant reste géré par maincache/metacache ; cette
# couche ne sert qu'à absorber les doublons immédiats (widgets, fenêtres info,
# bandes-annonces, images, etc.).
_TMDb_MEMORY_CACHE = {}
_TMDb_INFLIGHT = {}
_TMDb_INFLIGHT_LOCK = Lock()
_TMDb_MEMORY_TTL = 120
_TMDb_MEMORY_MAX = 200
_TMDb_NEGATIVE_CACHE_PREFIX = 'tmdb_negative_404_'
_TMDb_NEGATIVE_CACHE_TTL = EXPIRES_2_DAYS

def _tmdb_debug_enabled():
	try: return get_setting('debug.enabled', 'false') == 'true'
	except: return False

def _tmdb_route_label(url):
	try:
		parsed = urlsplit(str(url or ''))
		path = parsed.path.replace('/3/', '/').strip('/') or 'unknown'
		query = dict(parse_qsl(parsed.query, keep_blank_values=True))
		hints = []
		for key in ('language', 'page', 'append_to_response', 'include_image_language', 'external_source'):
			value = query.get(key)
			if value: hints.append('%s=%s' % (key, value))
		return '%s%s' % (path, ' | ' + ' | '.join(hints) if hints else '')
	except:
		return 'unknown'

def _tmdb_debug_log(action, url, extra=''):
	# Les traces techniques TMDb (request, cache miss/set/hit, images,
	# singleflight, negative cache) sont trop fréquentes pour le journal Kodi.
	# Elles n'indiquent pas une erreur et masquent les vrais problèmes.
	return

def _tmdb_memory_get(url):
	try:
		key = str(url or '')
		item = _TMDb_MEMORY_CACHE.get(key)
		if not item: return None
		expires, result = item
		if expires > time.time(): return result
		_TMDb_MEMORY_CACHE.pop(key, None)
	except: pass
	return None

def _tmdb_memory_set(url, result):
	try:
		if result is None: return
		key = str(url or '')
		if len(_TMDb_MEMORY_CACHE) >= _TMDb_MEMORY_MAX:
			oldest = next(iter(_TMDb_MEMORY_CACHE))
			_TMDb_MEMORY_CACHE.pop(oldest, None)
		_TMDb_MEMORY_CACHE[key] = (time.time() + _TMDb_MEMORY_TTL, result)
	except: pass


# Verrou court de type single-flight : lorsqu'une même URL TMDb est demandée
# par plusieurs widgets en même temps, un seul thread fait l'appel réseau et
# les autres attendent le résultat placé dans le cache mémoire. Cela évite les
# rafales d'appels identiques au chargement de l'accueil, sans changer le rendu
# ni la logique d'enrichissement alkoFlix.
def _tmdb_inflight_acquire(url):
	key = str(url or '')
	try:
		with _TMDb_INFLIGHT_LOCK:
			event = _TMDb_INFLIGHT.get(key)
			if event is None:
				event = Event()
				_TMDb_INFLIGHT[key] = event
				return key, event, True
			return key, event, False
	except:
		return key, None, True


def _tmdb_inflight_release(key, event):
	try:
		with _TMDb_INFLIGHT_LOCK:
			if _TMDb_INFLIGHT.get(key) is event:
				_TMDb_INFLIGHT.pop(key, None)
		event.set()
	except:
		try: event.set()
		except: pass


def _tmdb_inflight_wait(url, event):
	try:
		_tmdb_debug_log('singleflight wait', url)
		event.wait(timeout + 5)
		cached = _tmdb_memory_get(url)
		if cached is not None:
			_tmdb_debug_log('singleflight hit', url)
			return cached
		negative = _tmdb_negative_get(url)
		if negative is not None:
			_tmdb_memory_set(url, negative)
			return negative
		_tmdb_debug_log('singleflight miss', url)
	except: pass
	return None


def _tmdb_negative_cache_key(url):
	try: return '%s%s' % (_TMDb_NEGATIVE_CACHE_PREFIX, hashlib.md5(str(url or '').encode('utf-8')).hexdigest())
	except: return '%s%s' % (_TMDb_NEGATIVE_CACHE_PREFIX, abs(hash(str(url or ''))))


def _tmdb_negative_payload(url, cached=False):
	return {
		'success': False,
		'status_code': 404,
		'status_message': 'TMDb item not found',
		'_alkoflix_negative_cache': True,
		'_alkoflix_negative_cached': bool(cached)
	}


def _tmdb_negative_get(url):
	try:
		key = _tmdb_negative_cache_key(url)
		data = MainCache().get(key)
		if isinstance(data, dict) and data.get('status_code') == 404:
			_tmdb_debug_log('negative cache hit', url, 'key=%s' % key)
			return _tmdb_negative_payload(url, cached=True)
	except: pass
	return None


def _tmdb_negative_set(url):
	try:
		key = _tmdb_negative_cache_key(url)
		payload = _tmdb_negative_payload(url, cached=False)
		MainCache().set(key, payload, expiration=timedelta(hours=_TMDb_NEGATIVE_CACHE_TTL))
		_tmdb_memory_set(url, payload)
		_tmdb_debug_log('negative cache set', url, 'key=%s | expires=%sh' % (key, _TMDb_NEGATIVE_CACHE_TTL))
		return payload
	except: return _tmdb_negative_payload(url, cached=False)


def clear_tmdb_negative_cache():
	try:
		from modules import kodi_utils as _kodi_utils
		dbcon = _kodi_utils.database_connect(_kodi_utils.maincache_db)
		dbcur = dbcon.cursor()
		dbcur.execute("""SELECT id FROM maincache WHERE id LIKE ?""", (_TMDb_NEGATIVE_CACHE_PREFIX + '%',))
		items = [str(i[0]) for i in dbcur.fetchall()]
		if items:
			dbcur.execute("""DELETE FROM maincache WHERE id LIKE ?""", (_TMDb_NEGATIVE_CACHE_PREFIX + '%',))
			dbcon.commit()
			dbcur.execute("""VACUUM""")
			for item in items: _kodi_utils.clear_property(item)
		dbcon.close()
		return True
	except: return False


# Effectue une requête TMDb et retourne la réponse JSON ou texte.
def get_tmdb(url, errors=True):
	cached = _tmdb_memory_get(url)
	if cached is not None:
		_tmdb_debug_log('memory hit', url)
		return cached
	negative = _tmdb_negative_get(url)
	if negative is not None:
		_tmdb_memory_set(url, negative)
		return negative
	key, event, owner = _tmdb_inflight_acquire(url)
	if not owner and event is not None:
		waited = _tmdb_inflight_wait(url, event)
		if waited is not None:
			return waited
		# Si le premier thread a échoué sans rien mettre en cache, on continue
		# normalement pour ne jamais bloquer une navigation sur une attente vide.
		key, event, owner = _tmdb_inflight_acquire(url)
	try:
		_tmdb_debug_log('request', url)
		response = session.get(url, timeout=timeout)
		result = response.json() if 'json' in response.headers.get('Content-Type', '') else response.text
		if response.status_code == 404: return _tmdb_negative_set(url)
		if not response.ok: response.raise_for_status()
		_tmdb_memory_set(url, result)
		return result
	except requests.exceptions.RequestException as e:
		if errors: logger('tmdb error', str(e))
	finally:
		if owner and event is not None:
			_tmdb_inflight_release(key, event)


# Cache persistant léger pour les endpoints TMDb secondaires. Contrairement à
# cache_object(), ce helper distingue correctement une réponse vide déjà cachée
# d'une vraie absence de cache. Les écritures de diagnostic restent strictement
# limitées au mode debug alkoFlix.
def _tmdb_cached_secondary(string, url, expiration=EXPIRES_4_HOURS, label='secondary'):
	try:
		maincache = MainCache()
		cached = maincache.get(string)
		if cached is not None:
			_tmdb_debug_log('%s cache hit' % label, url, 'key=%s' % string)
			return cached
		_tmdb_debug_log('%s cache miss' % label, url, 'key=%s' % string)
		result = get_tmdb(url)
		if result is not None:
			maincache.set(string, result, expiration=timedelta(hours=expiration))
			_tmdb_debug_log('%s cache set' % label, url, 'key=%s | expires=%sh' % (string, expiration))
		return result
	except Exception as e:
		if _tmdb_debug_enabled():
			try: logger('TMDb', '%s cache error | key=%s | error=%s' % (label, string, e))
			except: pass
		return get_tmdb(url)

# Normalise la région d'un fournisseur TMDb.
def _provider_region(region=None):
	region = str(region or '').strip().upper()
	return region if len(region) == 2 else certification_country()

# Retourne une liste courte de régions à tester lorsqu'un fournisseur TMDb
# ne retourne rien dans la région demandée. Certains services mondiaux sont
# exposés de manière variable selon les pays dans TMDb/JustWatch.
def _provider_region_candidates(region=None, default_region=None):
	candidates = []
	for candidate in (_provider_region(region or default_region), certification_country(), 'US', 'CA', 'FR', 'GB'):
		candidate = str(candidate or '').strip().upper()
		if len(candidate) == 2 and candidate not in candidates:
			candidates.append(candidate)
	return candidates

# Effectue une requête Discover avec fournisseur et bascule de région si le
# premier pays ne retourne aucun résultat. Le fallback reste léger : il ne se
# déclenche que pour un dossier vide, avec résultats cachés par région/page.
def _tmdb_provider_discover(cache_prefix, provider_id, page_no, url_builder, region=None, default_region=None):
	regions = _provider_region_candidates(region, default_region)
	first_data = None
	first_region = regions[0] if regions else ''
	for current_region in regions:
		string = '%s_%s_%s_%s' % (cache_prefix, current_region, provider_id, page_no)
		data = _tmdb_cached_secondary(string, url_builder(current_region), expiration=EXPIRES_2_DAYS, label='provider discover') or {}
		if first_data is None: first_data = data
		results = data.get('results', []) if isinstance(data, dict) else []
		if results:
			if current_region != first_region:
				logger('catalogue provider region fallback', '%s | provider_id=%s | region=%s -> %s | page=%s' % (cache_prefix, provider_id, first_region or 'settings', current_region, page_no))
			return data
	return first_data if isinstance(first_data, dict) else {}

# Retourne une page aléatoire Discover tout en évitant une pagination infinie
# dans les menus. Le premier appel sert seulement à connaître le nombre de pages.
def _tmdb_random_discover(cache_prefix, base_url_no_page):
	seed_key = '%s_seed' % cache_prefix
	separator = '&' if '?' in base_url_no_page else '?'
	seed = cache_object(get_tmdb, seed_key, '%s%spage=1' % (base_url_no_page, separator), expiration=EXPIRES_4_HOURS) or {}
	try: total_pages = int(seed.get('total_pages', 1))
	except: total_pages = 1
	random_page = random.randint(1, max(1, min(total_pages, 500)))
	data = get_tmdb('%s%spage=%s' % (base_url_no_page, separator, random_page)) or {}
	if isinstance(data, dict):
		data['page'] = 1
		data['total_pages'] = 1
	return data

def _url_without_page(url):
	return str(url or '').rsplit('&page=', 1)[0].rsplit('?page=', 1)[0]

def _tmdb_random_discover_limited(cache_prefix, base_url_no_page, page_no, max_pages=4):
	# Variante utilisée par les tris de genres : chaque page Kodi retourne une page
	# TMDb aléatoire, mais la navigation reste volontairement limitée.
	seed_key = '%s_seed' % cache_prefix
	separator = '&' if '?' in base_url_no_page else '?'
	seed = cache_object(get_tmdb, seed_key, '%s%spage=1' % (base_url_no_page, separator), expiration=EXPIRES_4_HOURS) or {}
	try: total_pages = int(seed.get('total_pages', 1))
	except: total_pages = 1
	random_page = random.randint(1, max(1, min(total_pages, 500)))
	data = get_tmdb('%s%spage=%s' % (base_url_no_page, separator, random_page)) or {}
	if isinstance(data, dict):
		try: current_page = max(1, int(page_no))
		except: current_page = 1
		data['page'] = min(current_page, max_pages)
		data['total_pages'] = max_pages
	return data


def _tmdb_replace_sort_by(url, sort_by):
	url = str(url or '')
	if 'sort_by=' not in url:
		separator = '&' if '?' in url else '?'
		return '%s%ssort_by=%s' % (url, separator, sort_by)
	before, after = url.split('sort_by=', 1)
	if '&' in after:
		ignored, rest = after.split('&', 1)
		return '%ssort_by=%s&%s' % (before, sort_by, rest)
	return '%ssort_by=%s' % (before, sort_by)


def _tmdb_append_query_param(url, key, value):
	url = str(url or '')
	if not value or ('%s=' % key) in url: return url
	separator = '&' if '?' in url else '?'
	return '%s%s%s=%s' % (url, separator, key, value)


def _tmdb_set_query_param(url, key, value):
	url = str(url or '')
	if not value: return url
	if ('%s=' % key) not in url: return _tmdb_append_query_param(url, key, value)
	before, after = url.split('%s=' % key, 1)
	if '&' in after:
		ignored, rest = after.split('&', 1)
		return '%s%s=%s&%s' % (before, key, value, rest)
	return '%s%s=%s' % (before, key, value)


ALLOWED_CONTEXT_ORIGIN_COUNTRIES = ('FR', 'CA', 'US')
ALLOWED_CONTEXT_ORIGINAL_LANGUAGES = ('fr', 'en')
BLOCKED_CONTEXT_ORIGINAL_LANGUAGES = (
	'ja', 'ko', 'zh', 'cn', 'yue', 'th', 'hi', 'ta', 'te', 'ml', 'kn', 'bn', 'ur',
	'id', 'ms', 'vi', 'tl', 'fil', 'km', 'lo', 'my', 'ne', 'si', 'pa', 'gu', 'mr'
)
BLOCKED_CONTEXT_SCRIPT_RANGES = (
	(0x3040, 0x30ff), # Hiragana / Katakana
	(0x31f0, 0x31ff), # Katakana phonetic extensions
	(0x3400, 0x4dbf), # CJK extension A
	(0x4e00, 0x9fff), # CJK
	(0xac00, 0xd7af), # Hangul
	(0x1100, 0x11ff), # Hangul Jamo
	(0x0e00, 0x0e7f), # Thai
	(0x0e80, 0x0eff), # Lao
	(0x1780, 0x17ff), # Khmer
	(0x1000, 0x109f), # Myanmar
	(0x0900, 0x097f), # Devanagari
	(0x0980, 0x09ff), # Bengali
	(0x0a00, 0x0a7f), # Gurmukhi
	(0x0a80, 0x0aff), # Gujarati
	(0x0b80, 0x0bff), # Tamil
	(0x0c00, 0x0c7f), # Telugu
	(0x0c80, 0x0cff), # Kannada
	(0x0d00, 0x0d7f), # Malayalam
	(0x0d80, 0x0dff), # Sinhala
)

# Seuils utilisés seulement par les tris contextuels des genres.
# Objectif : garder un mode Aléatoire vivant, mais éviter les fiches trop obscures
# ou sans assez de votes pour être pertinentes dans les dossiers publics.
TMDB_CONTEXT_MIN_VOTES = {
	'movie': {'popular': 50, 'date': 20, 'rated': 100, 'random': 80},
	'tv': {'popular': 30, 'date': 15, 'rated': 50, 'random': 40}
}
TMDB_CONTEXT_RANDOM_SOURCE_PAGES = 25
TMDB_CONTEXT_SOURCE_PAGES = {
	'movie': {'popular': 4, 'date': 8, 'rated': 18},
	'tv': {'popular': 4, 'date': 8, 'rated': 14}
}
TMDB_CONTEXT_PAGE_SIZE = 20
TMDB_CONTEXT_MAX_PAGES = 4


def _tmdb_context_min_votes(media_kind='movie', sort_key='popular'):
	media_kind = 'tv' if str(media_kind or '') in ('tv', 'series', 'tvshow') else 'movie'
	sort_key = str(sort_key or 'popular').strip().lower()
	try: return int(TMDB_CONTEXT_MIN_VOTES.get(media_kind, {}).get(sort_key, 0) or 0)
	except: return 0

def _tmdb_context_source_pages(media_kind='movie', sort_key='popular'):
	media_kind = 'tv' if str(media_kind or '') in ('tv', 'series', 'tvshow') else 'movie'
	sort_key = str(sort_key or 'popular').strip().lower()
	try: return max(1, int(TMDB_CONTEXT_SOURCE_PAGES.get(media_kind, {}).get(sort_key, 4) or 4))
	except: return 4


def _tmdb_context_url_page(url, page_no):
	return _tmdb_set_query_param(url, 'page', str(page_no))


def _tmdb_context_paginated_payload(template, results, current_page, max_pages=TMDB_CONTEXT_MAX_PAGES):
	try: current_page = max(1, int(current_page or 1))
	except: current_page = 1
	try: max_pages = max(1, int(max_pages or TMDB_CONTEXT_MAX_PAGES))
	except: max_pages = TMDB_CONTEXT_MAX_PAGES
	page_size = TMDB_CONTEXT_PAGE_SIZE
	max_items = max_pages * page_size
	results = (results or [])[:max_items]
	start = (current_page - 1) * page_size
	end = start + page_size
	visible = results[start:end]
	total_pages = max(1, min(max_pages, ((len(results) + page_size - 1) // page_size)))
	if visible and current_page < max_pages and total_pages < current_page:
		total_pages = current_page
	template = template or {}
	template['page'] = min(current_page, max_pages)
	template['total_pages'] = total_pages if visible or current_page == 1 else max(1, current_page - 1)
	template['results'] = visible
	return template


def _tmdb_context_vote_count(item):
	try: return int(item.get('vote_count') or 0)
	except: return 0


def _tmdb_context_has_artwork(item):
	return bool((item or {}).get('poster_path') or (item or {}).get('backdrop_path'))


def _tmdb_context_language(value):
	value = str(value or '').strip().lower()
	if not value: return ''
	return value.split('-', 1)[0].split('_', 1)[0]


def _tmdb_has_blocked_context_script(value):
	try:
		for char in str(value or ''):
			code = ord(char)
			for start, end in BLOCKED_CONTEXT_SCRIPT_RANGES:
				if start <= code <= end: return True
	except: pass
	return False


def _tmdb_context_country_codes(item):
	codes = set()
	for key in ('origin_country', 'production_countries'):
		for country in item.get(key) or []:
			if isinstance(country, dict): country = country.get('iso_3166_1')
			country = str(country or '').strip().upper()
			if country: codes.add(country)
	return codes


def _tmdb_context_item_allowed(item, media_kind='movie', sort_key='popular'):
	if not isinstance(item, dict): return False
	# Les tris contextuels servent à présenter des dossiers propres : on évite donc
	# les fiches sans visuel, qui finissent trop souvent en vignettes vides.
	if not _tmdb_context_has_artwork(item): return False
	for key in ('title', 'original_title', 'name', 'original_name'):
		if _tmdb_has_blocked_context_script(item.get(key)): return False
	language = _tmdb_context_language(item.get('original_language'))
	if language in BLOCKED_CONTEXT_ORIGINAL_LANGUAGES: return False
	if language and language not in ALLOWED_CONTEXT_ORIGINAL_LANGUAGES: return False
	countries = _tmdb_context_country_codes(item)
	if countries and not countries.intersection(ALLOWED_CONTEXT_ORIGIN_COUNTRIES): return False
	min_votes = _tmdb_context_min_votes(media_kind, sort_key)
	if min_votes and _tmdb_context_vote_count(item) < min_votes: return False
	return True


def _tmdb_filter_context_results(data, media_kind='movie', sort_key='popular'):
	if not isinstance(data, dict): return data
	results = data.get('results')
	if not isinstance(results, list): return data
	data = dict(data)
	data['results'] = [i for i in results if _tmdb_context_item_allowed(i, media_kind, sort_key)]
	return data


def _tmdb_context_language_url(url, language_code):
	# TMDb ne gère pas toujours les filtres combinés avec | de manière fiable.
	# Pour les tris contextuels, on requête donc chaque variante séparément.
	return _tmdb_set_query_param(url, 'with_original_language', language_code)


def _tmdb_context_origin_url(url, country_code):
	return _tmdb_set_query_param(url, 'with_origin_country', country_code)


def _tmdb_context_url_variants(url):
	# Variantes volontairement strictes : France / Canada / USA, langues fr/en.
	# Pour les plateformes, on filtre surtout par disponibilité régionale
	# (watch_region). Ajouter aussi with_origin_country vide beaucoup trop
	# certains services et rend Aléatoire famélique.
	url_text = str(url or '')
	if 'with_watch_providers=' in url_text:
		variants = (
			('fr_FR', 'fr', 'FR'), ('fr_CA', 'fr', 'CA'), ('fr_US', 'fr', 'US'),
			('en_FR', 'en', 'FR'), ('en_CA', 'en', 'CA'), ('en_US', 'en', 'US')
		)
		for tag, language_code, country_code in variants:
			variant_url = _tmdb_set_query_param(url, 'watch_region', country_code)
			variant_url = _tmdb_context_language_url(variant_url, language_code)
			yield tag, variant_url
		return
	variants = (('fr_FR', 'fr', 'FR'), ('fr_CA', 'fr', 'CA'), ('en_CA', 'en', 'CA'), ('en_US', 'en', 'US'))
	for tag, language_code, country_code in variants:
		variant_url = _tmdb_context_language_url(url, language_code)
		variant_url = _tmdb_context_origin_url(variant_url, country_code)
		yield tag, variant_url


def _tmdb_apply_regional_content_filter(url):
	# On ne met pas de filtre OR ici : les appels fr/en sont générés juste avant
	# la requête, puis protégés par un filtre local strict.
	return url


def _tmdb_genre_sort_values(media_kind='movie', genre_sort='popular'):
	media_kind = 'tv' if str(media_kind or '') in ('tv', 'series', 'tvshow') else 'movie'
	genre_sort = str(genre_sort or 'popular').strip().lower()
	if genre_sort == 'date':
		return 'date', 'first_air_date.desc' if media_kind == 'tv' else 'primary_release_date.desc', (('first_air_date.lte' if media_kind == 'tv' else 'primary_release_date.lte', get_dates(0)[0]),)
	if genre_sort == 'rated':
		return 'rated', 'vote_average.desc', (('vote_count.gte', '30' if media_kind == 'tv' else '50'),)
	if genre_sort == 'random':
		return 'random', 'popularity.desc', ()
	return 'popular', 'popularity.desc', ()


def _tmdb_apply_genre_sort(url, media_kind='movie', genre_sort='popular'):
	sort_key, sort_by, extra_params = _tmdb_genre_sort_values(media_kind, genre_sort)
	url = _tmdb_replace_sort_by(url, sort_by)
	url = _tmdb_apply_regional_content_filter(url)
	for key, value in extra_params:
		url = _tmdb_append_query_param(url, key, value)
	min_votes = _tmdb_context_min_votes(media_kind, sort_key)
	if min_votes:
		url = _tmdb_set_query_param(url, 'vote_count.gte', str(min_votes))
	url = _tmdb_set_query_param(url, 'include_adult', 'false')
	return sort_key, url


def _tmdb_context_date_value(item, media_kind='movie'):
	for key in (('first_air_date', 'release_date', 'primary_release_date') if media_kind == 'tv' else ('release_date', 'primary_release_date', 'first_air_date')):
		value = item.get(key)
		if value: return str(value)
	return ''


def _tmdb_context_sort_results(results, sort_key, media_kind='movie'):
	try:
		if sort_key == 'date':
			results.sort(key=lambda i: (_tmdb_context_date_value(i, media_kind), i.get('popularity') or 0), reverse=True)
		elif sort_key == 'rated':
			results.sort(key=lambda i: (float(i.get('vote_average') or 0), int(i.get('vote_count') or 0), i.get('popularity') or 0), reverse=True)
		elif sort_key == 'random':
			random.shuffle(results)
		else:
			results.sort(key=lambda i: (float(i.get('popularity') or 0), int(i.get('vote_count') or 0)), reverse=True)
	except: pass
	return results


def _tmdb_context_dedupe_results(results):
	seen, cleaned = set(), []
	for item in results or []:
		try: key = '%s:%s' % (item.get('media_type') or item.get('media_kind') or '', item.get('id'))
		except: key = ''
		if not key or key in seen: continue
		seen.add(key)
		cleaned.append(item)
	return cleaned




def _tmdb_context_hash(value):
	try: return hashlib.md5(str(value or '').encode('utf-8')).hexdigest()
	except: return str(abs(hash(str(value or ''))))


def _tmdb_context_combined_cache_key(cache_prefix, url, sort_key, media_kind, suffix='combined'):
	return 'tmdb_context_%s_%s_%s_%s_%s' % (
		str(suffix or 'combined'), str(media_kind or 'movie'), str(sort_key or 'popular'),
		str(cache_prefix or 'discover'), _tmdb_context_hash(url)
	)


def _tmdb_context_cache_get(key):
	try:
		cached = MainCache().get(key)
		if cached is not None: return cached
	except: pass
	return None


def _tmdb_context_cache_set(key, data, expiration=EXPIRES_4_HOURS):
	try: MainCache().set(key, data, expiration=timedelta(hours=expiration))
	except: pass


def _tmdb_context_fetch_combined(cache_prefix, url, page_no, sort_key, media_kind='movie'):
	try: current_page = max(1, int(page_no or 1))
	except: current_page = 1
	# Les tris contextuels peuvent combiner beaucoup de variantes FR/CA/US + fr/en.
	# On met donc en cache le bassin final dédupliqué/trié : les pages suivantes
	# et les réouvertures du même dossier évitent de relire toutes les pages source.
	combined_key = _tmdb_context_combined_cache_key(cache_prefix, url, sort_key, media_kind, 'combined')
	cached = _tmdb_context_cache_get(combined_key)
	if isinstance(cached, dict):
		return _tmdb_context_paginated_payload(dict(cached.get('template') or {}), list(cached.get('results') or []), current_page)
	results, template = [], {}
	source_pages = _tmdb_context_source_pages(media_kind, sort_key)
	max_items = TMDB_CONTEXT_MAX_PAGES * TMDB_CONTEXT_PAGE_SIZE * 2
	for variant_tag, variant_url in _tmdb_context_url_variants(url):
		for source_page in range(1, source_pages + 1):
			page_url = _tmdb_context_url_page(variant_url, source_page)
			string = '%s_%s_source_%s' % (cache_prefix, variant_tag, source_page)
			data = cache_object(get_tmdb, string, page_url, expiration=EXPIRES_2_DAYS) or {}
			if isinstance(data, dict):
				if not template: template = dict(data)
				filtered = _tmdb_filter_context_results(data, media_kind, sort_key)
				results.extend(filtered.get('results', []) if isinstance(filtered, dict) else [])
			if len(results) >= max_items: break
		if len(results) >= max_items: break
	results = _tmdb_context_dedupe_results(results)
	results = _tmdb_context_sort_results(results, sort_key, media_kind)
	_tmdb_context_cache_set(combined_key, {'template': template, 'results': results}, expiration=EXPIRES_4_HOURS)
	return _tmdb_context_paginated_payload(template, results, current_page)


def _tmdb_context_random_combined(cache_prefix, base_url_no_page, page_no, media_kind='movie', max_pages=4):
	try: current_page = max(1, int(page_no))
	except: current_page = 1
	# Le mode Aléatoire garde un mélange neuf, mais le bassin source est caché.
	# Sinon chaque ouverture relit jusqu'à 6 variantes x 12 pages, même quand les
	# réponses TMDb sont déjà en cache local.
	pool_key = _tmdb_context_combined_cache_key(cache_prefix, base_url_no_page, 'random', media_kind, 'random_pool')
	cached = _tmdb_context_cache_get(pool_key)
	if isinstance(cached, dict):
		results = list(cached.get('results') or [])
		template = dict(cached.get('template') or {})
		results = _tmdb_context_sort_results(results, 'random', media_kind)
		return _tmdb_context_paginated_payload(template, results, current_page, max_pages=max_pages)
	results, template = [], {}
	# Aléatoire doit rester propre ET fourni. On scanne donc les premières
	# pages populaires filtrées, puis on mélange le bassin obtenu. Cela évite
	# les pages vides quand quelques pages aléatoires tombent sur du contenu
	# trop obscur ou trop filtré.
	source_pages = max(1, min(TMDB_CONTEXT_RANDOM_SOURCE_PAGES, 12))
	max_items = TMDB_CONTEXT_MAX_PAGES * TMDB_CONTEXT_PAGE_SIZE * 2
	for variant_tag, variant_base in _tmdb_context_url_variants(base_url_no_page):
		separator = '&' if '?' in variant_base else '?'
		for source_page in range(1, source_pages + 1):
			page_url = '%s%spage=%s' % (variant_base, separator, source_page)
			string = '%s_%s_random_source_%s' % (cache_prefix, variant_tag, source_page)
			data = cache_object(get_tmdb, string, page_url, expiration=EXPIRES_4_HOURS) or {}
			if isinstance(data, dict):
				if not template: template = dict(data)
				filtered = _tmdb_filter_context_results(data, media_kind, 'random')
				results.extend(filtered.get('results', []) if isinstance(filtered, dict) else [])
			if len(results) >= max_items: break
		if len(results) >= max_items: break
	results = _tmdb_context_dedupe_results(results)
	_tmdb_context_cache_set(pool_key, {'template': template, 'results': results}, expiration=EXPIRES_4_HOURS)
	results = _tmdb_context_sort_results(results, 'random', media_kind)
	return _tmdb_context_paginated_payload(template, results, current_page, max_pages=max_pages)


def _tmdb_genre_discover(cache_prefix, url, page_no, media_kind='movie', genre_sort='popular'):
	sort_key, url = _tmdb_apply_genre_sort(url, media_kind, genre_sort)
	cache_prefix = '%s_%s' % (cache_prefix, sort_key)
	if sort_key == 'random':
		return _tmdb_context_random_combined(cache_prefix, _url_without_page(url), page_no, media_kind, max_pages=4)
	return _tmdb_context_fetch_combined(cache_prefix, url, page_no, sort_key, media_kind)


def _tmdb_context_or_cache(cache_prefix, url, page_no, media_kind='movie', context_sort='', expiration=EXPIRES_2_DAYS, label='discover context'):
	# Mode contextuel : même moteur strict que les genres (FR/CA/US, fr/en,
	# votes minimum, image obligatoire, exclusion des écritures asiatiques).
	# Mode normal : conservation exacte du comportement existant.
	if context_sort:
		return _tmdb_genre_discover(cache_prefix, url, page_no, media_kind, context_sort)
	return cache_object(get_tmdb, '%s_%s' % (cache_prefix, page_no), url, expiration=expiration)


def _tmdb_recent_context_or_cache(cache_prefix, url, page_no, media_kind='tv', context_sort='', expiration=EXPIRES_2_DAYS):
	# Les dossiers "Nouveautés" sont déjà limités par date. Leur appliquer le
	# moteur contextuel strict (variantes FR/CA/US + seuil de votes) vide trop
	# facilement les pages, surtout en séries où les nouvelles fiches ont peu de
	# votes. Ici on conserve le bassin TMDb d'origine, puis on applique seulement
	# le tri demandé avant de paginer proprement.
	if not context_sort:
		return cache_object(get_tmdb, '%s_%s' % (cache_prefix, page_no), url, expiration=expiration)
	try: current_page = max(1, int(page_no or 1))
	except: current_page = 1
	sort_key = str(context_sort or 'popular').strip().lower()
	base = _url_without_page(url)
	cache_key = _tmdb_context_combined_cache_key(cache_prefix, base, sort_key, media_kind, 'recent_combined')
	cached = _tmdb_context_cache_get(cache_key)
	if isinstance(cached, dict):
		results = list(cached.get('results') or [])
		template = dict(cached.get('template') or {})
		if sort_key == 'random': results = _tmdb_context_sort_results(results, 'random', media_kind)
		return _tmdb_context_paginated_payload(template, results, current_page, max_pages=TMDB_CONTEXT_MAX_PAGES)
	separator = '&' if '?' in base else '?'
	results, template, source_total_pages = [], {}, 1
	first_url = '%s%spage=1' % (base, separator)
	first_data = cache_object(get_tmdb, '%s_recent_source_1' % cache_prefix, first_url, expiration=expiration) or {}
	if isinstance(first_data, dict):
		template = dict(first_data)
		try: source_total_pages = max(1, int(first_data.get('total_pages', 1) or 1))
		except: source_total_pages = 1
		results.extend(first_data.get('results', []) or [])
	if sort_key == 'random': source_pages = min(source_total_pages, TMDB_CONTEXT_RANDOM_SOURCE_PAGES)
	else: source_pages = min(source_total_pages, _tmdb_context_source_pages(media_kind, sort_key))
	max_items = TMDB_CONTEXT_MAX_PAGES * TMDB_CONTEXT_PAGE_SIZE * 2
	for source_page in range(2, source_pages + 1):
		page_url = '%s%spage=%s' % (base, separator, source_page)
		data = cache_object(get_tmdb, '%s_recent_source_%s' % (cache_prefix, source_page), page_url, expiration=expiration) or {}
		if isinstance(data, dict):
			if not template: template = dict(data)
			results.extend(data.get('results', []) or [])
		if len(results) >= max_items: break
	results = _tmdb_context_dedupe_results(results)
	results = _tmdb_context_sort_results(results, sort_key, media_kind)
	_tmdb_context_cache_set(cache_key, {'template': template, 'results': results}, expiration=EXPIRES_4_HOURS)
	return _tmdb_context_paginated_payload(template, results, current_page, max_pages=TMDB_CONTEXT_MAX_PAGES)


def _tmdb_provider_context_discover(cache_prefix, provider_id, page_no, url_builder, media_kind='movie', genre_sort='', region=None, default_region=None):
	if genre_sort:
		current_region = _provider_region(region or default_region or certification_country())
		url = url_builder(current_region)
		return _tmdb_genre_discover('%s_%s_%s' % (cache_prefix, provider_id, current_region), url, page_no, media_kind, genre_sort)
	return _tmdb_provider_discover(cache_prefix, provider_id, page_no, url_builder, region=region, default_region=default_region)


# Filtre localement les résultats TMDb par genre quand l'endpoint utilisé
# ne supporte pas le filtre de genre directement, par exemple /trending.
def _tmdb_filter_results_by_genre(data, genre_ids):
	if not isinstance(data, dict): return data
	wanted = set(str(genre_ids or '').replace('|', ',').split(','))
	wanted.discard('')
	if not wanted: return data
	filtered = []
	for item in data.get('results', []) or []:
		item_genres = set(str(i) for i in item.get('genre_ids', []) or [])
		if item_genres.intersection(wanted): filtered.append(item)
	data = dict(data)
	data['results'] = filtered
	return data

# Construit un dossier Tendances filtré par genre. TMDb ne permet pas de
# combiner directement /trending avec with_genres, donc on lit plusieurs pages
# tendance et on garde seulement les éléments du genre demandé.
def _tmdb_trending_by_genre(media_type, cache_prefix, genre_ids, page_no, pages_per_call=5):
	try: page_no = int(page_no)
	except: page_no = 1
	try: pages_per_call = max(1, int(pages_per_call))
	except: pages_per_call = 5
	start_page = ((max(1, page_no) - 1) * pages_per_call) + 1
	results, source_total_pages = [], start_page
	for source_page in range(start_page, start_page + pages_per_call):
		string = '%s_source_%s' % (cache_prefix, source_page)
		url = '%s/trending/%s/week?api_key=%s&language=en-US&page=%s' % (base_url, media_type, tmdb_api_key(), source_page)
		data = cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS) or {}
		try: source_total_pages = max(source_total_pages, int(data.get('total_pages', source_total_pages)))
		except: pass
		filtered = _tmdb_filter_results_by_genre(data, genre_ids)
		results.extend(filtered.get('results', []) if isinstance(filtered, dict) else [])
		if len(results) >= 20: break
	return {'page': page_no, 'results': results[:20], 'total_pages': page_no + 1 if source_total_pages >= start_page + pages_per_call else page_no}


# Recherche l’identifiant TMDb d’un mot-clé.
def tmdb_keyword_id(query):
	string = 'tmdb_keyword_id_%s' % query
	url = '%s/search/keyword?api_key=%s&query=%s' % (base_url, tmdb_api_key(), quote_plus(str(query)))
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK)

# Recherche les mots-clés TMDb correspondant à une saisie utilisateur.
def tmdb_keyword_search(query):
	return tmdb_keyword_id(query) or {'results': []}


# Retourne la liste officielle des genres TMDb pour les films ou les séries.
# Cette liste est mise en cache afin d'éviter de maintenir une table locale
# rigide pour les boutons de genres des fenêtres d'info.
def tmdb_genre_list(media_type, language=None):
	media_type = str(media_type or '').lower().strip()
	media_type = 'movie' if media_type in ('movie', 'movies') else 'tv'
	language = language or get_language() or 'fr-FR'
	string = 'tmdb_genre_list_%s_%s' % (media_type, language)
	url = '%s/genre/%s/list?api_key=%s&language=%s' % (base_url, media_type, tmdb_api_key(), language)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK) or {'genres': []}

# Construit le dossier de sélection des mots-clés TMDb trouvés.
def tmdb_build_keyword_results(media_type, query):
	def _builder():
		for count, item in enumerate(results, 1):
			try:
				keyword_id = item.get('id')
				keyword_name = item.get('name', '')
				if not keyword_id or not keyword_name: continue
				mode = 'build_movie_list' if media_type == 'movie' else 'build_tvshow_list'
				action = 'tmdb_movies_keyword' if media_type == 'movie' else 'tmdb_tv_keyword'
				url_params = {'mode': mode, 'action': action, 'keyword_id': keyword_id, 'name': keyword_name.upper(), 'iconImage': 'tmdb.png'}
				url = kodi_utils.build_url(url_params)
				listitem = kodi_utils.make_listitem()
				listitem.setLabel('%02d | [B]%s[/B]' % (count, keyword_name.upper()))
				listitem.setArt({'icon': default_icon, 'poster': default_icon, 'thumb': default_icon, 'fanart': fanart, 'banner': default_icon})
				yield (url, listitem, True)
			except Exception as e:
				logger('tmdb keyword result error', str(e))
	__handle__ = int(sys.argv[1])
	default_icon = kodi_utils.media_path('tmdb.png')
	fanart = ''
	results = (tmdb_keyword_search(query).get('results') or [])
	items = list(_builder()) if results else []
	if not items:
		logger('tmdb keyword search', 'Aucun résultat trouvé | media_type=%s | query=%s' % (media_type, query))
		# Depuis une recherche globale de skin, plusieurs URLs de recherche peuvent
		# être appelées en rafale. Une notification pour chaque catégorie vide
		# bloque ou pollue l'interface du skin. Dans les menus internes alkoFlix,
		# on conserve le comportement historique.
		if kodi_utils.external_browse():
			kodi_utils.end_directory(__handle__, False)
		else:
			kodi_utils.notification('Aucun résultat trouvé')
			kodi_utils.end_directory(__handle__, False)
			kodi_utils.execute_builtin('Action(ParentDir)')
		return
	kodi_utils.add_items(__handle__, items)
	kodi_utils.set_content(__handle__, 'files')
	kodi_utils.end_directory(__handle__)
	kodi_utils.set_view_mode('view.main')

# Recherche l’identifiant TMDb d’une société.
def tmdb_company_id(query):
	string = 'tmdb_company_id_%s' % query
	url = '%s/search/company?api_key=%s&query=%s' % (base_url, tmdb_api_key(), query)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK)

# Récupère les images d’un média TMDb.
def tmdb_media_images(media_type, tmdb_id, include_languages=None):
	if media_type == 'movies': media_type = 'movie'
	image_languages = include_languages or image_language_query()
	string = 'tmdb_media_images_%s_%s_%s' % (media_type, tmdb_id, image_languages.replace(',', '_'))
	url = '%s/%s/%s/images?api_key=%s&include_image_language=%s' % (base_url, media_type, tmdb_id, tmdb_api_key(), image_languages)
	return _tmdb_cached_secondary(string, url, expiration=EXPIRES_1_WEEK, label='images')

# Récupère les vidéos d’un média TMDb.
def tmdb_media_videos(media_type, tmdb_id, language=None):
	if media_type == 'movies': media_type = 'movie'
	if media_type in ('tvshow', 'tvshows'): media_type = 'tv'
	language = str(language or '').strip()
	string = 'tmdb_media_videos_%s_%s_%s' % (media_type, tmdb_id, language or 'default')
	language_query = '&language=%s' % language if language else ''
	url = '%s/%s/%s/videos?api_key=%s%s' % (base_url, media_type, tmdb_id, tmdb_api_key(), language_query)
	return _tmdb_cached_secondary(string, url, expiration=EXPIRES_1_WEEK, label='videos')

# Lance une requête Discover pour les films.
def tmdb_movies_discover(query, page_no):
	string = query % page_no
	url = query % page_no
	return cache_object(get_tmdb, string, url, json=False)

# Récupère les films liés à un mot-clé TMDb.
def tmdb_movies_keyword(keyword_id, page_no, genre_sort=''):
	language = get_language() or 'fr-FR'
	url = '%s/discover/movie?api_key=%s&language=%s&with_keywords=%s&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), language, keyword_id, page_no)
	if genre_sort: return _tmdb_genre_discover('tmdb_movies_keyword_%s_%s' % (language, keyword_id), url, page_no, 'movie', genre_sort)
	return _tmdb_cached_secondary('tmdb_movies_keyword_%s_%s_%s' % (language, keyword_id, page_no), url, expiration=EXPIRES_2_DAYS, label='discover keyword')

def tmdb_movies_collection(collection_id):
	language = get_language() or 'fr-FR'
	string = 'tmdb_movies_collection_%s_%s' % (language, collection_id)
	url = '%s/collection/%s?api_key=%s&language=%s' % (base_url, collection_id, tmdb_api_key(), language)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK)

# Collections populaires/pertinentes affichées dans les dossiers Sagas.
# TMDb ne propose pas de endpoint Discover pour lister directement les collections.
# On utilise donc une sélection stable d’identifiants TMDb + des recherches de
# collections ciblées. Les recherches sont paginées par groupes de 20 pour éviter
# de limiter artificiellement les menus Sagas à quelques entrées seulement.
CURATED_COLLECTION_IDS = {
	'movie': (10, 119, 121938, 1241, 435259, 645, 87359, 295, 9485, 328, 87096, 8091, 399, 528, 2344, 404609, 1570, 5039, 1575, 84, 264, 2980, 86055, 230, 263, 748, 86311, 131292, 131295, 131296, 284433, 422834, 529892, 10194, 87118, 137697, 137696, 468222, 86066, 544669, 2150, 77816, 89137, 185103, 8354, 325470, 14740, 720879),
	'family': (10194, 87118, 137697, 137696, 468222, 86066, 544669, 2150, 77816, 89137, 185103, 8354, 325470, 14740, 720879, 10, 1241, 435259, 121938, 328, 87096, 86055, 2980, 264),
	'animation': (10194, 87118, 137697, 137696, 468222, 86066, 544669, 2150, 77816, 89137, 185103, 8354, 325470, 14740, 720879),
	'anime': (34055,)
}

CURATED_COLLECTION_TERMS = {
	'movie': (
		'James Bond', 'Star Wars', 'Harry Potter', 'Fantastic Beasts', 'Marvel', 'Avengers', 'Spider-Man', 'Iron Man', 'Thor', 'Captain America',
		'Black Panther', 'Guardians of the Galaxy', 'Ant-Man', 'Doctor Strange', 'X-Men', 'Deadpool', 'Wolverine', 'Fantastic Four', 'Venom', 'Batman',
		'Superman', 'Wonder Woman', 'Aquaman', 'Justice League', 'Suicide Squad', 'Joker', 'The Matrix', 'John Wick', 'Mission: Impossible', 'Jason Bourne',
		'Jack Ryan', 'Kingsman', 'The Equalizer', 'Taken', 'Die Hard', 'Lethal Weapon', 'Beverly Hills Cop', 'Bad Boys', 'Fast & Furious', 'The Transporter',
		'Expendables', 'Rambo', 'Rocky', 'Creed', 'Terminator', 'Predator', 'Alien', 'Prometheus', 'Blade Runner', 'Robocop',
		'Planet of the Apes', 'Jurassic Park', 'Godzilla', 'King Kong', 'Pacific Rim', 'Transformers', 'G.I. Joe', 'Independence Day', 'Men in Black', 'Ghostbusters',
		'Back to the Future', 'Indiana Jones', 'National Treasure', 'Pirates of the Caribbean', 'The Mummy', 'Tomb Raider', 'Jumanji', 'Night at the Museum', 'The Hunger Games', 'Divergent',
		'Maze Runner', 'Twilight', 'Narnia', 'The Lord of the Rings', 'The Hobbit', 'Dune', 'Percy Jackson', 'The Karate Kid', 'Cobra Kai', 'Police Academy',
		'Rush Hour', 'Shanghai Noon', 'Austin Powers', 'Johnny English', 'Naked Gun', 'Scary Movie', 'Scream', 'Halloween', 'Friday the 13th', 'A Nightmare on Elm Street',
		'The Conjuring', 'Annabelle', 'The Nun', 'Insidious', 'Sinister', 'Paranormal Activity', 'The Purge', 'A Quiet Place', 'It', 'Saw',
		'Final Destination', 'Evil Dead', 'The Texas Chainsaw Massacre', 'Child\'s Play', 'Leprechaun', 'Candyman', 'The Ring', 'The Grudge', 'Resident Evil', 'Underworld',
		'Blade', 'Hellboy', 'The Crow', 'Sherlock Holmes', 'Knives Out', 'Now You See Me', 'Ocean\'s', 'The Da Vinci Code', 'Hannibal Lecter', 'The Godfather',
		'Kingsman', 'Mamma Mia', 'Grease', 'Sister Act', 'The Addams Family', 'Beetlejuice', 'Ace Ventura', 'The Mask', 'Dumb and Dumber', 'Meet the Parents',
		'American Pie', 'Legally Blonde', 'Bridget Jones', 'Sex and the City', 'Magic Mike', 'The Hangover', '21 Jump Street', 'Neighbors', 'Ted', 'The Princess Diaries',
		'Home Alone', 'The Santa Clause', 'A Christmas Story', 'The Christmas Chronicles', 'The Chronicles of Riddick', 'Escape Plan', 'Sicario', 'The Hitman\'s Bodyguard', 'Mortal Kombat', 'Teenage Mutant Ninja Turtles'
	),
	'family': (
		'Harry Potter', 'Fantastic Beasts', 'Narnia', 'Percy Jackson', 'Jumanji', 'Night at the Museum', 'Home Alone', 'The Santa Clause', 'The Christmas Chronicles', 'The Princess Diaries',
		'Paddington', 'Garfield', 'Alvin and the Chipmunks', 'Scooby-Doo', 'The Smurfs', 'Sonic the Hedgehog', 'Pokémon', 'Spy Kids', 'Cheaper by the Dozen', 'Beethoven',
		'Air Bud', 'Babe', 'Stuart Little', 'Dr. Dolittle', 'The Muppets', 'Lassie', 'Free Willy', 'The Addams Family', 'Casper', 'Hocus Pocus',
		'Goosebumps', 'Diary of a Wimpy Kid', 'Marmaduke', 'Dolphin Tale', 'The Mighty Ducks', 'Nanny McPhee', 'Honey, I Shrunk the Kids', 'Richie Rich', 'Willy Wonka', 'Matilda'
	),
	'animation': (
		'Toy Story', 'Cars', 'Finding Nemo', 'The Incredibles', 'Monsters, Inc.', 'Inside Out', 'Frozen', 'Moana', 'Wreck-It Ralph', 'Zootopia',
		'Tangled', 'The Lion King', 'Aladdin', 'The Little Mermaid', 'Beauty and the Beast', 'Mulan', 'Lilo & Stitch', 'Brother Bear', 'Bambi', 'Dumbo',
		'Peter Pan', 'Cinderella', 'The Jungle Book', 'Winnie the Pooh', 'Shrek', 'Puss in Boots', 'Kung Fu Panda', 'How to Train Your Dragon', 'Madagascar', 'The Croods',
		'Trolls', 'The Boss Baby', 'Megamind', 'Despicable Me', 'Minions', 'The Secret Life of Pets', 'Sing', 'Rio', 'Ice Age', 'Hotel Transylvania',
		'Cloudy with a Chance of Meatballs', 'Open Season', 'Surf\'s Up', 'The Angry Birds Movie', 'The Lego Movie', 'Happy Feet', 'The SpongeBob Movie', 'The Simpsons Movie', 'The Peanuts Movie', 'Chicken Run',
		'Shaun the Sheep', 'Wallace & Gromit', 'The Land Before Time', 'An American Tail', 'Balto', 'All Dogs Go to Heaven', 'The Swan Princess', 'Alpha and Omega', 'Hoodwinked', 'The Nut Job',
		'Ferdinand', 'The Book of Life', 'The Boxtrolls', 'Coraline', 'ParaNorman', 'The Addams Family', 'The Mitchells vs. the Machines', 'Spider-Verse', 'Teen Titans Go', 'DC Super Hero Girls'
	),
	'anime': (
		'Pokémon', 'Dragon Ball Z', 'One Piece', 'Detective Conan', 'Doraemon', 'Naruto', 'Sailor Moon', 'My Hero Academia', 'Demon Slayer', 'Evangelion',
		'Attack on Titan', 'Jujutsu Kaisen', 'Bleach', 'Black Clover', 'Fairy Tail', 'Sword Art Online', 'Fate', 'Psycho-Pass', 'Ghost in the Shell', 'Cowboy Bebop',
		'Trigun', 'Fullmetal Alchemist', 'Made in Abyss', 'Overlord', 'Re:ZERO', 'KonoSuba', 'The Seven Deadly Sins', 'Violet Evergarden', 'Kaguya-sama', 'Quintessential Quintuplets',
		'Love Live!', 'Free!', 'Haikyuu!!', 'Kuroko\'s Basketball', 'Yowamushi Pedal', 'Initial D', 'Lupin the Third', 'City Hunter', 'Patlabor', 'Macross',
		'Gundam', 'Code Geass', 'Tengen Toppa Gurren Lagann', 'Made in Abyss', 'Puella Magi Madoka Magica', 'Cardcaptor Sakura', 'Digimon Adventure', 'Yo-kai Watch', 'Inazuma Eleven', 'Beyblade',
		'Studio Ghibli', 'Uta no Prince-sama', 'K-On!', 'Girls und Panzer', 'The Ancient Magus\' Bride', 'That Time I Got Reincarnated as a Slime', 'Is It Wrong to Try to Pick Up Girls in a Dungeon', 'Laid-Back Camp', 'Rascal Does Not Dream', 'Chainsaw Man'
	)
}

def _collection_search_first(query):
	data = tmdb_movies_search_collections(query, 1) or {}
	for item in data.get('results', []) or []:
		if item.get('id') and item.get('name'):
			return item
	return None

def _collection_entry_id(entry):
	return string(entry[1]) if isinstance(entry, tuple) else string(entry)

def _collection_entries(saga_type):
	ids = CURATED_COLLECTION_IDS.get(saga_type) or CURATED_COLLECTION_IDS.get('movie')
	terms = CURATED_COLLECTION_TERMS.get(saga_type) or CURATED_COLLECTION_TERMS.get('movie')
	entries = [('id', i) for i in ids]
	entries.extend([('term', t) for t in terms])
	return entries

def tmdb_movies_curated_collections(saga_type='movie', page_no=1):
	saga_type = str(saga_type or 'movie')
	try: page_no = int(page_no)
	except: page_no = 1
	entries = _collection_entries(saga_type)
	start, end = (page_no - 1) * 20, page_no * 20
	results, seen = [], set()
	for entry_type, entry_value in entries[start:end]:
		try:
			if entry_type == 'id': item = tmdb_movies_collection(entry_value) or {}
			else: item = _collection_search_first(entry_value) or {}
		except Exception:
			item = {}
		collection_id = item.get('id')
		if not collection_id or not item.get('name'): continue
		if collection_id in seen: continue
		seen.add(collection_id)
		results.append(item)
	return {'results': results, 'page': page_no, 'total_pages': max(1, (len(entries) + 19) // 20)}

# Recherche un film par titre et année.
def tmdb_movies_title_year(title, year=None):
	language = get_language() or 'fr-FR'
	if year:
		string = 'tmdb_movies_title_year_%s_%s_%s' % (language, title, year)
		url = '%s/search/movie?api_key=%s&language=%s&query=%s&year=%s' % (base_url, tmdb_api_key(), language, title, year)
	else:
		string = 'tmdb_movies_title_year_%s_%s' % (language, title)
		url = '%s/search/movie?api_key=%s&language=%s&query=%s' % (base_url, tmdb_api_key(), language, title)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_MONTH)

# Récupère les films populaires.
def tmdb_movies_popular(page_no, genre_sort=''):
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), page_no)
	return _tmdb_context_or_cache('tmdb_movies_popular', url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_trending(page_no, genre_sort=''):
	if genre_sort:
		url = '%s/discover/movie?api_key=%s&language=en-US&region=US&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), page_no)
		return _tmdb_genre_discover('tmdb_movies_trending', url, page_no, 'movie', genre_sort)
	string = 'tmdb_movies_trending_%s' % page_no
	url = '%s/trending/movie/week?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_api_key(), page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)

def tmdb_movies_random(page_no, genre_sort=''):
	base = '%s/discover/movie?api_key=%s&language=en-US&region=US&include_adult=false&vote_count.gte=50&sort_by=popularity.desc' % (base_url, tmdb_api_key())
	if genre_sort and genre_sort != 'random':
		return _tmdb_genre_discover('tmdb_movies_random', '%s&page=%s' % (base, page_no), page_no, 'movie', genre_sort)
	# Le vieux mode choisissait une seule page TMDb au hasard. Après les filtres
	# locaux (vus, animation, visuels absents, etc.), il pouvait ne rester que
	# quelques items. On construit plutôt un bassin caché puis on pagine proprement.
	return _tmdb_context_random_combined('tmdb_movies_random', base, page_no, 'movie', max_pages=4)

def tmdb_movies_top_rated(page_no, genre_sort=''):
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US&sort_by=vote_average.desc&vote_count.gte=100&page=%s' % (base_url, tmdb_api_key(), page_no)
	return _tmdb_context_or_cache('tmdb_movies_top_rated', url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_language(language_code, page_no):
	string = 'tmdb_movies_language_%s_%s' % (language_code, page_no)
	url = '%s/discover/movie?api_key=%s&language=en-US&region=%s' % (base_url, tmdb_api_key(), certification_country())
	url += '&with_original_language=%s&sort_by=popularity.desc&page=%s' % (language_code, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films disponibles chez un fournisseur TMDb.
def tmdb_movies_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		url = '%s/discover/movie?api_key=%s&language=en-US&watch_region=%s' % (base_url, tmdb_api_key(), current_region)
		url += '&with_watch_providers=%s&sort_by=popularity.desc&page=%s' % (provider_id, page_no)
		return url
	if genre_sort:
		current_region = _provider_region(region or certification_country())
		return _tmdb_genre_discover('tmdb_movies_watch_provider_%s_%s' % (provider_id, current_region), url_builder(current_region), page_no, 'movie', genre_sort)
	return _tmdb_provider_discover('tmdb_movies_watch_provider', provider_id, page_no, url_builder, region=region)

def _tmdb_movies_family_url(page_no, genres='10751', year=None, provider_id=None, region=None, sort_by='popularity.desc', recent=False):
	url = '%s/discover/movie?api_key=%s&language=en-US&region=FR' % (base_url, tmdb_api_key())
	url += '&include_adult=false&certification_country=FR&certification.lte=10'
	url += '&with_genres=%s&without_genres=27&sort_by=%s' % (genres, sort_by)
	if year: url += '&primary_release_year=%s' % year
	if provider_id:
		watch_region = _provider_region(region or 'FR')
		url += '&watch_region=%s&with_watch_providers=%s' % (watch_region, provider_id)
	if recent:
		current_date, previous_date = get_dates(181, reverse=True)
		url += '&release_date.gte=%s&release_date.lte=%s&with_release_type=4|5|3' % (previous_date, current_date)
	url += '&page=%s' % page_no
	return url

def _tmdb_movies_family_genres_value(base_genres, genre_id):
	genre_id = str(genre_id or '').strip()
	base_genres = str(base_genres or '').strip()
	if not genre_id: return base_genres
	if genre_id == base_genres or (base_genres == '10751' and genre_id == '10751') or (base_genres == '16' and genre_id == '16'):
		return base_genres
	return '%s,%s' % (base_genres, genre_id)

# Récupère les films familiaux par genre en conservant le filtre enfants/famille.
def tmdb_movies_family_genres(genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_movies_family_url(page_no, genres=_tmdb_movies_family_genres_value('10751', genre_id))
	return _tmdb_genre_discover('tmdb_movies_family_genres_%s' % genre_id, url, page_no, 'movie', genre_sort)

# Récupère les films d'animation familiaux par genre.
def tmdb_movies_family_animation_genres(genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_movies_family_url(page_no, genres=_tmdb_movies_family_genres_value('16', genre_id))
	return _tmdb_genre_discover('tmdb_movies_family_animation_genres_%s' % genre_id, url, page_no, 'movie', genre_sort)

# Récupère les films familiaux populaires avec certification française enfants/famille.
def tmdb_movies_family_popular(page_no):
	string = 'tmdb_movies_family_popular_%s' % page_no
	url = _tmdb_movies_family_url(page_no, genres='10751')
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère une page aléatoire de films familiaux.
def tmdb_movies_family_random(page_no):
	base = _url_without_page(_tmdb_movies_family_url(1, genres='10751'))
	return _tmdb_random_discover('tmdb_movies_family_random', base)

# Récupère les sorties récentes de films familiaux.
def tmdb_movies_family_latest_releases(page_no):
	string = 'tmdb_movies_family_latest_releases_%s' % page_no
	url = _tmdb_movies_family_url(page_no, genres='10751', sort_by='primary_release_date.desc', recent=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films familiaux par année.
def tmdb_movies_family_year(year, page_no):
	string = 'tmdb_movies_family_year_%s_%s' % (year, page_no)
	url = _tmdb_movies_family_url(page_no, genres='10751', year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films familiaux disponibles chez un fournisseur TMDb.
def tmdb_movies_family_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_movies_family_url(page_no, genres='10751', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_movies_family_watch_provider', provider_id, page_no, url_builder, 'movie', genre_sort, region=region, default_region='FR')

# Récupère les films famille/enfants disponibles chez un fournisseur TMDb.
def tmdb_movies_family_kids_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_movies_family_url(page_no, genres='10751|16', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_movies_family_kids_watch_provider', provider_id, page_no, url_builder, 'movie', genre_sort, region=region, default_region='FR')

# Récupère les films d'animation familiaux populaires.
def tmdb_movies_family_animation_popular(page_no):
	string = 'tmdb_movies_family_animation_popular_%s' % page_no
	url = _tmdb_movies_family_url(page_no, genres='16', sort_by='popularity.desc')
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère une page aléatoire de films d'animation familiaux.
def tmdb_movies_family_animation_random(page_no):
	base = _url_without_page(_tmdb_movies_family_url(1, genres='16', sort_by='popularity.desc'))
	return _tmdb_random_discover('tmdb_movies_family_animation_random', base)

# Récupère les sorties récentes de films d'animation familiaux.
def tmdb_movies_family_animation_latest_releases(page_no):
	string = 'tmdb_movies_family_animation_latest_releases_%s' % page_no
	url = _tmdb_movies_family_url(page_no, genres='16', sort_by='primary_release_date.desc', recent=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films d'animation familiaux par année.
def tmdb_movies_family_animation_year(year, page_no):
	string = 'tmdb_movies_family_animation_year_%s_%s' % (year, page_no)
	url = _tmdb_movies_family_url(page_no, genres='16', year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films d'animation familiaux disponibles chez un fournisseur TMDb.
def tmdb_movies_family_animation_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_movies_family_url(page_no, genres='16', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_movies_family_animation_watch_provider', provider_id, page_no, url_builder, 'movie', genre_sort, region=region, default_region='FR')

# Récupère les films les plus rentables.
def tmdb_movies_blockbusters(page_no, genre_sort=''):
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US&sort_by=revenue.desc&page=%s' % (base_url, tmdb_api_key(), page_no)
	return _tmdb_context_or_cache('tmdb_movies_blockbusters', url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_premieres(page_no, genre_sort=''):
	current_date, previous_date = get_dates(31, reverse=True)
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&release_date.gte=%s&release_date.lte=%s&with_release_type=1|3|2&page=%s' % (previous_date, current_date, page_no)
	return _tmdb_context_or_cache('tmdb_movies_premieres', url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_latest_releases(page_no, genre_sort=''):
	current_date, previous_date = get_dates(31, reverse=True)
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&release_date.gte=%s&release_date.lte=%s&with_release_type=4|5&page=%s' % (previous_date, current_date, page_no)
	return _tmdb_context_or_cache('tmdb_movies_latest_releases', url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_upcoming(page_no, genre_sort=''):
	current_date, future_date = get_dates(31, reverse=False)
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&release_date.gte=%s&release_date.lte=%s&with_release_type=3|2|1&page=%s' % (current_date, future_date, page_no)
	return _tmdb_context_or_cache('tmdb_movies_upcoming', url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_genres(genre_id, page_no, genre_sort='popular'):
	url = '%s/discover/movie?api_key=%s&with_genres=%s&language=en-US&region=US&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), genre_id, page_no)
	return _tmdb_genre_discover('tmdb_movies_genres_%s' % genre_id, url, page_no, 'movie', genre_sort)

# Récupère les films d’une année donnée.
def tmdb_movies_year(year, page_no, genre_sort=''):
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&certification_country=US&primary_release_year=%s&page=%s' % (year, page_no)
	return _tmdb_context_or_cache('tmdb_movies_year_%s' % year, url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_networks(network_id, page_no, genre_sort=''):
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&certification_country=US&with_companies=%s&page=%s' % (network_id, page_no)
	return _tmdb_context_or_cache('tmdb_movies_networks_%s' % network_id, url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_movies_company(company_id, page_no, genre_sort=''):
	# Raccourcis éditoriaux basés sur les compagnies TMDb (Pixar, Studio Ghibli, etc.).
	url = '%s/discover/movie?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&certification_country=US&with_companies=%s&page=%s' % (company_id, page_no)
	return _tmdb_context_or_cache('tmdb_movies_company_%s' % company_id, url, page_no, 'movie', genre_sort, expiration=EXPIRES_2_DAYS)

def _tmdb_public_list_page(list_id, page_no):
	string = 'tmdb_movies_public_list_%s_%s' % (list_id, page_no)
	url = '%s/list/%s?api_key=%s&language=en-US&page=%s' % (base_url, list_id, tmdb_api_key(), page_no)
	data = cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK) or {}
	return data if isinstance(data, dict) else {}


def _tmdb_public_list_movie_results(data):
	results = data.get('results') or data.get('items') or []
	return [i for i in results if isinstance(i, dict) and i.get('id') and i.get('media_type', 'movie') == 'movie']


def _tmdb_public_list_sorted_payload(list_id, page_no, list_sort='', list_random_seed=''):
	first_page = _tmdb_public_list_page(list_id, 1)
	try: total_pages = max(1, int(first_page.get('total_pages') or 1))
	except: total_pages = 1
	max_pages = min(total_pages, 25)
	results = _tmdb_public_list_movie_results(first_page)
	for source_page in range(2, max_pages + 1):
		results.extend(_tmdb_public_list_movie_results(_tmdb_public_list_page(list_id, source_page)))
	results = _tmdb_context_dedupe_results(results)
	try:
		from modules.list_sorting import normalise_list_sort_key
		list_sort = normalise_list_sort_key(list_sort)
	except:
		list_sort = str(list_sort or '').strip().lower()
	if list_sort.startswith('title'):
		results = sort_for_article(results, 'title', ignore_articles())
		if list_sort.endswith('_desc'): results.reverse()
	elif list_sort.startswith('date'):
		results.sort(key=lambda i: (_tmdb_context_date_value(i, 'movie'), i.get('popularity') or 0), reverse=list_sort.endswith('_desc'))
	elif list_sort.startswith('rating'):
		results.sort(key=lambda i: (float(i.get('vote_average') or 0), int(i.get('vote_count') or 0), i.get('popularity') or 0), reverse=list_sort.endswith('_desc'))
	elif list_sort == 'random':
		try: random.Random(str(list_random_seed or '')).shuffle(results)
		except: random.shuffle(results)
	template = dict(first_page or {})
	return _tmdb_context_paginated_payload(template, results, page_no, max_pages=max_pages)


def tmdb_movies_public_list(list_id, page_no, list_sort='', list_random_seed=''):
	# Lecture publique via TMDb v3 : ne dépend pas du compte TMDb de l'utilisateur.
	# Les tris contextuels restent locaux afin de préserver exactement le contenu de la liste.
	try:
		from modules.list_sorting import normalise_list_sort_key
		list_sort = normalise_list_sort_key(list_sort)
	except:
		list_sort = str(list_sort or '').strip().lower()
	if list_sort and list_sort != 'original':
		return _tmdb_public_list_sorted_payload(list_id, page_no, list_sort, list_random_seed)
	data = _tmdb_public_list_page(list_id, page_no)
	if not isinstance(data, dict): return {'page': page_no, 'total_pages': page_no, 'results': []}
	payload = dict(data)
	payload['page'] = int(payload.get('page') or page_no)
	payload['total_pages'] = int(payload.get('total_pages') or page_no)
	payload['results'] = _tmdb_public_list_movie_results(payload)
	return payload

def tmdb_movies_similar(tmdb_id, page_no):
	string = 'tmdb_movies_similar_%s_%s' % (tmdb_id, page_no)
	url = '%s/movie/%s/similar?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_id, tmdb_api_key(), page_no)
	return _tmdb_cached_secondary(string, url, expiration=EXPIRES_2_DAYS, label='similar')

# Récupère les recommandations liées à un film.
def tmdb_movies_recommendations(tmdb_id, page_no):
	string = 'tmdb_movies_recommendations_%s_%s' % (tmdb_id, page_no)
	url = '%s/movie/%s/recommendations?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_id, tmdb_api_key(), page_no)
	return _tmdb_cached_secondary(string, url, expiration=EXPIRES_2_DAYS, label='recommendations')

# Recherche des films par texte.
def tmdb_movies_search(query, page_no):
	language = get_language() or 'fr-FR'
	string = 'tmdb_movies_search_%s_%s_%s' % (language, query, page_no)
	url = '%s/search/movie?api_key=%s&language=%s&query=%s&page=%s' % (base_url, tmdb_api_key(), language, query, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)

# Recherche des collections de films par texte.
def tmdb_movies_search_collections(query, page_no):
	language = get_language() or 'fr-FR'
	string = 'tmdb_movies_search_collections_%s_%s_%s' % (language, query, page_no)
	url = '%s/search/collection?api_key=%s&language=%s&query=%s&page=%s' % (base_url, tmdb_api_key(), language, query, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK)

# Lance une requête Discover pour les séries TV.
def tmdb_tv_discover(query, page_no):
	string = url = query % page_no
	return cache_object(get_tmdb, string, url, json=False)

# Recherche une série TV par titre et année.
def tmdb_tv_title_year(title, year=None):
	language = get_language() or 'fr-FR'
	if year:
		string = 'tmdb_tv_title_year_%s_%s_%s' % (language, title, year)
		url = '%s/search/tv?api_key=%s&query=%s&first_air_date_year=%s&language=%s' % (base_url, tmdb_api_key(), title, year, language)
	else:
		string = 'tmdb_tv_title_year_%s_%s' % (language, title)
		url = '%s/search/tv?api_key=%s&query=%s&language=%s' % (base_url, tmdb_api_key(), title, language)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_MONTH)

# Récupère les séries TV populaires.
def tmdb_tv_popular(page_no, genre_sort=''):
	url = '%s/discover/tv?api_key=%s&language=en-US&region=US&include_null_first_air_dates=false&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), page_no)
	return _tmdb_context_or_cache('tmdb_tv_popular', url, page_no, 'tv', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_tv_trending(page_no, genre_sort=''):
	if genre_sort:
		url = '%s/discover/tv?api_key=%s&language=en-US&region=US&include_null_first_air_dates=false&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), page_no)
		return _tmdb_genre_discover('tmdb_tv_trending', url, page_no, 'tv', genre_sort)
	string = 'tmdb_tv_trending_%s' % page_no
	url = '%s/trending/tv/week?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_api_key(), page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)

def tmdb_tv_random(page_no, genre_sort=''):
	base = '%s/discover/tv?api_key=%s&language=en-US&region=US&include_adult=false&vote_count.gte=30&include_null_first_air_dates=false&sort_by=popularity.desc' % (base_url, tmdb_api_key())
	if genre_sort and genre_sort != 'random':
		return _tmdb_genre_discover('tmdb_tv_random', '%s&page=%s' % (base, page_no), page_no, 'tv', genre_sort)
	# Le vieux mode choisissait une seule page TMDb au hasard. Après les filtres
	# locaux (vus, animation, visuels absents, etc.), il pouvait ne rester que
	# quelques items. On construit plutôt un bassin caché puis on pagine proprement.
	return _tmdb_context_random_combined('tmdb_tv_random', base, page_no, 'tv', max_pages=4)

def tmdb_tv_top_rated(page_no, genre_sort=''):
	url = '%s/discover/tv?api_key=%s&language=en-US&vote_count.gte=50&include_null_first_air_dates=false&sort_by=vote_average.desc&page=%s' % (base_url, tmdb_api_key(), page_no)
	return _tmdb_context_or_cache('tmdb_tv_top_rated', url, page_no, 'tv', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_tv_airing_today(page_no, genre_sort=''):
	url = '%s/discover/tv?api_key=%s&language=en-US&region=%s&include_null_first_air_dates=false&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), certification_country(), page_no)
	return _tmdb_context_or_cache('tmdb_tv_airing_today', url, page_no, 'tv', genre_sort, expiration=EXPIRES_4_HOURS)

def tmdb_tv_airing_this_week(page_no, genre_sort=''):
	current_date, future_date = get_dates(7, reverse=False)
	url = '%s/discover/tv?api_key=%s&language=en-US&region=%s' % (base_url, tmdb_api_key(), certification_country())
	url += '&sort_by=popularity.desc&air_date.gte=%s&air_date.lte=%s&include_null_first_air_dates=false&page=%s' % (current_date, future_date, page_no)
	return _tmdb_context_or_cache('tmdb_tv_airing_this_week_%s' % current_date, url, page_no, 'tv', genre_sort, expiration=EXPIRES_4_HOURS)

def tmdb_tv_language(language_code, page_no):
	string = 'tmdb_tv_language_%s_%s' % (language_code, page_no)
	url = '%s/discover/tv?api_key=%s&language=en-US&region=%s' % (base_url, tmdb_api_key(), certification_country())
	url += '&with_original_language=%s&sort_by=popularity.desc&include_null_first_air_dates=false&page=%s' % (language_code, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les séries disponibles chez un fournisseur TMDb.
def tmdb_tv_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		url = '%s/discover/tv?api_key=%s&language=en-US&watch_region=%s' % (base_url, tmdb_api_key(), current_region)
		url += '&with_watch_providers=%s&sort_by=popularity.desc&include_null_first_air_dates=false&page=%s' % (provider_id, page_no)
		return url
	if genre_sort:
		current_region = _provider_region(region or certification_country())
		return _tmdb_genre_discover('tmdb_tv_watch_provider_%s_%s' % (provider_id, current_region), url_builder(current_region), page_no, 'tv', genre_sort)
	return _tmdb_provider_discover('tmdb_tv_watch_provider', provider_id, page_no, url_builder, region=region)

def _tmdb_tv_family_url(page_no, genres='10751|10762', year=None, provider_id=None, region=None, sort_by='popularity.desc', airing_week=False):
	url = '%s/discover/tv?api_key=%s&language=en-US&region=FR' % (base_url, tmdb_api_key())
	url += '&include_adult=false&include_null_first_air_dates=false'
	url += '&with_genres=%s&sort_by=%s' % (genres, sort_by)
	if year: url += '&first_air_date_year=%s' % year
	if provider_id:
		watch_region = _provider_region(region or 'FR')
		url += '&watch_region=%s&with_watch_providers=%s' % (watch_region, provider_id)
	if airing_week:
		current_date, future_date = get_dates(7, reverse=False)
		url += '&air_date.gte=%s&air_date.lte=%s' % (current_date, future_date)
	url += '&page=%s' % page_no
	return url

def _tmdb_tv_family_genres_value(base_genres, genre_id):
	genre_id = str(genre_id or '').strip()
	base_genres = str(base_genres or '').strip()
	if not genre_id: return base_genres
	if genre_id in ('10751', '10762') and base_genres == '10751|10762':
		return genre_id
	if genre_id == '16' and base_genres == '16,10762|16,10751':
		return base_genres
	if base_genres == '10751|10762':
		return '10751,%s|10762,%s' % (genre_id, genre_id)
	if base_genres == '16,10762|16,10751':
		return '16,10762,%s|16,10751,%s' % (genre_id, genre_id)
	return '%s,%s' % (base_genres, genre_id)

# Récupère les séries familiales par genre en conservant le filtre famille/jeunesse.
def tmdb_tv_family_genres(genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_tv_family_url(page_no, genres=_tmdb_tv_family_genres_value('10751|10762', genre_id))
	return _tmdb_genre_discover('tmdb_tv_family_genres_%s' % genre_id, url, page_no, 'tv', genre_sort)

# Récupère les séries d'animation jeunesse/famille par genre.
def tmdb_tv_family_animation_genres(genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_tv_family_url(page_no, genres=_tmdb_tv_family_genres_value('16,10762|16,10751', genre_id))
	return _tmdb_genre_discover('tmdb_tv_family_animation_genres_%s' % genre_id, url, page_no, 'tv', genre_sort)

# Récupère les séries familiales populaires.
def tmdb_tv_family_popular(page_no):
	string = 'tmdb_tv_family_popular_%s' % page_no
	url = _tmdb_tv_family_url(page_no, genres='10751|10762')
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère une page aléatoire de séries familiales.
def tmdb_tv_family_random(page_no):
	base = _url_without_page(_tmdb_tv_family_url(1, genres='10751|10762'))
	return _tmdb_random_discover('tmdb_tv_family_random', base)

# Récupère les séries familiales diffusées cette semaine.
def tmdb_tv_family_airing_this_week(page_no):
	current_date, future_date = get_dates(7, reverse=False)
	string = 'tmdb_tv_family_airing_this_week_%s_%s' % (current_date, page_no)
	url = _tmdb_tv_family_url(page_no, genres='10751|10762', airing_week=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)

# Récupère les séries familiales par année.
def tmdb_tv_family_year(year, page_no):
	string = 'tmdb_tv_family_year_%s_%s' % (year, page_no)
	url = _tmdb_tv_family_url(page_no, genres='10751|10762', year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les séries familiales disponibles chez un fournisseur TMDb.
def tmdb_tv_family_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_tv_family_url(page_no, genres='10751|10762', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_tv_family_watch_provider', provider_id, page_no, url_builder, 'tv', genre_sort, region=region, default_region='FR')

# Récupère les séries famille/enfants disponibles chez un fournisseur TMDb.
def tmdb_tv_family_kids_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_tv_family_url(page_no, genres='10751|10762|16,10762|16,10751', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_tv_family_kids_watch_provider', provider_id, page_no, url_builder, 'tv', genre_sort, region=region, default_region='FR')

# Récupère les séries d'animation jeunesse/famille populaires.
def tmdb_tv_family_animation_popular(page_no):
	string = 'tmdb_tv_family_animation_popular_%s' % page_no
	url = _tmdb_tv_family_url(page_no, genres='16,10762|16,10751')
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère une page aléatoire de séries d'animation jeunesse/famille.
def tmdb_tv_family_animation_random(page_no):
	base = _url_without_page(_tmdb_tv_family_url(1, genres='16,10762|16,10751'))
	return _tmdb_random_discover('tmdb_tv_family_animation_random', base)

# Récupère les séries d'animation jeunesse/famille diffusées cette semaine.
def tmdb_tv_family_animation_airing_this_week(page_no):
	current_date, future_date = get_dates(7, reverse=False)
	string = 'tmdb_tv_family_animation_airing_this_week_%s_%s' % (current_date, page_no)
	url = _tmdb_tv_family_url(page_no, genres='16,10762|16,10751', airing_week=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)

# Récupère les séries d'animation jeunesse/famille par année.
def tmdb_tv_family_animation_year(year, page_no):
	string = 'tmdb_tv_family_animation_year_%s_%s' % (year, page_no)
	url = _tmdb_tv_family_url(page_no, genres='16,10762|16,10751', year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les séries d'animation jeunesse/famille disponibles chez un fournisseur TMDb.
def tmdb_tv_family_animation_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_tv_family_url(page_no, genres='16,10762|16,10751', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_tv_family_animation_watch_provider', provider_id, page_no, url_builder, 'tv', genre_sort, region=region, default_region='FR')

# Récupère les séries TV lancées récemment.
def tmdb_tv_premieres(page_no, genre_sort=''):
	current_date, previous_date = get_dates(31, reverse=True)
	url = '%s/discover/tv?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&first_air_date.gte=%s&first_air_date.lte=%s&page=%s' % (previous_date, current_date, page_no)
	return _tmdb_recent_context_or_cache('tmdb_tv_premieres', url, page_no, 'tv', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_tv_upcoming(page_no, genre_sort=''):
	current_date, future_date = get_dates(31, reverse=False)
	url = '%s/discover/tv?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&first_air_date.gte=%s&first_air_date.lte=%s&page=%s' % (current_date, future_date, page_no)
	return _tmdb_recent_context_or_cache('tmdb_tv_upcoming', url, page_no, 'tv', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_tv_genres(genre_id, page_no, genre_sort='popular'):
	url = '%s/discover/tv?api_key=%s' % (base_url, tmdb_api_key())
	url += '&with_genres=%s&sort_by=popularity.desc&include_null_first_air_dates=false&page=%s' % (genre_id, page_no)
	return _tmdb_genre_discover('tmdb_tv_genres_%s' % genre_id, url, page_no, 'tv', genre_sort)

# Récupère les séries TV d’une année donnée.
def tmdb_tv_year(year, page_no, genre_sort=''):
	url = '%s/discover/tv?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&include_null_first_air_dates=false&first_air_date_year=%s&page=%s' % (year, page_no)
	return _tmdb_context_or_cache('tmdb_tv_year_%s' % year, url, page_no, 'tv', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_tv_networks(network_id, page_no, genre_sort=''):
	url = '%s/discover/tv?api_key=%s&language=en-US&region=US' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&include_null_first_air_dates=false&with_networks=%s&page=%s' % (network_id, page_no)
	return _tmdb_context_or_cache('tmdb_tv_networks_%s' % network_id, url, page_no, 'tv', genre_sort, expiration=EXPIRES_2_DAYS)

def tmdb_tv_similar(tmdb_id, page_no):
	string = 'tmdb_tv_similar_%s_%s' % (tmdb_id, page_no)
	url = '%s/tv/%s/similar?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_id, tmdb_api_key(), page_no)
	return _tmdb_cached_secondary(string, url, expiration=EXPIRES_2_DAYS, label='similar')

# Récupère les recommandations liées à une série.
def tmdb_tv_recommendations(tmdb_id, page_no):
	string = 'tmdb_tv_recommendations_%s_%s' % (tmdb_id, page_no)
	url = '%s/tv/%s/recommendations?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_id, tmdb_api_key(), page_no)
	return _tmdb_cached_secondary(string, url, expiration=EXPIRES_2_DAYS, label='recommendations')

# Recherche des séries TV par texte.
def tmdb_tv_search(query, page_no):
	language = get_language() or 'fr-FR'
	string = 'tmdb_tv_search_%s_%s_%s' % (language, query, page_no)
	url = '%s/search/tv?api_key=%s&language=%s&query=%s&page=%s' % (base_url, tmdb_api_key(), language, query, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)

# Récupère les séries liées à un mot-clé TMDb.
def tmdb_tv_keyword(keyword_id, page_no, genre_sort=''):
	language = get_language() or 'fr-FR'
	url = '%s/discover/tv?api_key=%s&language=%s&with_keywords=%s&include_null_first_air_dates=false&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), language, keyword_id, page_no)
	if genre_sort: return _tmdb_genre_discover('tmdb_tv_keyword_%s_%s' % (language, keyword_id), url, page_no, 'tv', genre_sort)
	return _tmdb_cached_secondary('tmdb_tv_keyword_%s_%s_%s' % (language, keyword_id, page_no), url, expiration=EXPIRES_2_DAYS, label='discover keyword')

def _tmdb_movies_documentary_url(page_no, genre_id='99', year=None, language_code=None, provider_id=None, region=None, sort_by='popularity.desc', recent=False):
	current_date, previous_date = get_dates(365, reverse=True)
	url = '%s/discover/movie?api_key=%s&language=en-US&region=%s' % (base_url, tmdb_api_key(), certification_country())
	url += '&include_adult=false&with_genres=%s&sort_by=%s' % (genre_id, sort_by)
	if year: url += '&primary_release_year=%s' % year
	if language_code: url += '&with_original_language=%s&primary_release_date.lte=%s' % (language_code, current_date)
	if provider_id:
		watch_region = _provider_region(region or certification_country())
		url += '&watch_region=%s&with_watch_providers=%s' % (watch_region, provider_id)
	if recent:
		url += '&release_date.gte=%s&release_date.lte=%s' % (previous_date, current_date)
	url += '&page=%s' % page_no
	return url


def tmdb_movies_documentary_popular(page_no):
	string = 'tmdb_movies_documentary_popular_%s' % page_no
	url = _tmdb_movies_documentary_url(page_no, genre_id='99')
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_documentary_random(page_no):
	base = _url_without_page(_tmdb_movies_documentary_url(1, genre_id='99'))
	return _tmdb_random_discover('tmdb_movies_documentary_random', base)


def tmdb_movies_documentary_trending(page_no):
	return _tmdb_trending_by_genre('movie', 'tmdb_movies_documentary_trending', '99', page_no)


def tmdb_movies_documentary_latest_releases(page_no):
	string = 'tmdb_movies_documentary_latest_releases_%s' % page_no
	url = _tmdb_movies_documentary_url(page_no, genre_id='99', sort_by='primary_release_date.desc', recent=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_documentary_year(year, page_no):
	string = 'tmdb_movies_documentary_year_%s_%s' % (year, page_no)
	url = _tmdb_movies_documentary_url(page_no, genre_id='99', year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_documentary_language(language_code, page_no):
	string = 'tmdb_movies_documentary_language_%s_%s' % (language_code, page_no)
	url = _tmdb_movies_documentary_url(page_no, genre_id='99', language_code=language_code)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_documentary_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_movies_documentary_url(page_no, genre_id='99', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_movies_documentary_watch_provider', provider_id, page_no, url_builder, 'movie', genre_sort, region=region)


def _tmdb_movies_documentary_genres_value(base_genres, genre_id):
	genre_id = str(genre_id or '').strip()
	base_genres = str(base_genres or '').strip()
	if not genre_id or genre_id == base_genres: return base_genres
	return '%s,%s' % (base_genres, genre_id)


def tmdb_movies_documentary_genres(genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_movies_documentary_url(page_no, genre_id=_tmdb_movies_documentary_genres_value('99', genre_id))
	return _tmdb_genre_discover('tmdb_movies_documentary_genres_%s' % genre_id, url, page_no, 'movie', genre_sort)


def tmdb_movies_tv_movie_popular(page_no):
	string = 'tmdb_movies_tv_movie_popular_%s' % page_no
	url = _tmdb_movies_documentary_url(page_no, genre_id='10770')
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_tv_movie_random(page_no):
	base = _url_without_page(_tmdb_movies_documentary_url(1, genre_id='10770'))
	return _tmdb_random_discover('tmdb_movies_tv_movie_random', base)


def tmdb_movies_tv_movie_trending(page_no):
	return _tmdb_trending_by_genre('movie', 'tmdb_movies_tv_movie_trending', '10770', page_no)


def tmdb_movies_tv_movie_latest_releases(page_no):
	string = 'tmdb_movies_tv_movie_latest_releases_%s' % page_no
	url = _tmdb_movies_documentary_url(page_no, genre_id='10770', sort_by='primary_release_date.desc', recent=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_tv_movie_year(year, page_no):
	string = 'tmdb_movies_tv_movie_year_%s_%s' % (year, page_no)
	url = _tmdb_movies_documentary_url(page_no, genre_id='10770', year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_tv_movie_language(language_code, page_no):
	string = 'tmdb_movies_tv_movie_language_%s_%s' % (language_code, page_no)
	url = _tmdb_movies_documentary_url(page_no, genre_id='10770', language_code=language_code)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def tmdb_movies_tv_movie_watch_provider(provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_movies_documentary_url(page_no, genre_id='10770', provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('tmdb_movies_tv_movie_watch_provider', provider_id, page_no, url_builder, 'movie', genre_sort, region=region)


def tmdb_movies_tv_movie_genres(genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_movies_documentary_url(page_no, genre_id=_tmdb_movies_documentary_genres_value('10770', genre_id))
	return _tmdb_genre_discover('tmdb_movies_tv_movie_genres_%s' % genre_id, url, page_no, 'movie', genre_sort)


# Construit une requête Discover documentaire pour les séries.
def _tmdb_tv_documentary_url(page_no, genre_id='99', year=None, language_code=None, provider_id=None, network_id=None, region=None, sort_by='popularity.desc', recent=False, airing_week=False):
	current_date, previous_date = get_dates(365, reverse=True)
	url = '%s/discover/tv?api_key=%s&language=en-US&region=%s' % (base_url, tmdb_api_key(), certification_country())
	url += '&include_adult=false&include_null_first_air_dates=false&with_genres=%s&sort_by=%s' % (genre_id, sort_by)
	if year: url += '&first_air_date_year=%s' % year
	if language_code: url += '&with_original_language=%s&first_air_date.lte=%s' % (language_code, current_date)
	if provider_id:
		watch_region = _provider_region(region or certification_country())
		url += '&watch_region=%s&with_watch_providers=%s' % (watch_region, provider_id)
	if network_id: url += '&with_networks=%s' % network_id
	if recent: url += '&first_air_date.gte=%s&first_air_date.lte=%s' % (previous_date, current_date)
	if airing_week:
		current_date, future_date = get_dates(7, reverse=False)
		url += '&air_date.gte=%s&air_date.lte=%s' % (current_date, future_date)
	url += '&page=%s' % page_no
	return url


def _tmdb_tv_documentary_popular(prefix, genre_id, page_no):
	string = '%s_popular_%s' % (prefix, page_no)
	url = _tmdb_tv_documentary_url(page_no, genre_id=genre_id)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def _tmdb_tv_documentary_random(prefix, genre_id):
	base = _url_without_page(_tmdb_tv_documentary_url(1, genre_id=genre_id))
	return _tmdb_random_discover('%s_random' % prefix, base)


def _tmdb_tv_documentary_trending(prefix, genre_id, page_no):
	return _tmdb_trending_by_genre('tv', '%s_trending' % prefix, genre_id, page_no)


def _tmdb_tv_documentary_airing_this_week(prefix, genre_id, page_no):
	current_date, future_date = get_dates(7, reverse=False)
	string = '%s_airing_this_week_%s_%s' % (prefix, current_date, page_no)
	url = _tmdb_tv_documentary_url(page_no, genre_id=genre_id, airing_week=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)


def _tmdb_tv_documentary_premieres(prefix, genre_id, page_no):
	string = '%s_premieres_%s' % (prefix, page_no)
	url = _tmdb_tv_documentary_url(page_no, genre_id=genre_id, sort_by='first_air_date.desc', recent=True)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def _tmdb_tv_documentary_year(prefix, genre_id, year, page_no):
	string = '%s_year_%s_%s' % (prefix, year, page_no)
	url = _tmdb_tv_documentary_url(page_no, genre_id=genre_id, year=year)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def _tmdb_tv_documentary_language(prefix, genre_id, language_code, page_no):
	string = '%s_language_%s_%s' % (prefix, language_code, page_no)
	url = _tmdb_tv_documentary_url(page_no, genre_id=genre_id, language_code=language_code)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def _tmdb_tv_documentary_watch_provider(prefix, genre_id, provider_id, page_no, region=None, genre_sort=''):
	def url_builder(current_region):
		return _tmdb_tv_documentary_url(page_no, genre_id=genre_id, provider_id=provider_id, region=current_region)
	return _tmdb_provider_context_discover('%s_watch_provider' % prefix, provider_id, page_no, url_builder, 'tv', genre_sort, region=region)


def _tmdb_tv_documentary_networks(prefix, genre_id, network_id, page_no):
	string = '%s_networks_%s_%s' % (prefix, network_id, page_no)
	url = _tmdb_tv_documentary_url(page_no, genre_id=genre_id, network_id=network_id)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)


def _tmdb_tv_documentary_genres_value(base_genres, genre_id):
	genre_id = str(genre_id or '').strip()
	base_genres = str(base_genres or '').strip()
	if not genre_id or genre_id == base_genres: return base_genres
	return '%s,%s' % (base_genres, genre_id)


def _tmdb_tv_documentary_genres(prefix, base_genres, genre_id, page_no, genre_sort='popular'):
	genre_id = str(genre_id or '').strip()
	url = _tmdb_tv_documentary_url(page_no, genre_id=_tmdb_tv_documentary_genres_value(base_genres, genre_id))
	return _tmdb_genre_discover('%s_genres_%s' % (prefix, genre_id), url, page_no, 'tv', genre_sort)


def tmdb_tv_documentary_popular(page_no): return _tmdb_tv_documentary_popular('tmdb_tv_documentary', '99', page_no)
def tmdb_tv_documentary_random(page_no): return _tmdb_tv_documentary_random('tmdb_tv_documentary', '99')
def tmdb_tv_documentary_trending(page_no): return _tmdb_tv_documentary_trending('tmdb_tv_documentary', '99', page_no)
def tmdb_tv_documentary_airing_this_week(page_no): return _tmdb_tv_documentary_airing_this_week('tmdb_tv_documentary', '99', page_no)
def tmdb_tv_documentary_premieres(page_no): return _tmdb_tv_documentary_premieres('tmdb_tv_documentary', '99', page_no)
def tmdb_tv_documentary_year(year, page_no): return _tmdb_tv_documentary_year('tmdb_tv_documentary', '99', year, page_no)
def tmdb_tv_documentary_language(language_code, page_no): return _tmdb_tv_documentary_language('tmdb_tv_documentary', '99', language_code, page_no)
def tmdb_tv_documentary_watch_provider(provider_id, page_no, region=None, genre_sort=''): return _tmdb_tv_documentary_watch_provider('tmdb_tv_documentary', '99', provider_id, page_no, region, genre_sort)
def tmdb_tv_documentary_networks(network_id, page_no): return _tmdb_tv_documentary_networks('tmdb_tv_documentary', '99', network_id, page_no)
def tmdb_tv_documentary_genres(genre_id, page_no, genre_sort='popular'): return _tmdb_tv_documentary_genres('tmdb_tv_documentary', '99', genre_id, page_no, genre_sort)

def tmdb_tv_reality_popular(page_no): return _tmdb_tv_documentary_popular('tmdb_tv_reality', '10764', page_no)
def tmdb_tv_reality_random(page_no): return _tmdb_tv_documentary_random('tmdb_tv_reality', '10764')
def tmdb_tv_reality_trending(page_no): return _tmdb_tv_documentary_trending('tmdb_tv_reality', '10764', page_no)
def tmdb_tv_reality_airing_this_week(page_no): return _tmdb_tv_documentary_airing_this_week('tmdb_tv_reality', '10764', page_no)
def tmdb_tv_reality_premieres(page_no): return _tmdb_tv_documentary_premieres('tmdb_tv_reality', '10764', page_no)
def tmdb_tv_reality_year(year, page_no): return _tmdb_tv_documentary_year('tmdb_tv_reality', '10764', year, page_no)
def tmdb_tv_reality_language(language_code, page_no): return _tmdb_tv_documentary_language('tmdb_tv_reality', '10764', language_code, page_no)
def tmdb_tv_reality_watch_provider(provider_id, page_no, region=None, genre_sort=''): return _tmdb_tv_documentary_watch_provider('tmdb_tv_reality', '10764', provider_id, page_no, region, genre_sort)
def tmdb_tv_reality_networks(network_id, page_no): return _tmdb_tv_documentary_networks('tmdb_tv_reality', '10764', network_id, page_no)
def tmdb_tv_reality_genres(genre_id, page_no, genre_sort='popular'): return _tmdb_tv_documentary_genres('tmdb_tv_reality', '10764', genre_id, page_no, genre_sort)

def tmdb_tv_talkshow_popular(page_no): return _tmdb_tv_documentary_popular('tmdb_tv_talkshow', '10767', page_no)
def tmdb_tv_talkshow_random(page_no): return _tmdb_tv_documentary_random('tmdb_tv_talkshow', '10767')
def tmdb_tv_talkshow_trending(page_no): return _tmdb_tv_documentary_trending('tmdb_tv_talkshow', '10767', page_no)
def tmdb_tv_talkshow_airing_this_week(page_no): return _tmdb_tv_documentary_airing_this_week('tmdb_tv_talkshow', '10767', page_no)
def tmdb_tv_talkshow_premieres(page_no): return _tmdb_tv_documentary_premieres('tmdb_tv_talkshow', '10767', page_no)
def tmdb_tv_talkshow_year(year, page_no): return _tmdb_tv_documentary_year('tmdb_tv_talkshow', '10767', year, page_no)
def tmdb_tv_talkshow_language(language_code, page_no): return _tmdb_tv_documentary_language('tmdb_tv_talkshow', '10767', language_code, page_no)
def tmdb_tv_talkshow_watch_provider(provider_id, page_no, region=None, genre_sort=''): return _tmdb_tv_documentary_watch_provider('tmdb_tv_talkshow', '10767', provider_id, page_no, region, genre_sort)
def tmdb_tv_talkshow_networks(network_id, page_no): return _tmdb_tv_documentary_networks('tmdb_tv_talkshow', '10767', network_id, page_no)
def tmdb_tv_talkshow_genres(genre_id, page_no, genre_sort='popular'): return _tmdb_tv_documentary_genres('tmdb_tv_talkshow', '10767', genre_id, page_no, genre_sort)

def tmdb_tv_news_popular(page_no): return _tmdb_tv_documentary_popular('tmdb_tv_news', '10763', page_no)
def tmdb_tv_news_random(page_no): return _tmdb_tv_documentary_random('tmdb_tv_news', '10763')
def tmdb_tv_news_trending(page_no): return _tmdb_tv_documentary_trending('tmdb_tv_news', '10763', page_no)
def tmdb_tv_news_airing_this_week(page_no): return _tmdb_tv_documentary_airing_this_week('tmdb_tv_news', '10763', page_no)
def tmdb_tv_news_premieres(page_no): return _tmdb_tv_documentary_premieres('tmdb_tv_news', '10763', page_no)
def tmdb_tv_news_year(year, page_no): return _tmdb_tv_documentary_year('tmdb_tv_news', '10763', year, page_no)
def tmdb_tv_news_language(language_code, page_no): return _tmdb_tv_documentary_language('tmdb_tv_news', '10763', language_code, page_no)
def tmdb_tv_news_watch_provider(provider_id, page_no, region=None, genre_sort=''): return _tmdb_tv_documentary_watch_provider('tmdb_tv_news', '10763', provider_id, page_no, region, genre_sort)
def tmdb_tv_news_networks(network_id, page_no): return _tmdb_tv_documentary_networks('tmdb_tv_news', '10763', network_id, page_no)
def tmdb_tv_news_genres(genre_id, page_no, genre_sort='popular'): return _tmdb_tv_documentary_genres('tmdb_tv_news', '10763', genre_id, page_no, genre_sort)

# Récupère les films d’animation japonaise populaires.
def tmdb_moviesanime_popular(page_no):
	string = 'tmdb_moviesanime_popular_%s' % page_no
	url = '%s/discover/movie?api_key=%s&page=%s&with_keywords=210024&sort_by=popularity.desc' % (base_url, tmdb_api_key(), page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère une page aléatoire de films d’animation japonaise.
def tmdb_moviesanime_random(page_no):
	base = '%s/discover/movie?api_key=%s&with_keywords=210024&include_adult=false&sort_by=popularity.desc' % (base_url, tmdb_api_key())
	return _tmdb_random_discover('tmdb_moviesanime_random', base)

# Récupère les films animés japonais selon leur langue d’origine.
def tmdb_moviesanime_language(language_code, page_no):
	string = 'tmdb_moviesanime_language_%s_%s' % (language_code, page_no)
	url = '%s/discover/movie?api_key=%s&with_keywords=210024' % (base_url, tmdb_api_key())
	url += '&with_original_language=%s&sort_by=popularity.desc&page=%s' % (language_code, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films animés japonais disponibles chez un fournisseur TMDb.
def tmdb_moviesanime_watch_provider(provider_id, page_no, region=None):
	def url_builder(current_region):
		url = '%s/discover/movie?api_key=%s&watch_region=%s&with_keywords=210024' % (base_url, tmdb_api_key(), current_region)
		url += '&with_watch_providers=%s&sort_by=popularity.desc&page=%s' % (provider_id, page_no)
		return url
	return _tmdb_provider_discover('tmdb_moviesanime_watch_provider', provider_id, page_no, url_builder, region=region)

# Récupère les dernières sorties de films d’animation japonaise.
def tmdb_moviesanime_latest_releases(page_no):
	current_date, previous_date = get_dates(181, reverse=True)
	string = 'tmdb_moviesanime_latest_releases_%s' % page_no
	url = '%s/discover/movie?api_key=%s&with_keywords=210024&sort_by=primary_release_date.desc' % (base_url, tmdb_api_key())
	url += '&release_date.gte=%s&release_date.lte=%s&with_release_type=4|5&page=%s' % (previous_date, current_date, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les films d’animation japonaise par genre.
def tmdb_moviesanime_genres(genre_id, page_no, genre_sort='popular'):
	url = '%s/discover/movie?api_key=%s&with_keywords=210024&with_genres=%s&sort_by=popularity.desc&page=%s' % (base_url, tmdb_api_key(), genre_id, page_no)
	return _tmdb_genre_discover('tmdb_moviesanime_genres_%s' % genre_id, url, page_no, 'movie', genre_sort)

# Récupère les films d’animation japonaise par année.
def tmdb_moviesanime_year(year, page_no):
	string = 'tmdb_moviesanime_year_%s_%s' % (year, page_no)
	url = '%s/discover/movie?api_key=%s&with_keywords=210024' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&certification_country=US&primary_release_year=%s&page=%s' % (year, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les séries d’animation japonaise populaires.
def tmdb_tvanime_popular(page_no):
	string = 'tmdb_tvanime_popular_%s' % page_no
	url = '%s/discover/tv?api_key=%s&page=%s&with_keywords=210024&sort_by=popularity.desc' % (base_url, tmdb_api_key(), page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère une page aléatoire de séries d’animation japonaise.
def tmdb_tvanime_random(page_no):
	base = '%s/discover/tv?api_key=%s&with_keywords=210024&include_null_first_air_dates=false&sort_by=popularity.desc' % (base_url, tmdb_api_key())
	return _tmdb_random_discover('tmdb_tvanime_random', base)

# Récupère les séries animées japonaises selon leur langue d’origine.
def tmdb_tvanime_language(language_code, page_no):
	string = 'tmdb_tvanime_language_%s_%s' % (language_code, page_no)
	url = '%s/discover/tv?api_key=%s&with_keywords=210024' % (base_url, tmdb_api_key())
	url += '&with_original_language=%s&sort_by=popularity.desc&include_null_first_air_dates=false&page=%s' % (language_code, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les séries animées japonaises disponibles chez un fournisseur TMDb.
def tmdb_tvanime_watch_provider(provider_id, page_no, region=None):
	def url_builder(current_region):
		url = '%s/discover/tv?api_key=%s&watch_region=%s&with_keywords=210024' % (base_url, tmdb_api_key(), current_region)
		url += '&with_watch_providers=%s&sort_by=popularity.desc&include_null_first_air_dates=false&page=%s' % (provider_id, page_no)
		return url
	return _tmdb_provider_discover('tmdb_tvanime_watch_provider', provider_id, page_no, url_builder, region=region)

# Récupère les séries d’animation japonaise récentes.
def tmdb_tvanime_premieres(page_no):
	current_date, previous_date = get_dates(181, reverse=True)
	string = 'tmdb_tvanime_premieres_%s' % page_no
	url = '%s/discover/tv?api_key=%s&with_keywords=210024&sort_by=first_air_date.desc' % (base_url, tmdb_api_key())
	url += '&first_air_date.gte=%s&first_air_date.lte=%s&page=%s' % (previous_date, current_date, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les séries d’animation japonaise par genre.
def tmdb_tvanime_genres(genre_id, page_no, genre_sort='popular'):
	url = '%s/discover/tv?api_key=%s&with_keywords=210024' % (base_url, tmdb_api_key())
	url += '&with_genres=%s&sort_by=popularity.desc&include_null_first_air_dates=false&page=%s' % (genre_id, page_no)
	return _tmdb_genre_discover('tmdb_tvanime_genres_%s' % genre_id, url, page_no, 'tv', genre_sort)

# Récupère les séries d’animation japonaise par année.
def tmdb_tvanime_year(year, page_no):
	string = 'tmdb_tvanime_year_%s_%s' % (year, page_no)
	url = '%s/discover/tv?api_key=%s&with_keywords=210024' % (base_url, tmdb_api_key())
	url += '&sort_by=popularity.desc&include_null_first_air_dates=false&first_air_date_year=%s&page=%s' % (year, page_no)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_2_DAYS)

# Récupère les personnes populaires sur TMDb.
def tmdb_popular_people(page_no):
	string = 'tmdb_popular_people_%s' % page_no
	url = '%s/person/popular?api_key=%s&language=en-US&page=%s' % (base_url, tmdb_api_key(), page_no)
	return cache_object(get_tmdb, string, url, False)

# Récupère une liste de personnes filtrée selon le type de média présent dans known_for.
def _tmdb_people_by_media(cache_prefix, endpoint, media_type, page_no):
	def _matches(item):
		try: return any(i.get('media_type') == media_type for i in item.get('known_for', []))
		except: return False
	try: page_no = int(page_no)
	except: page_no = 1
	results, source_total_pages = [], page_no
	# Trois pages source par page Kodi donnent une liste moins clairsemée après filtrage.
	start_page = ((page_no - 1) * 3) + 1
	for source_page in range(start_page, start_page + 3):
		string = '%s_%s_%s' % (cache_prefix, media_type, source_page)
		url = '%s/%s?api_key=%s&language=en-US&page=%s' % (base_url, endpoint, tmdb_api_key(), source_page)
		data = cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS) or {}
		try: source_total_pages = max(source_total_pages, int(data.get('total_pages', source_total_pages)))
		except: pass
		for item in data.get('results', []):
			if _matches(item): results.append(item)
	results = results[:20]
	total_pages = max(1, int((source_total_pages + 2) / 3))
	return {'results': results, 'page': page_no, 'total_pages': total_pages}

# Récupère les personnes populaires liées surtout aux films ou aux séries.
def tmdb_people_popular_media(media_type, page_no):
	return _tmdb_people_by_media('tmdb_people_popular_media', 'person/popular', media_type, page_no)

# Récupère les personnes tendances liées surtout aux films ou aux séries.
def tmdb_people_trending_media(media_type, page_no):
	return _tmdb_people_by_media('tmdb_people_trending_media', 'trending/person/week', media_type, page_no)

# Récupère les informations complètes d’une personne.
def tmdb_people_full_info(actor_id, language=None):
	if not language: language = get_language()
	string = 'tmdb_people_full_info_%s_%s' % (actor_id, language)
	url = '%s/person/%s?api_key=%s&language=%s&append_to_response=external_ids,combined_credits,images,tagged_images' % (base_url, actor_id, tmdb_api_key(), language)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_1_WEEK)

# Recherche une personne par nom.
def tmdb_people_info(query):
	string = 'tmdb_people_info_%s' % query
	url = '%s/search/person?api_key=%s&language=en-US&query=%s' % (base_url, tmdb_api_key(), query)
	return cache_object(get_tmdb, string, url, expiration=EXPIRES_4_HOURS)['results']

# Calcule une date passée ou future à partir d’aujourd’hui.
def get_dates(days, reverse=True):
	import datetime
	current_date = datetime.date.today()
	if reverse: new_date = (current_date - datetime.timedelta(days=days)).strftime('%Y-%m-%d')
	else: new_date = (current_date + datetime.timedelta(days=days)).strftime('%Y-%m-%d')
	return str(current_date), new_date

# Récupère les détails complets d’un film.
def movie_details(tmdb_id, language, tmdb_api=None):
	try:
		url = '%s/movie/%s?api_key=%s&language=%s&append_to_response=%s&include_image_language=%s' % (base_url, tmdb_id, get_tmdb_api(tmdb_api), language, movies_append, image_language_query())
		return get_tmdb(url)
	except: return None

# Récupère les détails complets d’une série TV.
def tvshow_details(tmdb_id, language, tmdb_api=None):
	try:
		url = '%s/tv/%s?api_key=%s&language=%s&append_to_response=%s&include_image_language=%s' % (base_url, tmdb_id, get_tmdb_api(tmdb_api), language, tvshows_append, image_language_query())
		return get_tmdb(url)
	except: return None


# Récupère les crédits agrégés d'une série TV.
# Certains titres TMDb (surtout les anthologies) n'ont pas de distribution dans
# /tv/{id}?append_to_response=credits, alors que /aggregate_credits contient bien
# les acteurs affichés par TMDb Helper. On garde ce fallback caché séparément.
def tvshow_aggregate_credits(tmdb_id, language=None, tmdb_api=None):
	try:
		if not language: language = get_language()
		string = 'tmdb_tvshow_aggregate_credits_%s_%s' % (tmdb_id, language)
		url = '%s/tv/%s/aggregate_credits?api_key=%s&language=%s' % (base_url, tmdb_id, get_tmdb_api(tmdb_api), language)
		return _tmdb_cached_secondary(string, url, expiration=EXPIRES_1_WEEK, label='aggregate_credits')
	except: return None

# Récupère les détails des épisodes d’une saison.
def season_episodes_details(tmdb_id, season_no, language, tmdb_api=None):
	try:
		url = '%s/tv/%s/season/%s?api_key=%s&language=%s&append_to_response=credits' % (base_url, tmdb_id, season_no, get_tmdb_api(tmdb_api), language)
		return get_tmdb(url, False)
	except: return None

# Trouve un film TMDb à partir d’un identifiant externe.
def movie_external_id(external_source, external_id, tmdb_api=None):
	try:
		string = 'movie_external_id_%s_%s' % (external_source, external_id)
		url = '%s/find/%s?api_key=%s&external_source=%s' % (base_url, external_id, get_tmdb_api(tmdb_api), external_source)
		result = cache_function(get_tmdb, string, url, EXPIRES_1_MONTH)
		result = result['movie_results']
		if result: return result[0]
		else: return None
	except: return None

# Trouve une série TMDb à partir d’un identifiant externe.
def tvshow_external_id(external_source, external_id, tmdb_api=None):
	try:
		string = 'tvshow_external_id_%s_%s' % (external_source, external_id)
		url = '%s/find/%s?api_key=%s&external_source=%s' % (base_url, external_id, get_tmdb_api(tmdb_api), external_source)
		result = cache_function(get_tmdb, string, url, EXPIRES_1_MONTH)
		result = result['tv_results']
		if result: return result[0]
		else: return None
	except: return None

# Recherche un film exact par titre et année.
def movie_title_year(title, year, tmdb_api=None):
	try:
		string = 'movie_title_year_%s_%s' % (title, year)
		url = '%s/search/movie?api_key=%s&query=%s&year=%s&page=1' % (base_url, get_tmdb_api(tmdb_api), title, year)
		result = cache_function(get_tmdb, string, url, EXPIRES_1_MONTH)
		result = result['results']
		if result: return result[0]
		else: return None
	except: return None

# Recherche une série exacte par titre et année.
def tvshow_title_year(title, year, tmdb_api=None):
	try:
		string = 'tvshow_title_year_%s_%s' % (title, year)
		url = '%s/search/tv?api_key=%s&query=%s&first_air_date_year=%s' % (base_url, get_tmdb_api(tmdb_api), title, year)
		result = cache_function(get_tmdb, string, url, EXPIRES_1_MONTH)
		result = result['results']
		if result: return result[0]
		else: return None
	except: return None

# Récupère les mots-clés associés à un film.
def movie_keywords(tmdb_id, tmdb_api=None):
	try:
		string = 'tmdb_movie_keywords_%s' % tmdb_id
		url = '%s/movie/%s/keywords?api_key=%s' % (base_url, tmdb_id, get_tmdb_api(tmdb_api))
		result = _tmdb_cached_secondary(string, url, expiration=EXPIRES_2_DAYS, label='keywords') or {}
		result = result.get('keywords') if isinstance(result, dict) else None
		return result
	except: return None

# Récupère les traductions anglaises d’un média.
def english_translation(media_type, tmdb_id, tmdb_api=None):
	try:
		string = 'english_translation_%s_%s' % (media_type, tmdb_id)
		url = '%s/%s/%s/translations?api_key=%s' % (base_url, media_type, tmdb_id, get_tmdb_api(tmdb_api))
		result = cache_function(get_tmdb, string, url, EXPIRES_1_WEEK * 52)
		try: result = result['translations']
		except: result = None
		return result
	except: return None

# Retourne la clé API TMDb à utiliser.
def get_tmdb_api(tmdb_api):
	return tmdb_api or tmdb_api_key()

# Récupère les groupes d’épisodes disponibles pour une série.
def episode_groups(tmdb_id, tmdb_api=None):
	string = 'tmdb_episode_group_%s' % tmdb_id
	url = '%s/tv/%s/episode_groups?api_key=%s' % (base_url, tmdb_id, get_tmdb_api(tmdb_api))
	result = cache_function(get_tmdb, string, url, EXPIRES_1_WEEK)
	for i in result['results']: i['type'] = eps_map[i['type']]
	result = result['results']
	return result

# Récupère les détails d’un groupe d’épisodes.
def episode_group_details(group_id, tmdb_api=None):
	try:
		string = 'tmdb_episode_group_details_%s' % group_id
		url = '%s/tv/episode_group/%s?api_key=%s' % (base_url, group_id, get_tmdb_api(tmdb_api))
		result = cache_function(get_tmdb, string, url, EXPIRES_1_WEEK)
		result = sorted(result['groups'], key=lambda k: k['order'])
		return result
	except: return []

# Récupère et trie la watchlist TMDb.
def tmdb_watchlist(media_type, page, letter):
	title, premiered = ('name', 'first_air_date') if media_type == 'tv' else ('title', 'release_date')
	original_list = all_items(watchlist, media_type)
	if not show_unaired_watchlist():
		current_date = get_datetime()
		str_format = '%Y-%m-%d'
		original_list = [i for i in original_list if i.get(premiered) and js2date(i.get(premiered), str_format, remove_time=True) <= current_date]
	sort_key = lists_sort_order('watchlist')
	reverse = lists_sort_reverse('watchlist')
	if   sort_key == 2: original_list.sort(key=lambda k: k.get(premiered) or '', reverse=reverse)
	elif sort_key == 1:
		# L'appel TMDb renvoie déjà la watchlist par created_at.desc.
		if not reverse: original_list.reverse()
	else:
		original_list = sort_for_article(original_list, title, ignore_articles())
		if reverse: original_list.reverse()
	if paginate():
		limit = page_limit()
		final_list, total_pages = paginate_list(original_list, page, letter, limit)
	else: final_list, total_pages = original_list, 1
	return final_list, total_pages

# Récupère et trie les favoris TMDb.
def tmdb_favorite(media_type, page, letter):
	title, premiered = ('name', 'first_air_date') if media_type == 'tv' else ('title', 'release_date')
	original_list = all_items(favorite, media_type)
	sort_key = lists_sort_order('favorites')
	reverse = lists_sort_reverse('favorites')
	if sort_key == 2: original_list.sort(key=lambda k: k.get(premiered) or '', reverse=reverse)
	elif sort_key == 1:
		# L'appel TMDb renvoie déjà les favoris par created_at.desc.
		if not reverse: original_list.reverse()
	else:
		original_list = sort_for_article(original_list, title, ignore_articles())
		if reverse: original_list.reverse()
	if paginate():
		limit = page_limit()
		final_list, total_pages = paginate_list(original_list, page, letter, limit)
	else: final_list, total_pages = original_list, 1
	return final_list, total_pages

# Récupère les recommandations du compte TMDb.
def tmdb_recommendations(media_type, page, letter):
	original_list = recommendations(media_type, page)
	final_list, total_pages = original_list['results'], original_list['total_pages']
	return final_list, total_pages

# Ajoute ou retire un élément de la watchlist ou des favoris.
def add_to_watchlist_favorite(item, list_type):
	session_account_id = get_setting('tmdb.session_account_id')
	session_id = get_setting('tmdb.session_id')
	params = {'session_id': session_id}
	url = '%s/account/%s/%s' % (base_url, session_account_id, list_type)
	return list_request(url, params=params, data=item, method='post')

# Injecte automatiquement l’identifiant du compte TMDb.
def _account_id(func):
	def wrapper(*args, **kwargs):
		kwargs['account_id'] = kwargs.get('account_id') or get_setting('tmdb.account_id')
		result = func(*args, **kwargs)
		return result
	return wrapper

# Récupère toutes les pages d’une liste TMDb.
def all_items(func, *args):
	def _process(f, *a):
		r = f(*a)
		results[a[-1]] = r['results']
		return r['total_pages']
	results, page, total_pages = {}, 1, 1
	total_pages = _process(func, *args, page)
	threads = TaskPool(40).tasks(_process, [(func, *args, i) for i in range(page + 1, total_pages + 1)], Thread)
	[i.join() for i in threads]
	results = [item for items in sorted(results.items()) for item in items[1]]
	return results

# Récupère et trie toutes les listes de l’utilisateur.
def user_lists_all():
	sort = int(get_setting('tmdblist.sort_name', '0'))
	results = all_items(user_lists)
	try:
		if   sort == 2: results.sort(key=lambda k: k['updated_at'], reverse=True)
		elif sort == 1: results.sort(key=lambda k: k['number_of_items'], reverse=True)
		else: results.sort(key=lambda k: k['name'].lower(), reverse=False)
	except: pass
	return results

list_obj = {'description': '', 'name': '', 'iso_3166_1': 'US', 'iso_639_1': 'en', 'public': True}
list_url = 'https://api.themoviedb.org/4'
list_heading = ls(33179)

# Effectue une requête authentifiée sur l’API TMDb v4.
def list_request(url, params=None, data=None, method=None):
	access_token = get_setting('tmdb.token')
	headers = {'Authorization': f"Bearer {access_token}"}
	method = method or 'get'
	list_timeout=timeout ** 2 if not method in ('get',) else timeout
	try:
		response = session.request(method, url, params=params, json=data, headers=headers, timeout=list_timeout)
		result = response.json() if 'json' in response.headers.get('Content-Type', '') else response.text
		if not response.ok: response.raise_for_status()
		return result
	except requests.exceptions.RequestException as e:
		logger('tmdb error', str(e))

# Récupère les détails d’une liste TMDb.
def list_details(list_id, page=1):
	string = 'tmdblist_detail_%s_%s' % (list_id, page)
	url = '%s/list/%s?page=%s' % (list_url, list_id, page)
	return cache_object(list_request, string, url, json=False)

# Ajoute des éléments à une liste TMDb.
def list_add_items(list_id, items=None):
	url = '%s/list/%s/items' % (list_url, list_id)
	return list_request(url, data=items, method='post')

# Retire des éléments d’une liste TMDb.
def list_remove_items(list_id, items=None):
	url = '%s/list/%s/items' % (list_url, list_id)
	return list_request(url, data=items, method='delete')

# Met à jour les informations d’une liste TMDb.
def list_update(list_id, data):
	url = '%s/list/%s' % (list_url, list_id)
	return list_request(url, data=data, method='put')

# Vérifie si un élément est présent dans une liste TMDb.
def list_status(list_id, media_type, media_id):
	params = {'media_type': media_type, 'media_id': int(media_id)}
	url = '%s/list/%s/item_status' % (list_url, list_id)
	return list_request(url, params=params)

# Crée une nouvelle liste TMDb.
def list_create(item):
	url = '%s/list' % list_url
	return list_request(url, data=item, method='post')

# Vide une liste TMDb.
def list_clear(list_id):
	url = '%s/list/%s/clear' % (list_url, list_id)
	return list_request(url)

# Supprime une liste TMDb.
def list_delete(list_id):
	url = '%s/list/%s' % (list_url, list_id)
	return list_request(url, method='delete')

@_account_id
# Récupère les listes de l’utilisateur TMDb.
def user_lists(page=1, account_id=''):
	string = 'tmdblist_user_lists_%s' % page
	url = '%s/account/%s/lists?page=%s' % (list_url, account_id, page)
	return cache_object(list_request, string, url, json=False)

@_account_id
# Récupère la watchlist TMDb d’un type de média.
def watchlist(media_type, page=1, account_id=''):
	string = 'tmdblist_watchlist_%s_%s_%s' % (account_id, media_type, page)
	url = '%s/account/%s/%s/watchlist' % (list_url, account_id, media_type)
	url += '?page=%s&language=en-US&sort_by=created_at.desc' % page
	return cache_object(list_request, string, url, json=False)

@_account_id
# Récupère les favoris TMDb d’un type de média.
def favorite(media_type, page=1, account_id=''):
	string = 'tmdblist_favorite_%s_%s_%s' % (account_id, media_type, page)
	url = '%s/account/%s/%s/favorites' % (list_url, account_id, media_type)
	url += '?page=%s&language=en-US&sort_by=created_at.desc' % page
	return cache_object(list_request, string, url, json=False)

@_account_id
# Récupère les recommandations TMDb d’un type de média.
def recommendations(media_type, page=1, account_id=''):
	string = 'tmdblist_recommendations_%s_%s_%s' % (account_id, media_type, page)
	url = '%s/account/%s/%s/recommendations' % (list_url, account_id, media_type)
	url += '?page=%s&language=en-US' % page
	return cache_object(list_request, string, url, json=False)


def _tmdb_account_for_ratings():
	return get_setting('tmdb.session_account_id') or get_setting('tmdb.account_id')

def _tmdb_rating_path(params):
	media_type = params.get('media_type')
	if media_type == 'movie': return 'movie/%s' % params.get('tmdb_id')
	if media_type == 'episode': return 'tv/%s/season/%s/episode/%s' % (params.get('tmdb_id'), params.get('season'), params.get('episode'))
	if media_type == 'season': return None
	return 'tv/%s' % params.get('tmdb_id')

def tmdb_set_rating(params, rating):
	session_id = get_setting('tmdb.session_id')
	path = _tmdb_rating_path(params)
	if not path: return False
	url = '%s/%s/rating' % (base_url, path)
	result = list_request(url, params={'session_id': session_id}, data={'value': float(rating)}, method='post')
	success = bool(result) and result.get('success', True)
	if success: clear_tmdbl_cache()
	return success

def tmdb_remove_rating(params):
	session_id = get_setting('tmdb.session_id')
	path = _tmdb_rating_path(params)
	if not path: return False
	url = '%s/%s/rating' % (base_url, path)
	result = list_request(url, params={'session_id': session_id}, method='delete')
	success = bool(result) and result.get('success', True)
	if success: clear_tmdbl_cache()
	return success

def _tmdb_rated_endpoint(media_type, page=1):
	account_id = _tmdb_account_for_ratings()
	path = {'movie': 'movies', 'tvshow': 'tv', 'episode': 'tv/episodes'}[media_type]
	url = '%s/account/%s/rated/%s' % (base_url, account_id, path)
	url += '?session_id=%s&page=%s&sort_by=created_at.desc' % (get_setting('tmdb.session_id'), page)
	return list_request(url)

def tmdb_rated_all(media_type):
	def _process(page):
		result = _tmdb_rated_endpoint(media_type, page)
		results[page] = result.get('results', []) if result else []
		return result.get('total_pages', 1) if result else 1
	results, page = {}, 1
	total_pages = _process(page)
	threads = TaskPool(20).tasks(_process, [(i,) for i in range(page + 1, total_pages + 1)], Thread)
	[i.join() for i in threads]
	return [item for items in sorted(results.items()) for item in items[1]]

def tmdb_get_rating(params):
	media_type = params.get('media_type')
	if media_type == 'season': return None
	type_key = 'episode' if media_type == 'episode' else 'tvshow' if media_type == 'tvshow' else 'movie'
	for item in tmdb_rated_all(type_key):
		try:
			item_id = item.get('show_id') or item.get('series_id') or item.get('id') if media_type == 'episode' else item.get('id')
			if str(item_id) != str(params.get('tmdb_id')): continue
			if media_type == 'episode':
				if int(item.get('season_number')) != int(params.get('season')) or int(item.get('episode_number')) != int(params.get('episode')): continue
			return item.get('rating')
		except: pass
	return None

# Nettoie la watchlist TMDb des éléments déjà vus.
def tmdb_clean_watchlist(silent=False):
	if not get_setting('tmdb.token'): return
	if not silent and not kodi_utils.confirm_dialog(): return
	try:
		from caches.watched_cache import get_watched_items, get_in_progress_tvshows
		watchlist_ids = []
		watchlist_ids += all_items(watchlist, 'movie')
		watchlist_ids += all_items(watchlist, 'tv')
		watchlist_ids = [str(i['id']) for i in watchlist_ids]
		m = get_watched_items('movie', 1, 'None', False)
		t = get_watched_items('tvshow', 1, 'None', False)
		p = get_in_progress_tvshows('tvshow', 1, 'None', False)
		items = []
		items += [
			{'watchlist': False, 'media_type': 'movie', 'media_id': i['media_id']}
			for i in m[0] if i['media_id'] in watchlist_ids
		]
		items += [
			{'watchlist': False, 'media_type': 'tv', 'media_id': i['media_id']}
			for i in t[0] + p[0] if i['media_id'] in watchlist_ids
		]
		if not items: return '0 items to remove.'
		threads = TaskPool(40).tasks(add_to_watchlist_favorite, [(i, 'watchlist') for i in items], Thread)
		[i.join() for i in threads]
		clear_tmdbl_cache()
		if not silent: kodi_utils.notification(32576)
		return '%d items removed.' % len(items)
	except: pass

# Importe la watchlist Trakt dans la watchlist TMDb.
def import_trakt_watchlist(*args):
	if not kodi_utils.confirm_dialog(): return
	def _process(group, count):
		add_to_watchlist_favorite(group, 'watchlist')
		progressBG.update(int(count / len_items * 100), send_str)
	from indexers.trakt_api import trakt_fetch_collection_watchlist
	send_str = ls(33152)
	try:
		progressBG = kodi_utils.progressDialogBG
		progressBG.create(send_str, list_heading)
		items = []
		for i in (('movie', 'movie'), ('show', 'tv')):
			try: items += [
				{'collected_at': item['collected_at'], 'watchlist': True, 'media_type': i[1], 'media_id': item['media_ids']['tmdb']}
				for item in trakt_fetch_collection_watchlist('watchlist', i[0]) if 'tmdb' in item['media_ids']
			]
			except: pass
		if not items: return kodi_utils.notification(32760)
		try: items.sort(key=lambda k: k['collected_at'], reverse=False)
		except: pass
		len_items = len(items)
		threads = TaskPool(40).tasks(_process, [(i[1], i[0]) for i in enumerate(items, 1)], Thread)
		[i.join(3/4) for i in threads]
		clear_tmdbl_cache()
		kodi_utils.notification(ls(33153))
	except: kodi_utils.notification(32574)
	finally: progressBG.close()

# Importe une liste Trakt dans une liste TMDb.
def import_trakt_list(params):
	from indexers.trakt_api import get_trakt_list_contents
	send_str = ls(33152)
	try:
		progressBG = kodi_utils.progressDialogBG
		progressBG.create(send_str, list_heading)
		list_id, user, slug = params['trakt_list_id'], params['user'], params['list_slug']
		items = get_trakt_list_contents(params.get('list_type'), list_id, user, slug)
		len_items, wait = len(items), sum(1000 for i in chunks(items, 500))
		for count, item in enumerate(items, 1):
			kodi_utils.sleep(int(wait / len_items))
			if (mtype := item['type']) in ('movie', 'show') and 'tmdb' in item[mtype]['ids'] and item[mtype]['ids']['tmdb']:
				item['export'] = {'media_type': 'tv' if mtype == 'show' else mtype, 'media_id': item[mtype]['ids']['tmdb']}
			else: item['export'] = None
			progressBG.update(int(count / len_items * 100), send_str)
		items = {'items': [i['export'] for i in items if i['export']]}
		Thread(target=list_add_items, args=(params['list_id'], items)).start()
		clear_tmdbl_cache()
	except: kodi_utils.notification(32574)
	else: kodi_utils.notification(ls(33153))
	finally: progressBG.close()

# Importe une liste MDBList dans une liste TMDb.
def import_mdbl_list(params):
	from indexers.mdblist_api import mdbl_list_items
	send_str = ls(33154)
	try:
		progressBG = kodi_utils.progressDialogBG
		progressBG.create(send_str, list_heading)
		items = mdbl_list_items(params['mdbl_list_id'], None)
		len_items, wait = len(items), sum(1000 for i in chunks(items, 500))
		for count, item in enumerate(items, 1):
			kodi_utils.sleep(int(wait / len_items))
			if (mtype := item['mediatype']) in ('movie', 'show') and item['id']:
				item['export'] = {'media_type': 'tv' if mtype == 'show' else mtype, 'media_id': item['id']}
			else: item['export'] = None
			progressBG.update(int(count / len_items * 100), send_str)
		items = {'items': [i['export'] for i in items if i['export']]}
		Thread(target=list_add_items, args=(params['list_id'], items)).start()
		clear_tmdbl_cache()
	except: kodi_utils.notification(32574)
	else: kodi_utils.notification(ls(33153))
	finally: progressBG.close()

# Efface le cache local lié aux listes TMDb.
def clear_tmdbl_cache(silent=False):
	from modules.kodi_utils import path_exists, clear_property, database_connect, maincache_db
	try:
		if not path_exists(maincache_db): return True
		dbcon = database_connect(maincache_db, isolation_level=None)
		dbcur = dbcon.cursor()
		dbcur.execute("""PRAGMA synchronous = OFF""")
		dbcur.execute("""PRAGMA journal_mode = OFF""")
		dbcur.execute("""SELECT id FROM maincache WHERE id LIKE ?""", ('tmdblist_%',))
		tmdb_results = [str(i[0]) for i in dbcur.fetchall()]
		if not tmdb_results: return True
		dbcur.execute("""DELETE FROM maincache WHERE id LIKE ?""", ('tmdblist_%',))
		for i in tmdb_results: clear_property(i)
		return True
	except: return False

