# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/menus/movies.py

import sys
from threading import Thread
from indexers.metadata import movie_meta, rpdb_get, tmdb_image_base
from caches.watched_cache import get_watched_info_movie, get_watched_status_movie, get_bookmarks, get_resumetime, set_resumetime
from caches.favourites_cache import favourites_context_label, favourite_type_from_context, collection_favourite_type_from_saga_type
from modules import kodi_utils, settings, user_ratings, parental_control
#from modules.utils import manual_function_import, get_datetime, make_thread_list_enumerate, chunks
from modules.utils import manual_function_import, get_datetime, TaskPool, chunks
# logger = kodi_utils.logger

# Alias locaux pour éviter les accès répétés aux modules pendant la construction des listes.
movie_meta_function, get_datetime_function, default_duration = movie_meta, get_datetime, 5400
get_watched_function, get_watched_info_function = get_watched_status_movie, get_watched_info_movie
KODI_VERSION, make_cast_list = kodi_utils.get_kodi_version(), kodi_utils.make_cast_list
string, ls, build_url, get_infolabel = str, kodi_utils.local_string, kodi_utils.build_url, kodi_utils.get_infolabel
remove_meta_keys, dict_removals = kodi_utils.remove_meta_keys, kodi_utils.movie_dict_removals
run_plugin, container_refresh, container_update = 'RunPlugin(%s)', 'Container.Refresh(%s)', 'Container.Update(%s)'
fanart_empty = ''
poster_empty = kodi_utils.media_path('box_office.png')
item_jump = kodi_utils.media_path('item_jump.png')
item_next = kodi_utils.media_path('item_next.png')
watched_str, unwatched_str, traktmanager_str, tmdbmanager_str, mdblmanager_str = ls(32642), ls(32643), ls(32198), 'Gestion des listes TMDb', ls(32200)
favmanager_str, extras_str, options_str, recomm_str = 'Favoris (alkoFlix)', 'Info & Extra', 'Options (alkoFlix)', '%s...' % ls(32503)
trailer_str = 'Bande annonce'
hide_str, clearprog_str, play_str = ls(32648), ls(32651), '%s...' % ls(32174)
nextpage_str, switchjump_str, jumpto_str = ls(32799), ls(32784), ls(32964)

ANIMATION_GENRE_NAMES = ('animation', 'dessin animé', 'dessins animés')

def _meta_has_animation_genre(genre):
	try:
		if isinstance(genre, (list, tuple)): values = genre
		else: values = str(genre or '').split(',')
		return any(str(item or '').strip().lower() in ANIMATION_GENRE_NAMES for item in values)
	except: return False

def _exclude_animation_for_action(action, params):
	if str(params.get('exclude_animation', '')).lower() == 'true': return True
	action = str(action or '')
	if not action: return False
	if action.startswith(('tmdb_moviesanime_', 'trakt_moviesanime_', 'tmdb_movies_family_', 'tmdb_movies_documentary_', 'tmdb_movies_tv_movie_')): return False
	if action in ('tmdb_movies_search', 'tmdb_movies_search_collections', 'tmdb_movies_collection', 'tmdb_movies_company', 'tmdb_movies_public_list'): return False
	if action.startswith(('tmdb_movies_', 'trakt_movies_', 'imdb_movies_')): return True
	return False


def _ignore_menu_hide_watched_for_action(action):
	# Les recherches et Discover doivent rester indépendants du réglage global
	# « Masquer les éléments vus dans les menus ». Sinon une recherche peut
	# retourner peu ou pas de résultats simplement parce que l'utilisateur a déjà
	# vu certains titres.
	action = str(action or '')
	return action in (
		'watched_movies',
		'tmdb_movies_search',
		'tmdb_movies_search_collections',
		'tmdb_movies_keyword',
		'tmdb_movies_discover'
	)


def _ignore_unreleased_filter_for_action(action):
	# Les recherches manuelles et le menu « À venir » doivent rester accessibles
	# même si l'utilisateur masque les films non sortis dans les menus généraux.
	action = str(action or '')
	return action in (
		'tmdb_movies_search',
		'tmdb_movies_search_collections',
		'tmdb_movies_keyword',
		'tmdb_movies_upcoming'
	)


def _movie_date_is_future(release_date, current_date_text):
	try:
		release_date = str(release_date or '')[:10]
		if len(release_date) != 10: return False
		return release_date > current_date_text
	except: return False


def _favourite_type_for_action(action, params, meta=None):
	# Contexte du menu d'abord, puis fallback genres TMDb pour les widgets de skin.
	return favourite_type_from_context('movie', params, action, meta)


# Appelle les endpoints TMDb avec un tri contextuel optionnel, tout en gardant
# les signatures existantes intactes pour les dossiers qui ne supportent pas encore
# ce mode. Le fallback évite de casser un menu entier si une fonction n'a pas été
# prévue pour le tri régional strict.
def _tmdb_context_filter_page(data, context_sort=''):
	if not context_sort: return data
	try:
		from indexers.tmdb_api import _tmdb_filter_context_results, _tmdb_context_sort_results
		data = _tmdb_filter_context_results(data or {}, 'movie', context_sort)
		if isinstance(data, dict):
			data = dict(data)
			data['results'] = _tmdb_context_sort_results(data.get('results', []) or [], context_sort, 'movie')
		return data
	except: return data


def _call_tmdb_page_function(function, page_no, context_sort=''):
	if context_sort:
		try: return function(page_no, context_sort)
		except TypeError: pass
	return _tmdb_context_filter_page(function(page_no), context_sort)


def _call_tmdb_special_function(function, function_var, page_no, context_sort='', provider_region=None, is_provider=False):
	# Les fonctions de plateformes/diffuseurs qui acceptent une région ont la
	# signature (id, page, region, tri). Même si region est vide, il faut passer
	# le 4e argument, sinon le tri se retrouve accidentellement dans region.
	if context_sort:
		if is_provider:
			try: return function(function_var, page_no, provider_region, context_sort) or {}
			except TypeError: pass
		try: return function(function_var, page_no, context_sort) or {}
		except TypeError: pass
	if is_provider and provider_region not in ('', None):
		try: return _tmdb_context_filter_page(function(function_var, page_no, provider_region) or {}, context_sort)
		except TypeError: pass
	return _tmdb_context_filter_page(function(function_var, page_no) or {}, context_sort)


TMDB_SOURCE_PAGE_SIZE = 20
TMDB_FULL_MAX_PAGES = 3

def _safe_int(value, default=1):
	try: return int(value)
	except: return default

def _tmdb_result_rows(data):
	try:
		return [i for i in (data.get('results', []) or []) if isinstance(i, dict) and i.get('id')]
	except: return []


def _tmdb_result_ids(data):
	try:
		return [i.get('id') for i in _tmdb_result_rows(data)]
	except: return []

def _tmdb_page_number(data, fallback=1):
	try: return max(1, int((data or {}).get('page') or fallback or 1))
	except: return max(1, _safe_int(fallback, 1))

def _tmdb_total_pages(data, fallback=1):
	try: return max(1, int((data or {}).get('total_pages') or fallback or 1))
	except: return max(1, _safe_int(fallback, 1))

def _tmdb_full_page_limit():
	# Les menus publics TMDb/Discover ne suivent pas le réglage des listes
	# personnelles. TMDb travaille par pages de 20 éléments : on garde cette
	# taille fixe pour éviter les comportements variables entre menus.
	return TMDB_SOURCE_PAGE_SIZE

class Movies:
	def __init__(self, params):
		self.params = params
		self.items, self.new_page, self.total_pages = [], {}, None
		self.id_type, self.list, self.action, self.exit_list_params = (
			self.params.get('id_type', 'tmdb_id'),
			self.params.get('list', []),
			self.params.get('action'),
			self.params.get('exit_list_params')
		)
		self.append = self.items.append
		self.current_date = get_datetime_function()
		self.current_date_text = get_datetime_function(string=True)
		# Paramètres utilisés pour récupérer les métadonnées et les images selon la configuration utilisateur.
		self.meta_user_info = settings.metadata_user_info()
		self.watched_indicators = settings.watched_indicators()
		self.watched_info = get_watched_info_function(self.watched_indicators)
		self.bookmarks = get_bookmarks(self.watched_indicators, 'movie')
		self.include_year_in_title = settings.include_year_in_title('movie')
		self.media_click_opens_info = settings.media_click_opens_info()
		# Info & Extra utilise la fenêtre d'information native du skin.
		self.open_extras = False
		# Menu contextuel natif : ordre fixe, entrées externes selon services autorisés.
		self.cm_sort = settings.context_menu_sort()
		self.cm_services = settings.context_menu_services()
		self.rpdb_enabled = self.meta_user_info['extra_rpdb_movies']
		self.fanart_enabled = self.meta_user_info['extra_fanart_enabled']
		# Conserve l'identifiant brut du catalogue Stremio pendant la lecture.
		# Utilisé notamment par les catalogues personnalisés qui fournissent un ID interne en plus de TMDb/IMDb.
		self.catalogue_id = self.params.get('catalogue_id') or self.params.get('catalog_id') or ''
		self.is_widget = kodi_utils.external_browse()
		self.widget_hide_watched = self.is_widget and self.meta_user_info['widget_hide_watched']
		self.discover_hide_watched = self.params.get('discover_hide_watched', 'false') == 'true'
		self.menu_hide_watched = (not self.is_widget) and self.meta_user_info.get('menu_hide_watched', False) and not _ignore_menu_hide_watched_for_action(self.action)
		self.hide_unreleased_movies = settings.hide_unreleased_movies() and not _ignore_unreleased_filter_for_action(self.action)
		if not self.exit_list_params: self.exit_list_params = get_infolabel('Container.FolderPath')
		self.watched_title = ('alkoFlix', 'Trakt', 'MDBList')[self.watched_indicators]
		self.poster_main, self.poster_backup, self.fanart_main, self.fanart_backup = settings.get_art_provider()
		self.user_ratings = user_ratings.get_active_ratings_index()
		self.parental_blocked = False
		self.exclude_animation_content = _exclude_animation_for_action(self.action, self.params)
		self.tmdb_sort_offset = 0
		self._tmdb_full_fetcher = None
		self._tmdb_full_start_page = 1
		self._tmdb_full_start_skip = 0
		self._tmdb_full_total_pages = 1
		self._tmdb_full_next_source_page = 2
		self._tmdb_full_raw_count = 0
		self._tmdb_full_raw_rows = []


	def _fast_tmdb_search(self):
		# Les résultats de recherche doivent être affichés rapidement à partir de
		# la réponse TMDb brute. Les détails complets seront récupérés seulement
		# quand l'utilisateur ouvrira l'item.
		return self.action in ('tmdb_movies_search', 'tmdb_movies_keyword')

	def _set_tmdb_source(self, data, page_no, fetcher=None):
		# Prépare une page TMDb paginée. Si l'option « pages complètes » est active,
		# on conserve assez d'information pour compléter la page Kodi sans doublons.
		current_page = _tmdb_page_number(data, page_no)
		total_pages = _tmdb_total_pages(data, current_page)
		start_skip = max(0, _safe_int(self.params.get('tmdb_full_skip', '0'), 0))
		rows = _tmdb_result_rows(data)
		if start_skip:
			rows = rows[start_skip:] if start_skip < len(rows) else []
		if self.hide_unreleased_movies:
			rows = [i for i in rows if not _movie_date_is_future(i.get('release_date') or i.get('primary_release_date'), self.current_date_text)]
		ids = [i.get('id') for i in rows if i.get('id')]
		self.tmdb_sort_offset = start_skip
		self.list = ids
		self._tmdb_full_raw_rows = rows
		self._tmdb_full_fetcher = fetcher if settings.tmdb_try_full_pages() and not self._fast_tmdb_search() else None
		self._tmdb_full_start_page = current_page
		self._tmdb_full_start_skip = start_skip
		self._tmdb_full_total_pages = total_pages
		self._tmdb_full_next_source_page = current_page + 1
		self._tmdb_full_raw_count = len(ids)
		return current_page, total_pages

	def _update_tmdb_full_next_page(self, displayed_items, display_limit):
		# Calcule la prochaine page TMDb réelle à ouvrir. Si on a seulement consommé
		# une partie d'une page source, on reprend cette même page avec un décalage
		# interne pour éviter de sauter des éléments valides.
		# Important : certains menus n'ont aucun paramètre à transporter à part
		# new_page. Il faut donc mémoriser l'existence du lien avant de retirer
		# new_page, sinon les menus TMDb simples perdent leur pagination.
		base_next = dict(self.new_page or {})
		had_next_page = 'new_page' in base_next
		base_next.pop('new_page', None)
		base_next.pop('tmdb_full_skip', None)
		total_pages = max(1, int(self._tmdb_full_total_pages or 1))
		self.new_page = {}
		if not had_next_page:
			return
		if len(displayed_items) < display_limit and self._tmdb_full_next_source_page > total_pages:
			return
		if displayed_items and len(displayed_items) >= display_limit:
			try: consumed_offset = max(0, int(displayed_items[-1][1].getProperty('alkoflix_sort_order')) + 1)
			except: consumed_offset = self._tmdb_full_start_skip + self._tmdb_full_raw_count
		else:
			consumed_offset = self._tmdb_full_start_skip + self._tmdb_full_raw_count
		next_page = self._tmdb_full_start_page + (consumed_offset // TMDB_SOURCE_PAGE_SIZE)
		next_skip = consumed_offset % TMDB_SOURCE_PAGE_SIZE
		if next_page > total_pages:
			return
		base_next['new_page'] = string(next_page)
		if next_skip: base_next['tmdb_full_skip'] = string(next_skip)
		self.new_page = base_next

	def _dedupe_tmdb_items(self, items):
		seen, deduped = set(), []
		for item in items or []:
			try: key = item[0]
			except: key = None
			if key in seen: continue
			if key is not None: seen.add(key)
			deduped.append(item)
		return deduped

	def _complete_tmdb_items(self, items):
		if not self._tmdb_full_fetcher:
			return items
		display_limit = _tmdb_full_page_limit()
		try: items.sort(key=lambda k: int(k[1].getProperty('alkoflix_sort_order')))
		except: pass
		attempts = 1
		while len(items) < display_limit and attempts < TMDB_FULL_MAX_PAGES:
			next_source_page = self._tmdb_full_next_source_page
			if next_source_page > self._tmdb_full_total_pages: break
			data = self._tmdb_full_fetcher(next_source_page) or {}
			if not isinstance(data, dict): break
			self._tmdb_full_total_pages = max(self._tmdb_full_total_pages, _tmdb_total_pages(data, self._tmdb_full_total_pages))
			rows = _tmdb_result_rows(data)
			if not rows: break
			ids = [i.get('id') for i in rows if i.get('id')]
			if not ids: break
			self.tmdb_sort_offset = self._tmdb_full_start_skip + self._tmdb_full_raw_count
			self._tmdb_full_raw_count += len(rows)
			self._tmdb_full_next_source_page = next_source_page + 1
			self._tmdb_full_raw_rows = rows
			self.list = ids
			items = self.builder()
			try: items.sort(key=lambda k: int(k[1].getProperty('alkoflix_sort_order')))
			except: pass
			attempts += 1
		items = self._dedupe_tmdb_items(items)
		displayed = items[:display_limit]
		self._update_tmdb_full_next_page(displayed, display_limit)
		self.items = displayed
		self.list = []
		self.tmdb_sort_offset = 0
		return displayed

	def _carry_context_to_next_page(self, params_get):
		# Les dossiers thématiques (Famille, Docus, Animation, Anime, Sagas)
		# doivent garder leur contexte quand Kodi ajoute « Page suivante ».
		# Sans ces clés, les favoris ajoutés depuis une page suivante peuvent
		# retomber dans les favoris globaux movie/tvshow.
		for key in ('fav_type', 'family_type', 'documentary_type', 'native_type', 'saga_type', 'catalogue_id', 'catalog_id', 'exclude_animation', 'provider_region', 'hide_watched_items', 'discover_hide_watched', 'watched_indicators', 'genre_sort', 'list_sort', 'list_random_seed'):
			value = params_get(key)
			if value not in ('', None) and key not in self.new_page:
				self.new_page[key] = value

	def build_movie_search_content(self, _position, item):
		try:
			# Variante légère pour les recherches : aucune récupération meta complète,
			# aucun appel Fanart.tv/RPDb. On utilise directement la réponse TMDb.
			image_resolution = settings.get_resolution()
			tmdb_id = item.get('id')
			if not tmdb_id: return
			title = item.get('title') or item.get('name') or item.get('original_title') or ''
			if not title: return
			year = str(item.get('release_date') or '')[:4]
			label = '%s (%s)' % (title, year) if self.include_year_in_title and year else title
			poster_path, backdrop_path = item.get('poster_path'), item.get('backdrop_path')
			poster = tmdb_image_base % (image_resolution['poster'], poster_path) if poster_path else poster_empty
			fanart = tmdb_image_base % (image_resolution['fanart'], backdrop_path) if backdrop_path else fanart_empty
			thumb = fanart or poster
			playcount, overlay = get_watched_function(self.watched_info, string(tmdb_id))
			# Les recherches ne doivent jamais être vidées par le filtre global des menus.
			sort = _position + getattr(self, 'tmdb_sort_offset', 0)
			props = {'alkoflix_sort_order': string(sort)}
			play_params = build_url({'mode': 'play_media', 'media_type': 'movie', 'tmdb_id': tmdb_id})
			options_params = build_url({'mode': 'options_menu_choice', 'content': 'movie', 'tmdb_id': tmdb_id, 'is_widget': self.is_widget})
			info_extra_params = build_url({'mode': 'info_extra_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'is_widget': string(self.is_widget).lower()})
			trailer_params = build_url({'mode': 'play_trailer_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id})
			trakt_manager_params = build_url({'mode': 'trakt_manager_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': 'None', 'tvdb_id': 'None'})
			tmdb_manager_params = build_url({'mode': 'tmdb_manager_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': 'None', 'tvdb_id': 'None'})
			mdbl_manager_params = build_url({'mode': 'mdbl_manager_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': 'None', 'tvdb_id': 'None'})
			rating_params = build_url({'mode': 'rating_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': 'None', 'tvdb_id': 'None', 'title': title})
			fav_type = _favourite_type_for_action(self.action, self.params, None)
			fav_manager_label = favourites_context_label(fav_type, tmdb_id)
			fav_manager_params = build_url({'mode': 'favourites_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': 'None', 'tvdb_id': 'None', 'title': title, 'fav_type': fav_type, 'action': self.action})
			cm = []
			cm_append = cm.append
			cm_append((self.cm_sort['options'], options_str, run_plugin % options_params))
			cm_append((self.cm_sort['options'], trailer_str, run_plugin % trailer_params))
			cm_append((self.cm_sort['extras'], extras_str, run_plugin % info_extra_params))
			if self.cm_services['trakt']: cm_append((self.cm_sort['trakt'], traktmanager_str, run_plugin % trakt_manager_params))
			if self.cm_services['mdblist']: cm_append((self.cm_sort['mdblist'], mdblmanager_str, run_plugin % mdbl_manager_params))
			if self.cm_services['tmdblist']: cm_append((self.cm_sort['tmdblist'], tmdbmanager_str, run_plugin % tmdb_manager_params))
			if settings.context_rating_enabled('movie'): cm_append((self.cm_sort['rating'], ls(33135), run_plugin % rating_params))
			if settings.context_favourites_enabled(): cm_append((self.cm_sort['favourites'], fav_manager_label, run_plugin % fav_manager_params))
			if playcount: cm_append((self.cm_sort['mark'], unwatched_str % self.watched_title, run_plugin % build_url({'mode': 'mark_as_watched_unwatched_movie', 'action': 'mark_as_unwatched', 'tmdb_id': tmdb_id, 'title': title, 'year': year})))
			else: cm_append((self.cm_sort['mark'], watched_str % self.watched_title, run_plugin % build_url({'mode': 'mark_as_watched_unwatched_movie', 'action': 'mark_as_watched', 'tmdb_id': tmdb_id, 'title': title, 'year': year})))
			cm.sort(key=lambda k: k[0])
			cm = [v for k, *v in cm if k]
			listitem = kodi_utils.make_listitem()
			listitem.addContextMenuItems(cm)
			listitem.setProperties(props)
			listitem.setLabel(label)
			listitem.setArt(kodi_utils.clean_art({'poster': poster, 'fanart': fanart, 'icon': poster, 'thumb': thumb, 'landscape': thumb}))
			info = {'mediatype': 'movie', 'title': label, 'plot': item.get('overview', ''), 'year': year, 'premiered': item.get('release_date', ''), 'rating': item.get('vote_average', 0), 'votes': item.get('vote_count', 0), 'playcount': playcount}
			kodi_utils.set_listitem_video_info(listitem, info)
			try: listitem.getVideoInfoTag(offscreen=True).setUniqueIDs({'tmdb': string(tmdb_id)})
			except: pass
			url_params = info_extra_params if self.media_click_opens_info else play_params
			self.append((url_params, listitem, False))
		except Exception as e:
			kodi_utils.logger('quick movie search item error', str(e))

	def fast_search_worker(self):
		rows = self._tmdb_full_raw_rows or [{'id': i} for i in self.list]
		if not rows: return []
		for position, item in enumerate(rows):
			self.build_movie_search_content(position, item)
		try: self.items.sort(key=lambda k: int(k[1].getProperty('alkoflix_sort_order')))
		except: pass
		return self.items

	def build_movie_content(self, _position, _id):
		try:
			# Récupère les métadonnées du film avant de construire l'item Kodi.
			meta = movie_meta_function(self.id_type, _id, self.meta_user_info, self.current_date)
			meta_get = meta.get
			if not meta or meta_get('blank_entry', False): return
			if self.hide_unreleased_movies and _movie_date_is_future(meta_get('premiered'), self.current_date_text): return
			if self.exclude_animation_content and _meta_has_animation_genre(meta_get('genre')): return
			if not parental_control.meta_allowed(meta, self.action, 'movie'):
				self.parental_blocked = True
				return
			playcount, overlay = get_watched_function(self.watched_info, string(meta['tmdb_id']))
			# Dans les widgets ou les menus, on peut masquer les films déjà vus selon le réglage utilisateur.
			if (self.widget_hide_watched or self.menu_hide_watched or self.discover_hide_watched) and playcount: return
			meta.update({'playcount': playcount, 'overlay': overlay})
			resumetime, progress = get_resumetime(self.bookmarks, string(meta['tmdb_id']))
			sort = _id.get('sort', _position) if self.id_type == 'trakt_dict' else _position + getattr(self, 'tmdb_sort_offset', 0)
			props = {'alkoflix_sort_order': string(sort)}
			cm = []
			cm_append = cm.append
			clearprog_params, unwatched_params, watched_params = '', '', ''
			rootname, title, year = meta_get('rootname'), meta_get('title'), meta_get('year')
			tmdb_id, imdb_id = meta_get('tmdb_id'), meta_get('imdb_id')
			user_rating = user_ratings.get_user_rating(self.user_ratings, 'movie', tmdb_id=tmdb_id, imdb_id=imdb_id)
			if user_rating: meta['userrating'] = user_rating
			poster = settings.resolve_poster_art(meta, poster_empty)
			media_fanart = meta_get(self.fanart_main) or meta_get(self.fanart_backup)
			fanart = media_fanart or fanart_empty
			clearlogo = meta_get('clearlogo') or meta_get('tmdblogo') or ''
			if self.rpdb_enabled:
				rpdb_data = rpdb_get('movie', imdb_id or str(tmdb_id), self.meta_user_info['rpdb_api_key'])
				poster = rpdb_data.get('rpdb') or poster
			# Les images supplémentaires Fanart.tv restent optionnelles, mais l'image
			# landscape doit toujours respecter le choix de langue TMDb quand disponible.
			if self.fanart_enabled:
				banner, clearart, discart = meta_get('banner'), meta_get('clearart'), meta_get('discart')
				landscape = settings.resolve_landscape_art(meta, media_fanart or poster)
			else:
				banner, clearart, discart = '', '', ''
				landscape = meta_get('landscape_tmdb') or media_fanart or poster
			play_data = {'mode': 'play_media', 'media_type': 'movie', 'tmdb_id': tmdb_id}
			if self.catalogue_id: play_data['catalogue_id'] = self.catalogue_id
			play_params = build_url(play_data)
			options_params = build_url({'mode': 'options_menu_choice', 'content': 'movie', 'tmdb_id': tmdb_id, 'is_widget': self.is_widget})
			recommended_params = build_url({'mode': 'build_movie_list', 'action': 'tmdb_movies_recommendations', 'tmdb_id': tmdb_id})
			trakt_manager_params = build_url({'mode': 'trakt_manager_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': imdb_id, 'tvdb_id': 'None'})
			tmdb_manager_params = build_url({'mode': 'tmdb_manager_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': imdb_id, 'tvdb_id': 'None'})
			mdbl_manager_params = build_url({'mode': 'mdbl_manager_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': imdb_id, 'tvdb_id': 'None'})
			rating_params = build_url({'mode': 'rating_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': imdb_id, 'tvdb_id': 'None', 'title': title})
			fav_type = _favourite_type_for_action(self.action, self.params, meta)
			fav_manager_label = favourites_context_label(fav_type, tmdb_id)
			fav_payload = {'mode': 'favourites_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'imdb_id': imdb_id, 'tvdb_id': 'None', 'title': title, 'fav_type': fav_type, 'action': self.action}
			for ctx_key in ('family_type', 'documentary_type', 'native_type', 'fav_group', 'group', 'saga_type'):
				ctx_value = self.params.get(ctx_key)
				if ctx_value not in ('', None): fav_payload[ctx_key] = ctx_value
			fav_manager_params = build_url(fav_payload)
			trailer_params = build_url({'mode': 'play_trailer_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id})
			info_extra_params = build_url({'mode': 'info_extra_choice', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'is_widget': string(self.is_widget).lower()})
			# Menu contextuel natif : Info & Extra ouvre la fiche info du skin.
			cm_append((self.cm_sort['options'], options_str, run_plugin % options_params))
			cm_append((self.cm_sort['options'], trailer_str, run_plugin % trailer_params))
			cm_append((self.cm_sort['extras'], extras_str, run_plugin % info_extra_params))
			url_params = info_extra_params if self.media_click_opens_info else play_params
			if self.cm_services['trakt']: cm_append((self.cm_sort['trakt'], traktmanager_str, run_plugin % trakt_manager_params))
			if self.cm_services['mdblist']: cm_append((self.cm_sort['mdblist'], mdblmanager_str, run_plugin % mdbl_manager_params))
			if self.cm_services['tmdblist']: cm_append((self.cm_sort['tmdblist'], tmdbmanager_str, run_plugin % tmdb_manager_params))
			if settings.context_rating_enabled('movie'): cm_append((self.cm_sort['rating'], ls(33135), run_plugin % rating_params))
			if settings.context_favourites_enabled(): cm_append((self.cm_sort['favourites'], fav_manager_label, run_plugin % fav_manager_params))
			if progress != '0' or resumetime != '0': cm_append((self.cm_sort['mark'], clearprog_str, run_plugin % build_url({
				'mode': 'watched_unwatched_erase_bookmark', 'media_type': 'movie', 'tmdb_id': tmdb_id, 'refresh': 'true'
			})))
			if playcount: cm_append((self.cm_sort['mark'], unwatched_str % self.watched_title, run_plugin % build_url({
				'mode': 'mark_as_watched_unwatched_movie', 'action': 'mark_as_unwatched', 'tmdb_id': tmdb_id, 'title': title, 'year': year
			})))
			else: cm_append((self.cm_sort['mark'], watched_str % self.watched_title, run_plugin % build_url({
				'mode': 'mark_as_watched_unwatched_movie', 'action': 'mark_as_watched', 'tmdb_id': tmdb_id, 'title': title, 'year': year
			})))
			# Trie les entrées du menu contextuel selon l'ordre automatique.
			cm.sort(key=lambda k: k[0])
			cm = [v for k, *v in cm if k]
			listitem = kodi_utils.make_listitem()
			listitem.addContextMenuItems(cm)
			listitem.setProperties(props)
			listitem.setLabel(rootname if self.include_year_in_title else title)
#			listitem.setContentLookup(False)
			# Certaines vues de skin, surtout les vues horizontales, privilégient thumb
			# plutôt que poster/landscape. Sans thumb explicite, Kodi peut afficher
			# une tuile vide même si une autre image est disponible.
			thumb = landscape or fanart or poster
			listitem.setArt(kodi_utils.clean_art({'poster': poster, 'fanart': fanart, 'icon': poster, 'thumb': thumb, 'banner': banner, 'clearart': clearart,
							'clearlogo': clearlogo, 'landscape': landscape or thumb, 'discart': discart}))
			if KODI_VERSION < 20:
				listitem.setCast(meta_get('cast', []))
				listitem.setUniqueIDs({'imdb': imdb_id, 'tmdb': string(tmdb_id)})
				listitem.setInfo('video', remove_meta_keys(meta, dict_removals))
				listitem.setProperty('resumetime', resumetime)
			else:
				videoinfo = listitem.getVideoInfoTag(offscreen=True)
				videoinfo.setCast(make_cast_list(meta_get('cast', [])))
				videoinfo.setUniqueIDs({'imdb': imdb_id, 'tmdb': string(tmdb_id)})
				videoinfo.setCountries(meta_get('country'))
				videoinfo.setDirectors(meta_get('director').split(', '))
				videoinfo.setDuration(int(meta_get('duration') or default_duration))
				videoinfo.setGenres(meta_get('genre').split(', '))
				videoinfo.setIMDBNumber(imdb_id)
				videoinfo.setMediaType('movie')
				videoinfo.setMpaa(meta_get('mpaa'))
				videoinfo.setPlaycount(playcount)
				videoinfo.setPlot(meta_get('plot'))
				videoinfo.setPremiered(meta_get('premiered'))
				videoinfo.setRating(meta_get('rating'))
				user_ratings.apply_user_rating_to_listitem(listitem, user_rating, videoinfo)
				videoinfo.setStudios((meta_get('studio'),))
				videoinfo.setTagLine(meta_get('tagline'))
				videoinfo.setTitle(rootname if self.include_year_in_title else title)
				videoinfo.setTrailer(meta_get('trailer'))
				videoinfo.setVotes(meta_get('votes'))
				videoinfo.setWriters(meta_get('writer').split(', '))
				videoinfo.setYear(int(year))
				if int(progress):
					listitem.setProperty('watchedprogress', progress)
					videoinfo.setResumePoint(*set_resumetime(resumetime, progress, videoinfo.getDuration()))
			self.append((url_params, listitem, False))
		except: pass

	def worker(self):
		if not self.list: return []
		if self._fast_tmdb_search(): return self.fast_search_worker()
#		threads = list(make_thread_list_enumerate(self.build_movie_content, self.list, Thread))
		# Construction multi-threadée pour accélérer les longues listes de films.
		for i in TaskPool().tasks_enumerate(self.build_movie_content, self.list, Thread): i.join()
		self.items.sort(key=lambda k: int(k[1].getProperty('alkoflix_sort_order')))
		return self.items

class Menu(Movies):
	tmdb_main, trakt_main, tmdb_special_key_dict = (
		'tmdb_movies_popular', 'tmdb_movies_trending', 'tmdb_movies_random', 'tmdb_movies_top_rated', 'tmdb_movies_latest_releases', 'tmdb_movies_premieres', 'tmdb_movies_upcoming',
		'tmdb_movies_blockbusters', 'tmdb_moviesanime_popular', 'tmdb_moviesanime_random', 'tmdb_moviesanime_latest_releases',
		'tmdb_movies_family_popular', 'tmdb_movies_family_random', 'tmdb_movies_family_latest_releases',
		'tmdb_movies_family_animation_popular', 'tmdb_movies_family_animation_random', 'tmdb_movies_family_animation_latest_releases',
		'tmdb_movies_documentary_popular', 'tmdb_movies_documentary_random', 'tmdb_movies_documentary_trending', 'tmdb_movies_documentary_latest_releases',
		'tmdb_movies_tv_movie_popular', 'tmdb_movies_tv_movie_random', 'tmdb_movies_tv_movie_trending', 'tmdb_movies_tv_movie_latest_releases'
	), (
		'trakt_movies_trending', 'trakt_movies_trending_recent', 'trakt_movies_most_watched',
		'trakt_moviesanime_trending', 'trakt_moviesanime_most_watched'
	), {
		'tmdb_movies_networks': 'company', 'tmdb_movies_company': 'company_id', 'tmdb_movies_public_list': 'list_id', 'tmdb_movies_year': 'year', 'tmdb_moviesanime_year': 'year',
		'tmdb_movies_language': 'language_code', 'tmdb_movies_watch_provider': 'provider_id',
		'tmdb_moviesanime_language': 'language_code', 'tmdb_moviesanime_watch_provider': 'provider_id',
		'tmdb_movies_family_year': 'year', 'tmdb_movies_family_watch_provider': 'provider_id', 'tmdb_movies_family_kids_watch_provider': 'provider_id', 'tmdb_movies_family_genres': 'genre_id',
		'tmdb_movies_family_animation_year': 'year', 'tmdb_movies_family_animation_watch_provider': 'provider_id', 'tmdb_movies_family_animation_genres': 'genre_id',
		'tmdb_movies_documentary_year': 'year', 'tmdb_movies_documentary_language': 'language_code', 'tmdb_movies_documentary_watch_provider': 'provider_id', 'tmdb_movies_documentary_genres': 'genre_id',
		'tmdb_movies_tv_movie_year': 'year', 'tmdb_movies_tv_movie_language': 'language_code', 'tmdb_movies_tv_movie_watch_provider': 'provider_id', 'tmdb_movies_tv_movie_genres': 'genre_id',
		'tmdb_movies_keyword': 'keyword_id'
	}
	tmdb_personal = ('tmdb_watchlist', 'tmdb_favorite', 'tmdb_recommendations')
	trakt_personal = ('trakt_collection', 'trakt_watchlist', 'trakt_favorites', 'trakt_collection_lists')
	mdblist_personal = ('mdblist_collection', 'mdblist_watchlist', 'mdblist_favorites')
	imdb_personal = ('imdb_watchlist', 'imdb_user_list_contents')
	similar = ('tmdb_movies_similar', 'tmdb_movies_recommendations')
	personal_dict = {
		'in_progress_movies': ('caches.watched_cache', 'get_in_progress_movies'),
		'favourites_movies': ('caches.favourites_cache', 'get_favourites'),
		'favourites_movies_all': ('caches.favourites_cache', 'get_favourites_movies_all'),
		'favourites_movie_family': ('caches.favourites_cache', 'get_favourites_movie_family'),
		'favourites_movie_animation': ('caches.favourites_cache', 'get_favourites_movie_animation'),
		'favourites_anime_movie': ('caches.favourites_cache', 'get_favourites_anime_movie'),
		'favourites_movie_documentary': ('caches.favourites_cache', 'get_favourites_movie_documentary'),
		'favourites_movie_tv_movie': ('caches.favourites_cache', 'get_favourites_movie_tv_movie'),
		'favourites_custom_movies': ('caches.favourites_cache', 'get_favourites_custom'),
		'watched_movies': ('caches.watched_cache', 'get_watched_items')
	}

	def run(self):
		try:
			params_get = self.params.get
			self.handle, self.builder = int(sys.argv[1]), self.worker
			view_type, content_type = 'view.movies', 'movies'
			mode = params_get('mode')
			try: page_no = int(params_get('new_page', '1'))
			except ValueError: page_no = params_get('new_page')
			letter = params_get('new_letter', 'None')
			if self.action in Menu.personal_dict: var_module, import_function = Menu.personal_dict[self.action]
			else: var_module, import_function = 'indexers.%s_api' % self.action.split('_')[0], self.action
			try: function = manual_function_import(var_module, import_function)
			except: pass
			if self.action in Menu.tmdb_main:
				context_sort = params_get('genre_sort', '')
				data = _call_tmdb_page_function(function, page_no, context_sort) or {}
				current_page, total_pages = self._set_tmdb_source(data, page_no, lambda source_page: _call_tmdb_page_function(function, source_page, context_sort))
				if context_sort:
					try: total_pages = min(int(total_pages), 4)
					except: total_pages = 4
					self._tmdb_full_total_pages = total_pages
				if total_pages > page_no:
					self.new_page = {'new_page': string(current_page + 1)}
					if context_sort: self.new_page['genre_sort'] = context_sort
			elif self.action in Menu.trakt_main:
				self.id_type = 'trakt_dict'
				data = function(page_no)
				self.list = [i['movie']['ids'] for i in data]
				if self.action not in ('trakt_moviesanime_trending',):
					self.new_page = {'new_page': string(page_no + 1)}
			elif self.action in Menu.tmdb_personal:
				data, total_pages = function('movie', page_no, letter)
				self.list = [i['id'] for i in data]
				if total_pages > page_no: self.new_page = {'new_page': string(page_no + 1), 'new_letter': letter}
			elif self.action in Menu.trakt_personal:
				self.id_type = 'trakt_dict'
				list_sort, list_random_seed = params_get('list_sort', ''), params_get('list_random_seed', '')
				if self.action in ('trakt_collection', 'trakt_watchlist', 'trakt_favorites'):
					data, total_pages = function('movies', page_no, letter, list_sort, list_random_seed)
				else:
					data, total_pages = function('movies', page_no, letter)
				self.list = [i['media_ids'] for i in data]
				if total_pages > 2: self.total_pages = total_pages
				try:
					if total_pages > page_no:
						self.new_page = {'new_page': string(page_no + 1), 'new_letter': letter}
						if list_sort: self.new_page['list_sort'] = list_sort
						if list_random_seed: self.new_page['list_random_seed'] = list_random_seed
				except: pass
			elif self.action in Menu.mdblist_personal:
				self.id_type = 'trakt_dict'
				data, total_pages = function('movies', page_no, letter)
				self.list = [{'imdb': i['imdb_id'], 'tmdb': i['id']} for i in data]
				if total_pages > 2: self.total_pages = total_pages
				try:
					if total_pages > page_no: self.new_page = {'new_page': string(page_no + 1), 'new_letter': letter}
				except: pass
			elif self.action in Menu.imdb_personal:
				self.id_type = 'imdb_id'
				list_id = params_get('list_id')
				data, next_page = function('movie', list_id, page_no)
				self.list = [i['imdb_id'] for i in data]
				if next_page: self.new_page = {'list_id': list_id, 'new_page': string(page_no + 1), 'new_letter': letter}
			elif self.action in Menu.personal_dict:
				favourite_query_type = params_get('fav_type') if self.action == 'favourites_custom_movies' else 'movie'
				list_sort, list_random_seed = params_get('list_sort', ''), params_get('list_random_seed', '')
				if self.action in ('in_progress_movies', 'watched_movies'):
					data, total_pages = function(favourite_query_type, page_no, letter, list_sort=list_sort, list_random_seed=list_random_seed)
				else:
					data, total_pages = function(favourite_query_type, page_no, letter)
				self.list = [i['media_id'] for i in data]
				if total_pages > 2: self.total_pages = total_pages
				if total_pages > page_no:
					self.new_page = {'new_page': string(page_no + 1), 'new_letter': letter}
					if list_sort: self.new_page['list_sort'] = list_sort
					if list_random_seed: self.new_page['list_random_seed'] = list_random_seed
			elif self.action in Menu.similar:
				tmdb_id = params_get('tmdb_id')
				data = function(tmdb_id, page_no)
				current_page, total_pages = self._set_tmdb_source(data, page_no, lambda source_page: function(tmdb_id, source_page))
				if current_page < total_pages: self.new_page = {'new_page': string(current_page + 1), 'tmdb_id': tmdb_id}
			elif self.action in Menu.tmdb_special_key_dict:
				key = Menu.tmdb_special_key_dict[self.action]
				function_var = params_get(key)
				if not function_var: return
				provider_region = params_get('provider_region')
				context_sort = params_get('genre_sort', '')
				list_sort = params_get('list_sort', '')
				list_random_seed = params_get('list_random_seed', '')
				if key == 'language_code' and str(function_var).lower() not in ('fr', 'en'):
					context_sort = ''
				if self.action == 'tmdb_movies_public_list':
					data = function(function_var, page_no, list_sort, list_random_seed) or {}
					context_sort = ''
					fetcher = lambda source_page: function(function_var, source_page, list_sort, list_random_seed) or {}
				else:
					data = _call_tmdb_special_function(function, function_var, page_no, context_sort, provider_region if self.action.endswith('_watch_provider') else None, self.action.endswith('_watch_provider'))
					fetcher = lambda source_page: _call_tmdb_special_function(function, function_var, source_page, context_sort, provider_region if self.action.endswith('_watch_provider') else None, self.action.endswith('_watch_provider'))
				results = data.get('results', []) if isinstance(data, dict) else []
				current_page, total_pages = self._set_tmdb_source(data, page_no, fetcher)
				if self.action.endswith('_watch_provider') and not self.list:
					kodi_utils.logger('catalogue provider empty', '%s | provider_id=%s | region=%s | page=%s' % (self.action, function_var, provider_region or 'settings', page_no))
				if isinstance(data, dict):
					current_page = current_page
					total_pages = total_pages
					if context_sort:
						try: total_pages = min(int(total_pages), 4)
						except: total_pages = 4
						self._tmdb_full_total_pages = total_pages
					if current_page < total_pages:
						self.new_page = {'new_page': string(current_page + 1), key: function_var}
						if provider_region: self.new_page['provider_region'] = provider_region
						if context_sort: self.new_page['genre_sort'] = context_sort
						if list_sort: self.new_page['list_sort'] = list_sort
						if list_random_seed: self.new_page['list_random_seed'] = list_random_seed
			elif self.action in ('tmdb_person_movies', 'tmdb_person_director'):
				from menus.people import person_credit_ids, _person_credits_debug
				actor_id = params_get('actor_id')
				credit_type = 'director' if self.action == 'tmdb_person_director' else 'movie'
				credit_ids = person_credit_ids(actor_id, credit_type)
				credit_pages = list(chunks(credit_ids, 40))
				try: self.list = credit_pages[page_no - 1]
				except: self.list = []
				_person_credits_debug('menu page build', actor_id, credit_type, len(self.list), 'page=%s/%s | prepared=%s | prepared_count=%s' % (page_no, len(credit_pages), params_get('person_credit_prepared', 'false'), params_get('person_credit_count', 'N/A')))
				if len(credit_pages) > page_no:
					self.new_page = {'actor_id': actor_id, 'new_page': string(page_no + 1), 'person_credit_prepared': params_get('person_credit_prepared', 'false'), 'person_credit_type': credit_type}
			elif self.action == 'tmdb_movies_discover':
				from menus.discover import set_history
				name, query = params_get('name'), params_get('query')
				if page_no == 1: set_history('movie', name, query)
				data = function(query, page_no)
				current_page, total_pages = self._set_tmdb_source(data, page_no, lambda source_page: function(query, source_page))
				if current_page < total_pages:
					self.new_page = {'query': query, 'name': name, 'new_page': string(current_page + 1)}
					if self.discover_hide_watched: self.new_page['discover_hide_watched'] = 'true'
			elif self.action == 'imdb_movies_oscar_winners':
				from modules.meta_lists import oscar_winners
				self.list = [i for i in chunks(oscar_winners, 20)][page_no-1]
				if self.list[-1] != 631: self.new_page = {'new_page': string(page_no + 1)}
			elif self.action in ('tmdb_movies_genres', 'tmdb_moviesanime_genres'):
				# Certains retours TMDb peuvent être vides ou incomplets.
				# On sécurise la lecture pour éviter un échec complet du répertoire Kodi.
				genre_id = params_get('genre_id')
				genre_sort = params_get('genre_sort', '')
				if not genre_id: return
				data = function(genre_id, page_no, genre_sort) if genre_sort else function(genre_id, page_no)
				data = data or {}
				results = data.get('results', []) if isinstance(data, dict) else []
				current_page, total_pages = self._set_tmdb_source(data, page_no, lambda source_page: function(genre_id, source_page, genre_sort) if genre_sort else function(genre_id, source_page))
				if isinstance(data, dict):
					current_page = current_page
					total_pages = total_pages
					if genre_sort:
						try: total_pages = min(int(total_pages), 4)
						except: total_pages = 4
						self._tmdb_full_total_pages = total_pages
					if current_page < total_pages:
						self.new_page = {'new_page': string(current_page + 1), 'genre_id': genre_id}
						if genre_sort: self.new_page['genre_sort'] = genre_sort
			elif self.action == 'tmdb_movies_search':
				query = params_get('query')
				data = function(query, page_no)
				current_page, total_pages = self._set_tmdb_source(data, page_no, lambda source_page: function(query, source_page))
				if total_pages > current_page: self.new_page = {'new_page': string(current_page + 1), 'new_letter': letter, 'query': query}
			elif self.action == 'tmdb_movies_search_collections':
				self.builder, view_type, content_type = self.build_collections_results, 'view.movies', 'sets'
				query = params_get('query')
				data = function(query, page_no)
				self.list = data['results']
				total_pages = data['total_pages']
				if total_pages > page_no: self.new_page = {'new_page': string(page_no + 1), 'query': query}
			elif self.action == 'tmdb_movies_curated_collections':
				self.builder, view_type, content_type = self.build_collections_results, 'view.movies', 'sets'
				saga_type = params_get('saga_type', 'movie')
				fav_type = params_get('fav_type') or collection_favourite_type_from_saga_type(saga_type)
				data = function(saga_type, page_no)
				self.list = data.get('results', [])
				total_pages = data.get('total_pages', 1)
				if total_pages > page_no: self.new_page = {'new_page': string(page_no + 1), 'saga_type': saga_type, 'fav_type': fav_type}
			elif self.action == 'tmdb_movies_collection':
				data = sorted(function(params_get('collection_id'))['parts'], key=lambda k: k['release_date'] or '2050')
				self.list = [i['id'] for i in data]
			elif self.action == 'trakt_recommendations':
				self.id_type = 'trakt_dict'
				data = function('movies')
				self.list = [i['ids'] for i in data]
			# Ajoute l'entrée de navigation rapide seulement dans les listes complètes, pas dans les widgets.
			if self.total_pages and not self.is_widget and settings.nav_jump_use_alphabet():
				url_params = {
					'mode': 'build_navigate_to_page', 'current_page': page_no, 'total_pages': self.total_pages,
					'query': params_get('search_name', ''), 'actor_id': params_get('actor_id', ''),
					'transfer_mode': mode, 'transfer_action': self.action, 'media_type': 'Films'
				}
				for key in ('list_sort', 'list_random_seed', 'genre_sort', 'provider_region', 'genre_id', 'provider_id', 'company_id', 'company', 'network_id', 'year', 'language_code', 'keyword_id'):
					if params_get(key): url_params[key] = params_get(key)
				kodi_utils.add_dir(self.handle, url_params, jumpto_str, item_jump, isFolder=False)
			items = self._complete_tmdb_items(self.builder())
			if items: kodi_utils.add_items(self.handle, items)
			if self.new_page:
				self._carry_context_to_next_page(params_get)
				self.new_page.update({'mode': mode, 'action': self.action, 'exit_list_params': self.exit_list_params, 'name': ls(params_get('name'))})
				kodi_utils.add_dir(self.handle, self.new_page, nextpage_str, item_next)
		except Exception as e:
			kodi_utils.logger('movies menu error', '%s: %s' % (self.action, e))
		kodi_utils.set_category(self.handle, ls(params_get('name')))
		kodi_utils.set_sort_method(self.handle, content_type)
		kodi_utils.set_content(self.handle, content_type)
		kodi_utils.end_directory(self.handle, False if self.is_widget else None)
		kodi_utils.set_view_mode(view_type, content_type)

	def build_collections_results(self):
		# Construit les résultats de collections TMDb comme dossiers navigables.
		# Le contexte saga/favoris doit rester local à cette méthode : utiliser un
		# alias défini seulement dans worker() retire silencieusement le menu contextuel.
		params_get = self.params.get
		saga_type = params_get('saga_type', '')
		fav_type = params_get('fav_type') or collection_favourite_type_from_saga_type(saga_type)
		def _process():
			for item in self.list:
				collection_id = item.get('id')
				name = item.get('name') or 'Saga'
				if not collection_id: continue
				url_params = build_url({'mode': 'build_movie_list', 'action': 'tmdb_movies_collection', 'collection_id': collection_id, 'name': name, 'saga_type': saga_type, 'fav_type': fav_type})
				poster_path, backdrop_path = item.get('poster_path'), item.get('backdrop_path')
				if poster_path: poster = tmdb_image_base % (image_resolution['poster'], poster_path)
				else: poster = poster_empty
				if backdrop_path: fanart = tmdb_image_base % (image_resolution['fanart'], backdrop_path)
				else: fanart = fanart_empty
				listitem = kodi_utils.make_listitem()
				listitem.setLabel(name)
				kodi_utils.set_listitem_video_info(listitem, {'plot': item.get('overview', ''), 'mediatype': 'set', 'title': name})
				listitem.setArt(kodi_utils.clean_art({'icon': poster, 'poster': poster, 'thumb': poster, 'fanart': fanart, 'landscape': fanart or poster}))
				try:
					context_items = []
					info_extra_params = build_url({'mode': 'info_extra_choice', 'media_type': 'collection', 'tmdb_id': collection_id, 'collection_id': collection_id, 'title': name, 'fav_type': fav_type, 'saga_type': saga_type, 'is_widget': string(self.is_widget).lower()})
					context_items.append((extras_str, run_plugin % info_extra_params))
					if settings.context_favourites_enabled():
						fav_params = build_url({'mode': 'favourites_choice', 'media_type': 'collection', 'fav_type': fav_type, 'saga_type': saga_type, 'tmdb_id': collection_id, 'title': name})
						context_items.append((favourites_context_label(fav_type, collection_id), run_plugin % fav_params))
					if context_items: listitem.addContextMenuItems(context_items, replaceItems=False)
				except Exception as e:
					kodi_utils.logger('collections context menu error', '%s: %s' % (collection_id, e))
				yield (url_params, listitem, True)
		image_resolution = settings.get_resolution()
		self.items = list(_process())
		return self.items

