# -*- coding: utf-8 -*-
# plugin.video.alkoflix
# resources/lib/modules/dialogs.py

import json
import time
from indexers import metadata, tmdb_api
from modules import kodi_utils, source_utils, settings, user_ratings
from modules.info_windows import extras_window_xml
from modules.cache import clear_cache
from modules.utils import get_datetime, clean_file_name
# logger = kodi_utils.logger  # Décommenter temporairement pour diagnostiquer les menus/dialogues.

ls, build_url, media_path, select_dialog = kodi_utils.local_string, kodi_utils.build_url, kodi_utils.media_path, kodi_utils.select_dialog
show_busy_dialog, hide_busy_dialog, notification, ok_dialog = kodi_utils.show_busy_dialog, kodi_utils.hide_busy_dialog, kodi_utils.notification, kodi_utils.ok_dialog
get_property, set_property, clear_property, container_refresh = kodi_utils.get_property, kodi_utils.set_property, kodi_utils.clear_property, kodi_utils.container_refresh
execute_builtin, confirm_dialog, container_content, sleep = kodi_utils.execute_builtin, kodi_utils.confirm_dialog, kodi_utils.container_content, kodi_utils.sleep
get_setting, set_setting = kodi_utils.get_setting, kodi_utils.set_setting

def _refresh_settings_after_choice():
	# Les choix lancés depuis les réglages Kodi ne doivent pas fermer, rouvrir
	# ou rafraîchir la fenêtre : on vide seulement le cache interne alkoFlix.
	try: kodi_utils.clear_property('alkoflix_settings')
	except: pass

# Ce module regroupe les choix affichés dans les dialogues Kodi :
# menus contextuels, gestionnaires Trakt/MDBList/TMDb, options de scrape, couleurs, favoris, etc.
# Les identifiants techniques (movie, tvshow, watchlist, favorite, etc.) doivent rester en anglais.

# Sélection d'une vidéo IMDb disponible pour le média.
def imdb_videos_choice(videos, poster):
	try: videos = json.loads(videos)
	except: pass
	videos.sort(key=lambda x: x['quality_rank'])
	list_items = [{'line1': i['quality'], 'icon': poster} for i in videos]
	kwargs = {'items': json.dumps(list_items), 'heading': ls(32241)}
	return select_dialog([i['url'] for i in videos], **kwargs)

# Normalise le type de média avant les appels TMDb.
def _tmdb_video_media_type(media_type):
	if media_type == 'movies': return 'movie'
	if media_type in ('tvshow', 'tvshows'): return 'tv'
	return media_type

# Récupère les vidéos TMDb dans une langue précise.
# Utilisé pour prioriser les bandes-annonces françaises, puis anglaises en secours.
def _tmdb_videos_by_language(media_type, tmdb_id, language):
	if not tmdb_id: return []
	try:
		from indexers import tmdb_api
		media_type = _tmdb_video_media_type(media_type)
		tmdb_media_videos = getattr(tmdb_api, 'tmdb_media_videos', None)
		if callable(tmdb_media_videos): data = tmdb_media_videos(media_type, tmdb_id, language=language)
		else:
			base_url = getattr(tmdb_api, 'base_url', 'https://api.themoviedb.org/3').rstrip('/')
			api_key = settings.tmdb_api_key()
			url = '%s/%s/%s/videos?api_key=%s&language=%s' % (base_url, media_type, tmdb_id, api_key, language)
			get_tmdb = getattr(tmdb_api, 'get_tmdb', None)
			if callable(get_tmdb): data = get_tmdb(url)
			else:
				import requests
				data = requests.get(url, timeout=10).json()
		return (data or {}).get('results') or []
	except Exception as e:
		kodi_utils.logger('erreur récupération bandes annonces TMDb', str(e))
		return []

# Garde uniquement les vidéos YouTube lisibles par plugin.video.youtube.
def _youtube_trailers(videos):
	allowed_types = ('Trailer', 'Teaser', 'Clip', 'Featurette')
	return [
		i for i in videos or []
		if i.get('key') and (i.get('site') or '').lower() == 'youtube' and i.get('type') in allowed_types
	]

# Donne priorité aux vraies bandes-annonces officielles.
def _sort_trailers(videos):
	def _key(item):
		video_type = item.get('type') or ''
		return (
			0 if video_type == 'Trailer' else 1,
			0 if item.get('official') else 1,
			item.get('published_at') or ''
		)
	return sorted(videos or [], key=_key)

# Sélectionne une bande-annonce dans la liste préparée.
def _choose_trailer(videos, poster):
	videos = _sort_trailers(videos)
	if len(videos) > 1:
		language_labels = {'fr': 'Français', 'en': 'Anglais'}
		type_labels = {'Trailer': 'Bande annonce', 'Teaser': 'Teaser', 'Clip': 'Extrait', 'Featurette': 'Bonus'}
		list_items = [
			{
				'line1': clean_file_name(i.get('name', 'Bande annonce')),
				'line2': '%s (%s) - %s' % (
					type_labels.get(i.get('type'), i.get('type') or 'Vidéo'),
					i.get('site') or 'N/A',
					language_labels.get(i.get('iso_639_1'), i.get('iso_639_1') or 'N/A')
				),
				'icon': poster
			}
			for i in videos
		]
		kwargs = {'items': json.dumps(list_items), 'heading': 'Bande annonce', 'multi_line': 'true'}
		video_id = select_dialog([i['key'] for i in videos], **kwargs)
	else: video_id = next(iter(videos), {}).get('key')
	if video_id is None: return 'canceled'
	return 'plugin://plugin.video.youtube/play/?video_id=%s' % video_id

# Sélection d'une bande-annonce.
# Si prefer_french=True, les vidéos TMDb sont cherchées en français d'abord,
# puis en anglais seulement si aucune bande-annonce française n'est disponible.
def trailer_choice(media_type, poster, tmdb_id, trailer_url, all_trailers=None, prefer_french=False):
	if prefer_french:
		all_trailers = _youtube_trailers(_tmdb_videos_by_language(media_type, tmdb_id, 'fr-FR'))
		if not all_trailers: all_trailers = _youtube_trailers(_tmdb_videos_by_language(media_type, tmdb_id, 'en-US'))
		if not all_trailers and trailer_url: return trailer_url
	elif settings.get_language() != 'en' and not trailer_url and not all_trailers:
		from indexers.tmdb_api import tmdb_media_videos
		try: all_trailers = tmdb_media_videos(media_type, tmdb_id)['results']
		except: pass
	all_trailers = _youtube_trailers(all_trailers)
	if not all_trailers: return trailer_url
	return _choose_trailer(all_trailers, poster)

# Sélection d'un genre à partir des genres déjà associés au média.
def genres_choice(media_type, genres, poster, return_genres=False, genre_ids=None):
	from modules.meta_lists import movie_genres, tvshow_genres
	from indexers import tmdb_api

	def _normalise_genre(value):
		try:
			from html import unescape
			from unicodedata import normalize
			import re
			value = unescape(str(value or '')).lower()
			value = value.replace('&', ' et ').replace('+', ' et ')
			value = value.replace('sci-fi', 'science fiction').replace('science-fiction', 'science fiction')
			value = normalize('NFKD', value).encode('ascii', 'ignore').decode('ascii')
			value = re.sub(r'[^a-z0-9]+', ' ', value)
			return re.sub(r'\s+', ' ', value).strip()
		except:
			return str(value or '').lower().strip()

	def _genre_aliases(media_type):
		# Filet de secours uniquement : la correspondance TMDb officielle est utilisée
		# en priorité. Ces alias gardent les anciennes métadonnées/caches lisibles.
		common = {
			'action adventure': 'Action et aventure', 'action et adventure': 'Action et aventure',
			'action aventure': 'Action et aventure', 'adventure action': 'Action et aventure',
			'sci fi fantasy': 'Science-fiction et fantastique', 'science fiction fantasy': 'Science-fiction et fantastique',
			'science fiction fantastique': 'Science-fiction et fantastique', 'science fiction et fantasy': 'Science-fiction et fantastique',
			'science fiction et fantastique': 'Science-fiction et fantastique', 'fantasy science fiction': 'Science-fiction et fantastique',
			'war politics': 'Guerre et politique', 'war et politics': 'Guerre et politique',
			'guerre politique': 'Guerre et politique', 'guerre et politics': 'Guerre et politique',
			'kids': 'Jeunesse', 'children': 'Jeunesse', 'family': 'Familial',
			'soap': 'Feuilleton', 'soap opera': 'Feuilleton', 'mystery': 'Mystère',
			'animation': 'Animation', 'comedy': 'Comédie', 'crime': 'Crime', 'drama': 'Drame', 'western': 'Western'
		}
		if media_type in ('movie', 'movies'):
			common.update({
				'adventure': 'Aventure', 'fantasy': 'Fantastique', 'history': 'Histoire', 'horror': 'Horreur',
				'music': 'Musique', 'romance': 'Romance', 'science fiction': 'Science-fiction',
				'tv movie': 'Téléfilm', 'war': 'Guerre', 'thriller': 'Thriller'
			})
		return {_normalise_genre(k): v for k, v in common.items()}

	def _split_genres(value):
		if isinstance(value, (list, tuple)): raw_items = value
		else:
			try: raw_items = str(value or '').replace(' / ', ',').split(',')
			except: raw_items = []
		return [i.strip() for i in raw_items if str(i or '').strip()]

	def _parse_genre_ids(value):
		if not value: return []
		if isinstance(value, (list, tuple, set)): raw_items = value
		else:
			try: raw_items = str(value or '').replace('|', ',').split(',')
			except: raw_items = []
		genre_ids = []
		for item in raw_items:
			item = str(item or '').strip()
			if item and item not in genre_ids: genre_ids.append(item)
		return genre_ids

	def _legacy_icon_for_id(genre_id, legacy_dict):
		genre_id = str(genre_id or '').strip()
		for value in legacy_dict.values():
			try:
				if str(value[0]) == genre_id: return value[1]
			except: pass
		return 'genres.png'

	def _official_tmdb_genres(media_type, legacy_dict):
		# TMDb est la source prioritaire. On charge la langue de l'utilisateur puis
		# l'anglais en secours, car certains caches/métadonnées peuvent contenir les
		# noms anglais même si l'interface est en français.
		by_id, by_name = {}, {}
		media_kind = 'movie' if media_type in ('movie', 'movies') else 'tv'
		languages = []
		try: preferred_language = settings.get_language() or 'fr-FR'
		except: preferred_language = 'fr-FR'
		for language in (preferred_language, 'fr-FR', 'en-US'):
			if language and language not in languages: languages.append(language)
		for language in languages:
			try: items = (tmdb_api.tmdb_genre_list(media_kind, language) or {}).get('genres') or []
			except: items = []
			for item in items:
				genre_id = str(item.get('id') or '').strip()
				name = str(item.get('name') or '').strip()
				if not genre_id or not name: continue
				entry = by_id.get(genre_id)
				if not entry:
					entry = {'genre': name, 'value': [genre_id, _legacy_icon_for_id(genre_id, legacy_dict)]}
					by_id[genre_id] = entry
				by_name[_normalise_genre(name)] = entry
		return by_id, by_name

	def _legacy_lookup(legacy_dict):
		return {_normalise_genre(key): key for key in legacy_dict}

	def _process_by_ids(genre_ids, official_by_id, legacy_dict):
		final_genres_list, added = [], set()
		for genre_id in _parse_genre_ids(genre_ids):
			entry = official_by_id.get(str(genre_id))
			if not entry:
				for legacy_name, legacy_value in legacy_dict.items():
					if str(legacy_value[0]) == str(genre_id):
						entry = {'genre': legacy_name, 'value': legacy_value}
						break
			if entry and str(entry['value'][0]) not in added:
				added.add(str(entry['value'][0]))
				final_genres_list.append(entry)
		return final_genres_list

	def _process_by_names(genre_str, legacy_dict, media_type, official_by_name):
		final_genres_list, added = [], set()
		legacy_lookup = _legacy_lookup(legacy_dict)
		aliases = _genre_aliases(media_type)
		for raw_genre in _split_genres(genre_str):
			normalised = _normalise_genre(raw_genre)
			entry = official_by_name.get(normalised)
			key = None
			if not entry:
				key = legacy_lookup.get(normalised) or aliases.get(normalised)
			if not entry and not key:
				for candidate, official_entry in official_by_name.items():
					if candidate and candidate in normalised:
						entry = official_entry
						break
			if not entry and not key:
				for candidate, canonical in aliases.items():
					if candidate and candidate in normalised:
						key = canonical
						break
			if not entry and not key:
				for candidate, canonical in legacy_lookup.items():
					if candidate and candidate in normalised:
						key = canonical
						break
			if not entry and key and key in legacy_dict:
				entry = {'genre': key, 'value': legacy_dict[key]}
			if entry and str(entry['value'][0]) not in added:
				added.add(str(entry['value'][0]))
				final_genres_list.append(entry)
		return final_genres_list

	if media_type in ('movie', 'movies'):
		genre_action, meta_type, action = movie_genres, 'movie', 'tmdb_movies_genres'
	else: genre_action, meta_type, action = tvshow_genres, 'tvshow', 'tmdb_tv_genres'

	official_by_id, official_by_name = _official_tmdb_genres(media_type, genre_action)
	genre_list = _process_by_ids(genre_ids, official_by_id, genre_action)
	if not genre_list: genre_list = _process_by_names(genres, genre_action, media_type, official_by_name)
	if return_genres: return genre_list
	if len(genre_list) == 0: return notification(32760, 1500)
	mode = 'build_%s_list' % meta_type
	choices = [{'mode': mode, 'action': action, 'genre_id': i['value'][0]} for i in genre_list]
	list_items = [{'line1': i['genre'], 'icon': poster} for i in genre_list]
	kwargs = {'items': json.dumps(list_items), 'heading': ls(32470)}
	return select_dialog(choices, **kwargs)

# Gestion des actions Trakt depuis le menu contextuel.
def trakt_manager_choice(params):
	if not get_setting('trakt_user', ''): return notification(32760, 3500)
	heading = ls(32198).replace('[B]', '').replace('[/B]', '').replace('[I]', '').replace('[/I]', '')
	icon = media_path('trakt.png')
	choices = [(ls(32499), 'Collection'), (ls(32500), 'Watchlist'), (ls(32453), 'Favorites')]
	choices += [('%s %s...' % (ls(32602), ls(32199)), 'Add'), ('%s %s...' % (ls(32603), ls(32199)), 'Remove')]
	choices += [('Basculer l’état « Abandonné »...', 'Drop')] if params['media_type'] == 'tvshow' else []
	list_items = [{'line1': item[0], 'icon': icon} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': heading}
	choice = select_dialog([i[1] for i in choices], **kwargs)
	if choice is None: return
	from indexers import trakt_api
	choice_labels = {'Collection': ls(32499), 'Watchlist': ls(32500), 'Favorites': ls(32453), 'Add': ls(32602), 'Remove': ls(32603), 'Drop': 'Abandonné'}
	choice_label = choice_labels.get(choice, choice)
	add_str, rem_str = 'Ajouter à %s ?' % choice_label, 'Retirer de %s ?' % choice_label
	if   choice == 'Collection':
		data = trakt_api.trakt_fetch_collection_watchlist('collection', params['media_type'])
		action = 'remove' if str(params['tmdb_id']) in {str(i['media_ids']['tmdb']) for i in data} else 'add'
		data = {
			i[0]: i[1] if i[0] == 'imdb' else int(i[1])
			for i in (('imdb', params.get('imdb_id')), ('tmdb', params.get('tmdb_id')), ('tvdb', params.get('tvdb_id')))
			if not i[1] in ('', 'None', None)
		}
		data = {'shows' if params['media_type'] == 'tvshow' else 'movies': [{'ids': data}]}
		if action == 'remove':
			if not confirm_dialog(text=rem_str, top_space=True): return
			trakt_api.remove_from_collection(data)
		else: trakt_api.add_to_collection(data)
	elif choice == 'Watchlist':
		data = trakt_api.trakt_fetch_collection_watchlist('watchlist', params['media_type'])
		action = 'remove' if str(params['tmdb_id']) in {str(i['media_ids']['tmdb']) for i in data} else 'add'
		data = {
			i[0]: i[1] if i[0] == 'imdb' else int(i[1])
			for i in (('imdb', params.get('imdb_id')), ('tmdb', params.get('tmdb_id')), ('tvdb', params.get('tvdb_id')))
			if not i[1] in ('', 'None', None)
		}
		data = {'shows' if params['media_type'] == 'tvshow' else 'movies': [{'ids': data}]}
		if action == 'remove':
			if not confirm_dialog(text=rem_str, top_space=True): return
			trakt_api.remove_from_watchlist(data)
		else: trakt_api.add_to_watchlist(data)
	elif choice == 'Favorites':
		data = trakt_api.trakt_fetch_favorites(params['media_type'])
		action = 'remove' if str(params['tmdb_id']) in {str(i['media_ids']['tmdb']) for i in data} else 'add'
		data = {
			i[0]: i[1] if i[0] == 'imdb' else int(i[1])
			for i in (('imdb', params.get('imdb_id')), ('tmdb', params.get('tmdb_id')), ('tvdb', params.get('tvdb_id')))
			if not i[1] in ('', 'None', None)
		}
		data = {'shows' if params['media_type'] == 'tvshow' else 'movies': [{'ids': data}]}
		if action == 'remove':
			if not confirm_dialog(text=rem_str, top_space=True): return
			trakt_api.remove_from_favorites(data)
		else: trakt_api.add_to_favorites(data)
	elif choice == 'Add': trakt_api.trakt_add_to_list(params)
	elif choice == 'Remove': trakt_api.trakt_remove_from_list(params)
	else: trakt_api.hide_unhide_trakt_items(params['tmdb_id'], 'shows', params['imdb_id'], 'dropped')

# Gestion MDBList depuis le menu contextuel.
def mdbl_manager_choice(params):
	if not (get_setting('mdblist.access_token', '') or get_setting('mdblist.token', '')): return notification(32760, 3500)
	from indexers.mdblist_api import mdbl_userlists, mdbl_list_items, mdbl_modify_list, clear_mdbl_cache
	heading = ls(32200).replace('[B]', '').replace('[/B]', '').replace('[I]', '').replace('[/I]', '')
	icon = media_path('mdblist.png')
	choices = [(str(item['id']), item['name'], '%s éléments' % item['items']) for item in mdbl_userlists() if not item['dynamic']]
	choices += [('collection', 'Collection', ''), ('watchlist', 'Liste de suivi', ''), ('favorites', ls(32453), ''), ('clear', 'Vider le cache des listes', '')]
	if not choices: return
	list_items = [{'line1': item[1], 'line2': item[2],'icon': icon} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': heading, 'multi_line': 'true'}
	choice = select_dialog([(i[0], i[1]) for i in choices], **kwargs)
	if choice is None: return
	if choice[0] == 'clear':
		clear_mdbl_cache()
		return mdbl_manager_choice(params)
	add_str, rem_str = 'Ajouter à %s ?' % choice[1], 'Retirer de %s ?' % choice[1]
	data = mdbl_list_items(choice[0], None)
	action = False if str(params['tmdb_id']) in {str(i.get('id')) for i in (data or []) if isinstance(i, dict)} else True
	if not action and not confirm_dialog(text=rem_str, top_space=True): return
	key = 'shows' if params['media_type'] == 'tvshow' else 'movies'
	val = [{'tmdb': int(params.get('tmdb_id')), 'imdb': params.get('imdb_id')}]
	if mdbl_modify_list(choice[0], {key: val}, action):
		clear_cache('mdblist', silent=True) if choice[0] in ('collection', 'watchlist', 'favorites') else clear_mdbl_cache()
		notification(32576)
		if not action: container_refresh()
	else: notification(32574)

# Gestion des listes TMDb, favoris et liste de suivi.
def tmdb_manager_choice(params):
	if not get_setting('tmdb.token', ''): return notification(32760, 3500)
	from indexers import tmdb_api
	image_resolution, tmdb_image_base = settings.get_resolution(), tmdb_api.tmdb_image_base
	heading = ls(tmdb_api.list_heading).replace('[B]', '').replace('[/B]', '')
	icon = media_path('tmdb.png')
	list_name = params.get('trakt_list_name') or params.get('mdbl_list_name') or ''
	choices = []
	choices += [
		(str(item['id']), item['name'], '%s éléments' % item['number_of_items'],
		 tmdb_image_base % (image_resolution['poster'], item['poster_path']) if item['poster_path'] else icon)
		for item in tmdb_api.user_lists_all()
	]
	if not list_name:
		watch_media_type = 'Série TV' if params['media_type'] == 'tvshow' else 'Film'
		choices += [('watchlist', 'Liste de suivi', watch_media_type, icon), ('favorite', 'Favoris', watch_media_type, icon)]
	choices += [('new', 'Créer une nouvelle liste', list_name, icon), ('clear', 'Vider le cache des listes', '', icon)]
	if not choices: return
	list_items = [{'line1': item[1], 'line2': item[2], 'icon': item[3]} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': heading, 'multi_line': 'true'}
	choice = select_dialog([(i[0], i[1]) for i in choices], **kwargs)
	if choice is None: return
	if 'new' in choice[0]:
		obj = tmdb_api.list_obj.copy()
		obj['name'] = kodi_utils.dialog.input('Nom de la nouvelle liste', defaultt=list_name)
		if not obj['name']: return tmdb_manager_choice(params)
		if not tmdb_api.list_create(obj)['success']: return notification(32574)
		tmdb_api.clear_tmdbl_cache()
		return tmdb_manager_choice(params)
	if 'clear' in choice[0]:
		tmdb_api.clear_tmdbl_cache()
		return tmdb_manager_choice(params)
	if 'trakt_list_id' in params or 'mdbl_list_id' in params:
		function = tmdb_api.import_trakt_list if 'trakt_list_id' in params else tmdb_api.import_mdbl_list
		return function({**params, 'list_id': choice[0]})
	add_str, rem_str = 'Ajouter à %s ?' % choice[1], 'Retirer de %s ?' % choice[1]
	params['media_type'] = 'tv' if params['media_type'] == 'tvshow' else 'movie'
	if 'watchlist' in choice[0] or 'favorite' in choice[0]:
		if 'watchlist' == choice[0]:
			list_type, data = 'watchlist', tmdb_api.all_items(tmdb_api.watchlist, params['media_type'])
		else: list_type, data = 'favorite', tmdb_api.all_items(tmdb_api.favorite, params['media_type'])
		action = False if str(params['tmdb_id']) in {str(i.get('id')) for i in (data or []) if isinstance(i, dict)} else True
		item = {'media_type': params['media_type'], 'media_id': params['tmdb_id'], list_type: action}
		if not action and not confirm_dialog(text=rem_str, top_space=True): return
		if tmdb_api.add_to_watchlist_favorite(item, list_type)['success']:
			tmdb_api.clear_tmdbl_cache()
			if not action: container_refresh()
			return notification(32576)
		else: return notification(32574)
	items = {'items': [{'media_type': params['media_type'], 'media_id': params['tmdb_id']}]}
	status = tmdb_api.list_status(choice[0], params['media_type'], params['tmdb_id'])
	if status and status['success']:
		if not confirm_dialog(text=rem_str, top_space=True): return
		action, function = False, tmdb_api.list_remove_items
	else: action, function = True, tmdb_api.list_add_items
	if function(choice[0], items)['success']:
		tmdb_api.clear_tmdbl_cache()
		if not action: container_refresh()
		notification(32576)
	else: notification(32574)

# Lecture d'un épisode aléatoire, avec ou sans enchaînement continu.
def random_choice(choice, meta):
	tmdb_id = meta.get('tmdb_id')
	if not tmdb_id: return
	from modules.episode_tools import get_random_episode
	from modules.sources import SourceSelect
	meta, play_params = get_random_episode(tmdb_id, True if choice == 'play_random_continual' else False)
	if not play_params: return notification(32760)
	SourceSelect.factory(play_params)

# Menu de relance du scrape avec différentes options.
def playback_choice(content, poster, meta):
	items = [
		('clear_and_rescrape', ls(32014)),
		('rescrape_with_disabled', ls(32006)),
		('scrape_with_filters_ignored', ls(32807)),
		('scrape_with_custom_values', ls(32135))
	]
	list_items = [{'line1': i[1], 'icon': poster} for i in items]
	kwargs = {'items': json.dumps(list_items), 'heading': ls(32174)}
	choice = select_dialog([i[0] for i in items], **kwargs)
	if choice is None: return
	if choice == 'clear_and_rescrape': clear_and_rescrape(content, meta)
	elif choice == 'rescrape_with_disabled': rescrape_with_disabled(content, meta)
	elif choice == 'scrape_with_filters_ignored': scrape_with_filters_ignored(content, meta)
	else: scrape_with_custom_values(content, meta)

# Sélection des qualités vidéo autorisées.
def set_quality_choice(quality_setting):
	include = ls(32188)
	dl = ['%s SD' % include, '%s 720p' % include, '%s 1080p' % include, '%s 4K' % include]
	fl = ['SD', '720p', '1080p', '4K']
	try: preselect = [fl.index(i) for i in get_setting(quality_setting).split(', ')]
	except: preselect = []
	list_items = [{'line1': item} for item in dl]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix', 'multi_choice': 'true', 'preselect': preselect}
	choice = select_dialog(fl, **kwargs)
	if choice is None: return
	if choice == []:
		ok_dialog(text=32574, top_space=True)
		return set_quality_choice(quality_setting)
	set_setting(quality_setting, ', '.join(choice))
	_refresh_settings_after_choice()

# Sélection des sous-menus affichés dans la fenêtre Extras.
def extras_lists_choice():
	fl = [2050, 2051, 2052, 2053, 2054, 2055, 2056, 2057, 2058, 2059, 2060, 2061, 2062]
	dl = [
		ls(32664), ls(32503), ls(32607), ls(32984), ls(32986), ls(32989), ls(32531), ls(32616), ls(32617),
		'%s %s' % (ls(32612), ls(32543)), '%s %s' % (ls(32612), ls(32470)),
		'%s %s' % (ls(32612), ls(32480)), '%s %s' % (ls(32612), ls(32499))
	]
	try: preselect = [fl.index(i) for i in settings.extras_enabled_menus()]
	except: preselect = []
	list_items = [{'line1': item} for item in dl]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix', 'multi_choice': 'true', 'preselect': preselect}
	selection = select_dialog(fl, **kwargs)
	if selection == []:
		set_setting('extras.enabled_menus', 'noop')
		_refresh_settings_after_choice()
		return
	elif selection is None: return
	selection = [str(i) for i in selection]
	set_setting('extras.enabled_menus', ','.join(selection))
	_refresh_settings_after_choice()

# Sélection des langues à inclure dans un filtre de recherche.
def set_language_filter_choice(filter_setting):
	from modules.meta_lists import language_choices
	lang_choices = language_choices
	lang_choices.pop('None')
	dl = list(lang_choices.keys())
	fl = list(lang_choices.values())
	try: preselect = [fl.index(i) for i in get_setting(filter_setting).split(', ')]
	except: preselect = []
	list_items = [{'line1': item} for item in dl]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix', 'multi_choice': 'true', 'preselect': preselect}
	choice = select_dialog(fl, **kwargs)
	if choice is None: return
	if choice == []:
		set_setting(filter_setting, 'eng')
		_refresh_settings_after_choice()
		return
	set_setting(filter_setting, ', '.join(choice))
	_refresh_settings_after_choice()

# Les dossiers locaux comme sources/scrapers ne sont plus pris en charge.
def folder_scraper_manager_choice(folder_info=None):
	notification('Les dossiers locaux ne sont plus pris en charge dans alkoFlix.', 2500)
	return

def results_sorting_choice():
	quality, provider, size = ls(32241), ls(32583), ls(32584)
	choices = [
		('%s, %s, %s' % (quality, provider, size), '1'), ('%s, %s, %s' % (quality, size, provider), '2'),
		('%s, %s, %s' % (provider, quality, size), '3'), ('%s, %s, %s' % (provider, size, quality), '4'),
		('%s, %s, %s' % (size, quality, provider), '5'), ('%s, %s, %s' % (size, provider, quality), '6')
	]
	list_items = [{'line1': item[0]} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix'}
	choice = select_dialog(choices, **kwargs)
	if choice:
		set_setting('results.sort_order_display', choice[0])
		set_setting('results.sort_order', choice[1])
		_refresh_settings_after_choice()



def results_sort_priority_choice():
	choices = (('Langue', 'language'), ('Qualité', 'quality'))
	current = get_setting('results.sort_priority', 'language')
	try: preselect = [index for index, item in enumerate(choices) if item[1] == current]
	except: preselect = [0]
	if not preselect: preselect = [0]
	list_items = [{'line1': item[0]} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix', 'preselect': preselect}
	choice = select_dialog(choices, **kwargs)
	if choice:
		set_setting('results.sort_priority_display', choice[0])
		set_setting('results.sort_priority', choice[1])
		_refresh_settings_after_choice()

def results_priority_quality_choice():
	# Choix volontairement court : la qualité sélectionnée passe toujours en tête
	# des résultats alkoFlix intégrés, avant les autres critères de tri.
	choices = ('4K', '1080p', '720p')
	current = get_setting('results.priority_quality', '4K')
	try: preselect = [choices.index(current)]
	except: preselect = [0]
	list_items = [{'line1': item} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix', 'preselect': preselect}
	choice = select_dialog(choices, **kwargs)
	if choice in choices:
		set_setting('results.priority_quality', choice)
		set_setting('results.priority_quality_display', choice)
		_refresh_settings_after_choice()

def genre_sort_choice(params):
	# Dialogue lancé depuis l'appui long sur un dossier TMDb compatible.
	# Le nom historique genre_sort est conservé pour éviter de toucher aux routes
	# déjà validées, mais il sert maintenant aussi aux plateformes, diffuseurs,
	# années et autres dossiers Discover filtrables.
	try:
		target = json.loads(params.get('target') or '{}')
	except:
		target = {}
	mode = str(target.get('mode') or '') if isinstance(target, dict) else ''
	action = str(target.get('action') or '') if isinstance(target, dict) else ''
	if not isinstance(target, dict) or mode not in ('build_movie_list', 'build_tvshow_list') or not action.startswith('tmdb_'):
		return notification(32760, 1500)
	choices = (
		('Popularité', 'popular', 'popular.png'),
		('Nouveautés', 'date', 'fresh.png'),
		('Mieux notés', 'rated', 'star.png'),
		('Aléatoire', 'random', 'shuffle.png')
	)
	current_sort = target.get('genre_sort', '')
	preselect = []
	try:
		preselect = [max(0, [i[1] for i in choices].index(current_sort))] if current_sort else []
	except:
		preselect = []
	list_items = [{'line1': i[0], 'icon': media_path(i[2])} for i in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'Trier par...', 'preselect': preselect}
	choice = select_dialog([i[1] for i in choices], **kwargs)
	if choice is None: return
	target['genre_sort'] = choice
	target['new_page'] = '1'
	# Nettoyage des clés de transport qui ne doivent pas être rejouées dans l'URL finale.
	for key in ('handle', 'isFolder', 'new_letter', 'tmdb_full_skip'):
		target.pop(key, None)
	try:
		label = next(i[0] for i in choices if i[1] == choice)
		base_name = str(target.get('name') or '').split(' - ')[0].strip()
		if base_name: target['name'] = '%s - %s' % (base_name, label)
	except:
		pass
	return kodi_utils.navigation_update(target)


def list_sort_choice(params):
	# Dialogue lancé depuis l'appui long sur une liste Trakt/MDBList ou
	# sur les dossiers personnels Trakt affichés par alkoFlix.
	try:
		target = json.loads(params.get('target') or '{}')
	except:
		target = {}
	if not isinstance(target, dict):
		return notification(32760, 1500)
	target_mode = str(target.get('mode') or '')
	target_action = str(target.get('action') or '')
	personal_trakt_actions = ('trakt_collection', 'trakt_watchlist', 'trakt_favorites')
	history_actions = ('in_progress_movies', 'watched_movies', 'in_progress_tvshows', 'trakt_in_progress_tvshows', 'watched_tvshows')
	valid_target = False
	if target_mode in ('build_trakt_list', 'build_mdb_list'):
		valid_target = bool(target.get('list_id'))
	elif target_mode == 'build_movie_list' and target_action == 'tmdb_movies_public_list':
		valid_target = bool(target.get('list_id'))
	elif target_mode in ('build_movie_list', 'build_tvshow_list') and target_action in personal_trakt_actions + history_actions:
		valid_target = True
	elif target_mode == 'build_in_progress_episode':
		valid_target = True
	if not valid_target:
		return notification(32760, 1500)
	choices = (
		('Prédéfini', 'original', 'sets.png'),
		('Titre (A-Z)', 'title_asc', 'sets.png'),
		('Titre (Z-A)', 'title_desc', 'sets.png'),
		('Date récente d’abord', 'date_desc', 'fresh.png'),
		('Date ancienne d’abord', 'date_asc', 'calendar.png'),
		('Ajout récent d’abord', 'added_desc', 'star.png'),
		('Ajout ancien d’abord', 'added_asc', 'star.png'),
		('Mieux notés', 'rating_desc', 'star.png'),
		('Aléatoire', 'random', 'shuffle.png')
	)
	try:
		from modules.list_sorting import normalise_list_sort_key
		current_sort = normalise_list_sort_key(target.get('list_sort', ''))
	except:
		current_sort = str(target.get('list_sort', '') or '')
	try:
		preselect = [max(0, [i[1] for i in choices].index(current_sort))] if current_sort else []
	except:
		preselect = []
	list_items = [{'line1': i[0], 'icon': media_path(i[2])} for i in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'Trier par...', 'preselect': preselect}
	choice = select_dialog([i[1] for i in choices], **kwargs)
	if choice is None: return
	# « Prédéfini » doit être strictement identique au clic normal sur le dossier.
	# On retire donc le paramètre list_sort au lieu d'envoyer list_sort=original :
	# certaines listes rapides (en cours/vus) appliquent déjà leur ordre par défaut
	# lorsque le paramètre est absent.
	if choice == 'original':
		target.pop('list_sort', None)
		target.pop('list_random_seed', None)
	else:
		target['list_sort'] = choice
		if choice == 'random':
			target['list_random_seed'] = str(time.time())
		else:
			target.pop('list_random_seed', None)
	target.pop('regional_filter', None)
	target['new_page'] = '1'
	for key in ('handle', 'isFolder', 'new_letter', 'tmdb_full_skip'):
		target.pop(key, None)
	try:
		base_name = str(target.get('name') or '').split(' - ')[0].strip()
		if choice == 'original':
			if base_name: target['name'] = base_name
		else:
			label = next(i[0] for i in choices if i[1] == choice)
			if base_name: target['name'] = '%s - %s' % (base_name, label)
	except:
		pass
	return kodi_utils.navigation_update(target)


# Choix du type de surbrillance dans la fenêtre des résultats.
def results_highlights_choice():
	choices = [(ls(32240), '0'), (ls(32583), '1'), (ls(32241), '2')]
	list_items = [{'line1': item[0]} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix'}
	choice = select_dialog([i[1] for i in choices], **kwargs)
	if choice:
		set_setting('highlight.type', choice)
		_refresh_settings_after_choice()
		return

# Choix de l'affichage XML des résultats.
# Les valeurs affichées restent en français; settings.results_xml_style() les convertit
# vers les noms techniques attendus par les layouts XML.
def results_layout_choice():
	choices = ['Liste', 'InfoListe', 'Liste large']
	try: current = settings.normalize_results_xml_style_display()
	except Exception: current = get_setting('results.xml_style', 'Liste large')
	preselect = choices.index(current) if current in choices else 2
	list_items = [{'line1': item} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix', 'preselect': [preselect]}
	choice = select_dialog(choices, **kwargs)
	if choice in choices:
		set_setting('results.xml_style', choice)
		_refresh_settings_after_choice()

# Choix du comportement des sous-titres.
def set_subtitle_choice():
	choices = [(ls(32192), '0'), (ls(32193), '1'), (ls(32027), '2')]
	list_items = [{'line1': item[0]} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix'}
	choice = select_dialog([i[1] for i in choices], **kwargs)
	if choice:
		set_setting('subtitles.subs_action', choice)
		_refresh_settings_after_choice()
		return

# Choix de la couleur de surbrillance des dialogues de scrapers.
def scraper_dialog_color_choice(setting):
	# Ancienne option retirée : les couleurs des dialogues de scraping sont fixes.
	return

# Choix de la couleur associée aux qualités vidéo.
def scraper_quality_color_choice(setting):
	chosen_color = color_choice()
	if chosen_color:
		set_setting(setting, chosen_color)
		_refresh_settings_after_choice()

# Choix de la couleur associée à un provider ou type de source.
def scraper_color_choice(setting):
	choices = {
		'aiostreams': 'provider.aiostreams_colour',
		'aiostreams_custom': 'provider.aiostreams_colour',
		'custom_1': 'provider.custom_1_colour',
		'custom_2': 'provider.custom_2_colour',
		'custom_stremio1': 'provider.custom_1_colour',
		'custom_stremio2': 'provider.custom_2_colour',
		'ygg': 'provider.ygg_colour',
		'c411': 'provider.c411_colour',
		'torr9': 'provider.torr9_colour',
		'tr4ker': 'provider.tr4ker_colour',
		'easynews': 'provider.easynews_colour',
		'debrid_cloud': 'provider.debrid_cloud_colour',
		'folders': 'provider.folders_colour',
		'hoster': 'hoster.identify',
		'torrent': 'torrent.identify',
		'ad': 'provider.ad_colour',
		'rd': 'provider.rd_colour',
		'tb': 'provider.tb_colour'
	}
	setting = choices.get(setting)
	if not setting: return
	chosen_color = color_choice()
	if chosen_color:
		set_setting(setting, chosen_color)
		_refresh_settings_after_choice()

# Sélecteur générique de couleur.
def color_choice(msg_dialog='alkoFlix', no_color=False):
	from modules.meta_lists import meta_colors
	color_chart = meta_colors
	color_display = ['[COLOR %s]%s[/COLOR]' % (i, i.capitalize()) for i in color_chart]
	if no_color:
		color_chart.insert(0, 'No Color')
		color_display.insert(0, 'Aucune couleur')
	list_items = [{'line1': item} for item in color_display]
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix'}
	choice = select_dialog(color_chart, **kwargs)
	if choice is None: return
	return choice

def image_language_choice():
	from modules.meta_lists import image_languages
	langs = image_languages
	list_items = [{'line1': i['name']} for i in langs]
	kwargs = {'items': json.dumps(list_items), 'heading': 'Langue des images'}
	list_choose = select_dialog(langs, **kwargs)
	if list_choose is None: return None
	chosen_language, chosen_language_display = list_choose['iso'], list_choose['name']
	set_setting('image_language', chosen_language)
	set_setting('image_language_display', chosen_language_display)
	clear_cache('meta', silent=True)
	_refresh_settings_after_choice()


# Choix du pays utilisé pour les classifications d'âge TMDb.
def _certification_countries():
	return [
		(33203, 'US'), (33204, 'CA'), (33205, 'FR'), (33206, 'GB'), (33207, 'DE'),
		(33208, 'ES'), (33209, 'IT'), (33210, 'AU'), (33211, 'BR')
	]


def _certification_country_display(country_code=None):
	countries = _certification_countries()
	if country_code:
		for label_id, code in countries:
			if code == country_code: return ls(label_id)
	display = get_setting('certification.country_display', '')
	if display: return display
	try:
		from modules import parental_control
		country_code = parental_control.current_country()
	except:
		country_code = 'FR'
	for label_id, code in countries:
		if code == country_code: return ls(label_id)
	return country_code or 'FR'


def _normalize_certification_country_value(value=None):
	countries = _certification_countries()
	value = (value if value is not None else get_setting('certification.country', 'FR') or 'FR').strip()
	if value.upper() in [country_code for label_id, country_code in countries]:
		country_code = value.upper()
	else:
		try: index = int(value)
		except: index = 2
		if index < 0 or index >= len(countries): index = 2
		country_code = countries[index][1]
	return country_code


def _apply_certification_country(country_code=None, notify=False):
	# Application ciblée du pays de certification. On ne rafraîchit pas la fenêtre
	# des settings Kodi : on valide les valeurs internes et on laisse l'UI tranquille.
	country_code = _normalize_certification_country_value(country_code)
	label_id = next((label_id for label_id, code in _certification_countries() if code == country_code), 33205)
	country_display = ls(label_id)
	set_setting('certification.country', country_code)
	set_setting('certification.country_display', country_display)
	set_setting('parental.certification_country_display', country_display)
	try:
		from modules import parental_control
		valid_keys = [item['key'] for item in parental_control.choices_for_country(country_code)]
		if get_setting('parental.rating', '') not in valid_keys:
			default_choice = parental_control.default_choice(country_code)
			set_setting('parental.rating', default_choice['key'])
			set_setting('parental.rating_display', default_choice['label'])
	except Exception as exc:
		kodi_utils.logger('alkoFlix Parental', 'Certification default rating skipped | %s: %s' % (type(exc).__name__, exc))
	try: clear_cache('meta', silent=True)
	except Exception: pass
	try: kodi_utils.clear_property('alkoflix_settings')
	except Exception: pass
	if notify:
		notification('Pays de certification appliqué : %s' % ls(label_id), 3000)
	return country_code


def certification_country_choice(refresh=True):
	countries = _certification_countries()
	current = _normalize_certification_country_value()
	current_index = next((i for i, item in enumerate(countries) if item[1] == current), 0)
	list_items = [{'line1': ls(label_id)} for label_id, country_code in countries]
	kwargs = {'items': json.dumps(list_items), 'heading': ls(33202), 'preselect': [current_index]}
	choice = select_dialog(countries, **kwargs)
	if choice is None: return None
	label_id, country_code = choice
	# Le choix applique immédiatement le pays sans bouton séparé.
	return _apply_certification_country(country_code, notify=True)


def sync_certification_country_display():
	# Synchronise la ligne informative de l’onglet Contrôle parental avec
	# le pays réellement choisi dans l’onglet Métadonnées.
	country_code = _normalize_certification_country_value()
	label_id = next((label_id for label_id, code in _certification_countries() if code == country_code), 33205)
	country_display = ls(label_id)
	if get_setting('certification.country_display', '') != country_display:
		set_setting('certification.country_display', country_display)
	if get_setting('parental.certification_country_display', '') != country_display:
		set_setting('parental.certification_country_display', country_display)
	return country_display


def _parental_pin_input(heading):
	try:
		return kodi_utils.dialog.input(heading, type=kodi_utils.xbmcgui.INPUT_NUMERIC, option=kodi_utils.xbmcgui.ALPHANUM_HIDE_INPUT)
	except:
		try: return kodi_utils.dialog.input(heading, type=kodi_utils.xbmcgui.INPUT_NUMERIC)
		except: return ''


def _parental_validate_current_pin(parental_control):
	pin = _parental_pin_input('Code PIN parental')
	if not parental_control.verify_pin(pin):
		notification('Code PIN parental incorrect', 3000)
		return False
	return True


def _parental_pin_backup_choice(pin):
	try:
		from modules import parental_pin_backup
	except Exception as exc:
		kodi_utils.logger('Parental PIN backup module failed', '%s: %s' % (type(exc).__name__, exc))
		return
	text = ('Voulez-vous enregistrer une copie de récupération de ce code PIN dans un paste privé alkoPastes séparé ?[CR][CR]'
		'Cette copie sert uniquement en cas d’oubli du code. Elle ne sera créée ou mise à jour que si vous répondez Oui.')
	if confirm_dialog(text=text, ok_label='Oui', cancel_label='Non', top_space=True):
		if not _alkopastes_backup_api_ready():
			return ok_dialog(text='Le code PIN est enregistré localement, mais aucune copie de récupération alkoPastes n’a été créée.', top_space=True)
		try: show_busy_dialog()
		except Exception: pass
		try:
			result = parental_pin_backup.save_pin_backup(pin)
		finally:
			try: hide_busy_dialog()
			except Exception: pass
		if result.get('success'):
			return ok_dialog(text='Copie de récupération du code PIN enregistrée dans un paste privé alkoPastes.', top_space=True)
		return ok_dialog(text='Le code PIN est enregistré localement, mais l’enregistrement alkoPastes a échoué.[CR][CR]%s' % (result.get('error') or 'Erreur inconnue'), top_space=True)

	# Si le PIN est modifié et que l’utilisateur refuse la sauvegarde, on évite de conserver une ancienne copie périmée.
	if parental_pin_backup.backup_configured():
		try: show_busy_dialog()
		except Exception: pass
		try:
			result = parental_pin_backup.delete_pin_backup()
		finally:
			try: hide_busy_dialog()
			except Exception: pass
		if result.get('success') and result.get('deleted'):
			return notification('Ancienne copie alkoPastes du PIN supprimée', 3000)
		if not result.get('success'):
			return ok_dialog(text='Le code PIN est enregistré localement, mais l’ancienne copie alkoPastes n’a pas pu être supprimée.[CR][CR]%s' % (result.get('error') or 'Erreur inconnue'), top_space=True)


# Création ou modification du code PIN des réglages parentaux.
def parental_pin_setup(refresh=True):
	from modules import parental_control
	if parental_control.has_pin() and not _parental_validate_current_pin(parental_control): return
	new_pin = _parental_pin_input('Nouveau code PIN parental (4 chiffres)')
	if not parental_control.normalize_pin(new_pin):
		return notification('Le code PIN doit contenir exactement 4 chiffres', 3500)
	if not parental_control.set_pin(new_pin):
		return notification('Code PIN parental invalide', 3500)
	# Modifier le PIN ne doit pas déverrouiller automatiquement les réglages.
	parental_control.lock_settings()
	notification('Code PIN parental enregistré', 2500)
	_parental_pin_backup_choice(new_pin)
	# Pas de rafraîchissement forcé de la fenêtre des réglages Kodi.


# Déverrouille les réglages du contrôle parental dans la fenêtre courante.
def parental_pin_unlock():
	from modules import parental_control
	if not parental_control.has_pin():
		return notification('Créez d’abord un code PIN parental', 3500)
	if not _parental_validate_current_pin(parental_control):
		return
	parental_control.unlock_settings()
	notification('Réglages parentaux déverrouillés', 2500)


# Reverrouille manuellement les réglages parentaux sans attendre la fermeture des paramètres.
def parental_pin_lock():
	from modules import parental_control
	parental_control.lock_settings()
	notification('Réglages parentaux verrouillés', 2500)


# Choix de la classification maximale autorisée pour le contrôle parental.
def parental_rating_choice(refresh=True):
	from modules import parental_control
	# Le choix reste modifiable dans la fenêtre des réglages sans dépendre
	# d'un rafraîchissement forcé de Kodi.
	country = parental_control.current_country()
	country_display = _certification_country_display(country)
	choices = parental_control.choices_for_country(country)
	current_key = get_setting('parental.rating', '')
	current_index = next((i for i, item in enumerate(choices) if item['key'] == current_key), -1)
	if current_index < 0:
		default_choice = parental_control.default_choice(country)
		current_index = next((i for i, item in enumerate(choices) if item['key'] == default_choice.get('key')), 0)
	# Dialog Kodi simple et direct : évite les listes détaillées/JSON pour ce choix,
	# qui peuvent être capricieuses selon les skins et les plateformes Android.
	labels = [item['label'] for item in choices]
	selection = kodi_utils.dialog.select('Classification maximale — %s' % country_display, labels, preselect=current_index)
	if selection in (-1, None): return None
	try: choice = choices[int(selection)]
	except: return None
	set_setting('parental.rating', choice['key'])
	set_setting('parental.rating_display', choice['label'])
	try: kodi_utils.clear_property('alkoflix_settings')
	except: pass
	# Pas de rafraîchissement forcé : la valeur est enregistrée, Kodi garde la fenêtre stable.
	return choice


# Synchronise les notes existantes du fournisseur actif vers le cache alkoFlix.
def sync_existing_ratings_choice():
	provider = max(0, min(settings.ratings_provider(), 3))
	provider_name = ('Trakt', 'MDBList', 'TMDb', 'Aucun')[provider]
	if provider == 3: return notification('Aucun service de notation n’est sélectionné.', 3500)
	kodi_utils.logger('alkoFlix UserRatings', 'Sync Dialog Opened | Provider: %s' % provider_name)
	if not confirm_dialog(text='Synchroniser les notes existantes de %s vers alkoFlix ?[CR][CR]Elles seront ensuite envoyées à Kodi comme “Ma note” lors de l’affichage des médias.' % provider_name, ok_label='Synchroniser', cancel_label='Annuler', top_space=True): return
	try: kodi_utils.show_busy_dialog()
	except: pass
	try:
		result = user_ratings.sync_existing_ratings(provider)
	finally:
		try: kodi_utils.hide_busy_dialog()
		except: pass
	if not result.get('success'):
		kodi_utils.logger('alkoFlix UserRatings', 'Sync Dialog Failed | Provider: %s | Error: %s' % (provider_name, result.get('error')))
		if result.get('error') == 'unauthorized': return notification('Autorisez %s pour synchroniser les notes.' % provider_name, 3500)
		return notification(32574)
	count = result.get('count', 0)
	counts = result.get('counts') or {}
	kodi_utils.logger('alkoFlix UserRatings', 'Sync Dialog Success | Provider: %s | movies=%s | shows=%s | seasons=%s | episodes=%s | total=%s' % (provider_name, counts.get('movie', 0), counts.get('tvshow', 0), counts.get('season', 0), counts.get('episode', 0), count))
	message = '%s note(s) synchronisée(s) depuis %s vers alkoFlix.[CR][CR]Films: %s[CR]Séries: %s[CR]Saisons: %s[CR]Épisodes: %s[CR][CR]Les skins compatibles pourront les afficher dans “Ma note” quand les médias seront affichés.' % (count, provider_name, counts.get('movie', 0), counts.get('tvshow', 0), counts.get('season', 0), counts.get('episode', 0))
	ok_dialog(text=message, top_space=True)
	container_refresh()


def _favourites_provider_info(provider):
	return {
		1: ('Trakt', 'trakt_user'),
		3: ('TMDb', 'tmdb.token'),
		4: ('alkoPastes', 'alkopastes.api_key_user')
	}.get(provider, ('', ''))


def _favourites_provider_from_params(params=None, default=None):
	raw = ''
	try: raw = (params or {}).get('provider', '')
	except Exception: raw = ''
	raw = str(raw or '').strip().lower()
	if raw in ('trakt', '1'): return 1
	if raw in ('tmdb', 'tmdblist', '3'): return 3
	if raw in ('alkopastes', 'paste', 'pastes', '4'): return 4
	if default is not None: return default
	try: return settings.favourites_import_source()
	except Exception: return 4

def _alkopastes_backup_api_ready():
	try:
		from modules.alkopastes_api import has_user_api_key
		ready = has_user_api_key()
	except Exception:
		ready = bool(get_setting('alkopastes.api_key_user', ''))
	if ready: return True
	if confirm_dialog(text='alkoPastes n’est pas connecté.[CR][CR]Voulez-vous ouvrir Mes services pour ajouter votre clé API ?', ok_label='Ouvrir', cancel_label='Annuler', top_space=True):
		try:
			from modules.myservices import authorize
			authorize()
		except Exception as e:
			kodi_utils.logger('alkoPastes services open failed', '%s: %s' % (type(e).__name__, e))
	return False

# Récupère les favoris existants du fournisseur choisi vers la base locale alkoFlix.
def sync_existing_favourites_choice(params=None):
	provider = _favourites_provider_from_params(params)
	provider_name, auth_setting = _favourites_provider_info(provider)
	if not provider_name: return notification('Aucun service de récupération sélectionné.', 3500)
	if provider == 4:
		if not _alkopastes_backup_api_ready(): return
	elif not get_setting(auth_setting, ''):
		return notification('Autorisez %s pour récupérer les favoris.' % provider_name, 3500)
	text = 'Importer vos favoris depuis %s vers alkoFlix ?[CR][CR]Les favoris seront fusionnés dans la base locale alkoFlix.' % provider_name
	if not confirm_dialog(text=text, ok_label='Importer', cancel_label='Annuler', top_space=True): return
	try: kodi_utils.show_busy_dialog()
	except: pass
	try:
		from modules.favourites_sync import import_external_favourites
		result = import_external_favourites(provider, force=True)
	finally:
		try: kodi_utils.hide_busy_dialog()
		except: pass
	if not result or not result.get('success'):
		error = result.get('error') if isinstance(result, dict) else 'unknown'
		if error == 'missing_backup':
			kodi_utils.logger('alkoFlix favourites manual import skipped', '%s | no backup found' % provider_name)
			return ok_dialog(text='Aucune sauvegarde de favoris alkoPastes n’a été trouvée pour ce compte.[CR][CR]Envoyez d’abord vos favoris depuis un appareil pour créer le miroir alkoPastes.', top_space=True)
		kodi_utils.logger('alkoFlix favourites manual import failed', '%s | %s' % (provider_name, error))
		if error == 'not_authorized':
			return notification('Autorisez %s avant l’import.' % provider_name, 5000)
		return notification('Import des favoris impossible : %s' % error, 5000)
	count = result.get('count', 0)
	ok_dialog(text='%s favori(s) importé(s) depuis %s vers alkoFlix.' % (count, provider_name), top_space=True)
	container_refresh()


def export_local_favourites_choice(params=None):
	provider = _favourites_provider_from_params(params, default=0)
	provider_name, auth_setting = _favourites_provider_info(provider)
	if provider not in (1, 3) or not provider_name:
		return notification('Aucun service d’export sélectionné.', 3500)
	if not get_setting(auth_setting, ''):
		return notification('Autorisez %s pour exporter les favoris.' % provider_name, 3500)
	if provider == 1:
		text = ('Exporter vos favoris alkoFlix vers Trakt ?[CR][CR]'
			'Trakt accepte un maximum de [B]100 favoris[/B] sur le compte.[CR]'
			'Si cette limite est atteinte, alkoFlix affichera un message et gardera vos favoris locaux intacts.[CR][CR]'
			'Seuls les favoris Films et Séries peuvent être exportés vers Trakt.')
	else:
		text = ('Exporter vos favoris alkoFlix vers TMDb ?[CR][CR]'
			'Seuls les favoris Films et Séries peuvent être exportés vers TMDb.')
	if not confirm_dialog(text=text, ok_label='Exporter', cancel_label='Annuler', top_space=True): return
	show_busy_dialog()
	try:
		from modules.favourites_sync import export_local_favourites_to_provider
		result = export_local_favourites_to_provider(provider)
	finally:
		hide_busy_dialog()
	if isinstance(result, dict) and result.get('success'):
		added = result.get('added', 0)
		existing = result.get('existing', 0)
		failed = result.get('failed', 0)
		message = '%s favori(s) traité(s) vers %s.[CR][CR]Films : %s[CR]Séries : %s' % (added + existing, provider_name, result.get('movies', 0), result.get('shows', 0))
		if existing: message += '[CR]Déjà présents : %s' % existing
		if failed: message += '[CR]Échecs : %s' % failed
		return ok_dialog(text=message, top_space=True)
	error = result.get('error') if isinstance(result, dict) else 'unknown'
	if error == 'account_limit':
		return ok_dialog(text='Export Trakt impossible : la limite de 100 favoris Trakt semble atteinte.[CR][CR]Vos favoris restent bien conservés localement dans alkoFlix.', top_space=True)
	if error == 'empty':
		return ok_dialog(text='Aucun favori Film ou Série à exporter.', top_space=True)
	if error == 'not_authorized':
		return notification('Autorisez %s avant l’export.' % provider_name, 5000)
	kodi_utils.logger('alkoFlix favourites export failed', '%s | %s' % (provider_name, error))
	return notification('Export des favoris impossible : %s' % error, 5000)


def delete_external_favourites_choice(params=None):
	provider = _favourites_provider_from_params(params, default=0)
	provider_name, auth_setting = _favourites_provider_info(provider)
	if provider not in (1, 3) or not provider_name:
		return notification('Aucun service sélectionné.', 3500)
	if not get_setting(auth_setting, ''):
		return notification('Autorisez %s avant la suppression.' % provider_name, 3500)
	text = (
		'Supprimer vos favoris %s ?[CR][CR]'
		'Cette action retire les favoris de votre compte %s.[CR]'
		'Vos favoris alkoFlix locaux ne seront pas supprimés.'
	) % (provider_name, provider_name)
	if not confirm_dialog(text=text, ok_label='Supprimer', cancel_label='Annuler', top_space=True): return
	text = (
		'Confirmer la suppression des favoris %s ?[CR][CR]'
		'Cette action ne touche pas aux favoris enregistrés dans alkoFlix.'
	) % provider_name
	if not confirm_dialog(text=text, ok_label='Confirmer', cancel_label='Annuler', top_space=True): return
	show_busy_dialog()
	try:
		from modules.favourites_sync import delete_external_favourites_from_provider
		result = delete_external_favourites_from_provider(provider)
	finally:
		hide_busy_dialog()
	if isinstance(result, dict) and result.get('success'):
		deleted = int(result.get('deleted') or 0)
		movies = int(result.get('movies') or 0)
		shows = int(result.get('shows') or 0)
		failed = int(result.get('failed') or 0)
		message = '%s favori(s) supprimé(s) de %s.[CR][CR]Films : %s[CR]Séries : %s' % (deleted, provider_name, movies, shows)
		if failed: message += '[CR]Échecs : %s' % failed
		return ok_dialog(text=message, top_space=True)
	error = result.get('error') if isinstance(result, dict) else 'unknown'
	if error == 'empty':
		return ok_dialog(text='Aucun favori à supprimer sur %s.' % provider_name, top_space=True)
	if error == 'not_authorized':
		return notification('Autorisez %s avant la suppression.' % provider_name, 5000)
	kodi_utils.logger('alkoFlix external favourites delete failed', '%s | %s' % (provider_name, error))
	return notification('Suppression des favoris impossible : %s' % error, 5000)


# Gestion des notes utilisateur via le fournisseur choisi dans les réglages.
def rating_choice(params):
	media_type = params.get('media_type', 'movie')
	provider = max(0, min(settings.ratings_provider(), 3))
	if provider == 3: return notification('Aucun service de notation n’est sélectionné.', 3500)
	provider_data = (
		('Trakt', 'trakt_user', 'trakt.png'),
		('MDBList', 'mdblist_user', 'mdblist.png'),
		('TMDb', 'tmdb.token', 'tmdb.png')
	)[provider]
	provider_name, auth_setting, icon_name = provider_data
	if not get_setting(auth_setting, ''): return notification('Autorisez %s pour utiliser les notes.' % provider_name, 3500)
	icon = media_path(icon_name)
	labels = {'movie': ls(33135), 'tvshow': ls(33136), 'episode': ls(33137), 'season': ls(33141)}
	if media_type == 'season' and provider == 2:
		return notification('TMDb ne permet pas de noter une saison individuellement.', 3500)
	try:
		current_rating = user_ratings.get_current_user_rating(params)
	except Exception as e:
		kodi_utils.logger('rating get error', '%s | %s: %s' % (provider_name, type(e).__name__, e))
		current_rating = None
	heading = ls(33138) if current_rating not in ('', None) else labels.get(media_type, ls(33135))
	choices = [(str(i), '%s/10' % i, '') for i in range(1, 11)]
	if current_rating not in ('', None):
		choices.append(('delete', ls(33139), ls(33140) % current_rating))
	list_items = [{'line1': item[1], 'line2': item[2], 'icon': icon} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': '%s - %s' % (heading, provider_name), 'multi_line': 'true'}
	choice = select_dialog([i[0] for i in choices], **kwargs)
	if choice is None: return
	action_label = 'delete' if choice == 'delete' else 'set'
	kodi_utils.logger('alkoFlix UserRatings', 'Rating Action Starting | Provider: %s | Media: %s | Action: %s | Value: %s | tmdb=%s | imdb=%s | tvdb=%s | season=%s | episode=%s' % (provider_name, media_type, action_label, choice, params.get('tmdb_id'), params.get('imdb_id'), params.get('tvdb_id'), params.get('season'), params.get('episode')))
	try:
		if provider == 0:
			from indexers import trakt_api
			success = trakt_api.trakt_remove_rating(params) if choice == 'delete' else trakt_api.trakt_set_rating(params, int(choice))
		elif provider == 1:
			from indexers import mdblist_api
			success = mdblist_api.mdbl_remove_rating(params) if choice == 'delete' else mdblist_api.mdbl_set_rating(params, int(choice))
		else:
			from indexers import tmdb_api
			success = tmdb_api.tmdb_remove_rating(params) if choice == 'delete' else tmdb_api.tmdb_set_rating(params, int(choice))
	except Exception as e:
		kodi_utils.logger('rating set error', '%s | %s: %s' % (provider_name, type(e).__name__, e))
		success = False
	if success:
		kodi_utils.logger('alkoFlix UserRatings', 'Rating Action Success | Provider: %s | Media: %s | Action: %s | Value: %s' % (provider_name, media_type, action_label, choice))
		user_ratings.clear_user_ratings_cache(provider)
		notification(32576)
		container_refresh()
	else:
		kodi_utils.logger('alkoFlix UserRatings', 'Rating Action Failed | Provider: %s | Media: %s | Action: %s | Value: %s' % (provider_name, media_type, action_label, choice))
		notification(32574)

# Construit les identifiants envoyés aux services externes pour les favoris.

def sync_local_favourites_choice():
	provider = 4
	if not _alkopastes_backup_api_ready(): return
	from modules.alkopastes_backup import favourites_sync_enabled, favourites_single_device_mode
	if favourites_single_device_mode():
		return notification('Activez d’abord « Synchroniser d’autres appareils » dans les réglages.', 5000)
	if not favourites_sync_enabled():
		return notification('Activez d’abord l’envoi des favoris vers alkoPastes dans les réglages.', 5000)
	if not confirm_dialog(text='Synchroniser les favoris alkoFlix avec alkoPastes ?[CR][CR]Les favoris présents sur les autres appareils seront fusionnés avant l’écriture. Les ajouts et suppressions les plus récents seront conservés.', ok_label='Synchroniser', cancel_label='Annuler', top_space=True):
		return
	show_busy_dialog()
	try:
		from modules.favourites_sync import sync_local_favourites_to_external
		result = sync_local_favourites_to_external(provider)
	finally:
		hide_busy_dialog()
	if isinstance(result, dict) and result.get('success'):
		return ok_dialog(text='Favoris alkoPastes synchronisés.[CR][CR]Aucun code paste n’est à saisir : alkoFlix retrouve le miroir avec votre clé API.', top_space=True)
	kodi_utils.logger('alkoFlix favourites alkoPastes backup failed', '%s' % (result.get('error') if isinstance(result, dict) else 'unknown'))
	return notification('Synchronisation des favoris alkoPastes impossible.', 5000)


def restore_local_favourites_choice():
	if not _alkopastes_backup_api_ready(): return
	try:
		from modules.alkopastes_backup import favourites_single_device_mode
		if favourites_single_device_mode():
			return notification('Activez d’abord « Synchroniser d’autres appareils » dans les réglages.', 5000)
	except Exception: pass
	if not confirm_dialog(text='Récupérer les favoris depuis alkoPastes vers alkoFlix ?[CR][CR]Les ajouts seront récupérés. Les suppressions faites sur les autres appareils seront aussi appliquées ici.', ok_label='Récupérer', cancel_label='Annuler', top_space=True):
		return
	show_busy_dialog()
	try:
		from modules.alkopastes_backup import import_alkopastes_favourites
		result = import_alkopastes_favourites()
	finally:
		hide_busy_dialog()
	if isinstance(result, dict) and result.get('success'):
		container_refresh()
		return ok_dialog(text='%s favori(s) récupéré(s) depuis alkoPastes vers alkoFlix.' % result.get('count', 0), top_space=True)
	error = result.get('error') if isinstance(result, dict) else 'unknown'
	if error == 'missing_backup':
		return ok_dialog(text='Aucune sauvegarde de favoris alkoPastes n’a été trouvée pour ce compte.[CR][CR]Envoyez d’abord les favoris depuis un appareil pour créer le miroir automatique.', top_space=True)
	kodi_utils.logger('alkoFlix favourites alkoPastes restore failed', '%s' % error)
	return notification('Récupération des favoris alkoPastes impossible.', 5000)


def _favourites_ids_from_params(params):
	ids = {}
	for key in ('imdb', 'tmdb', 'tvdb'):
		value = params.get('%s_id' % key)
		if value in ('', 'None', None): continue
		try: ids[key] = value if key == 'imdb' else int(value)
		except: ids[key] = value
	return ids


def _list_favourite_storage_id(params):
	service = str(params.get('list_service') or params.get('service') or '').strip().lower()
	target_mode = str(params.get('target_mode') or params.get('list_mode') or '').strip()
	if not target_mode:
		if service == 'trakt': target_mode = 'build_trakt_list'
		elif service == 'tmdb': target_mode = 'build_tmdb_list'
		elif service in ('mdblist', 'mdb'): target_mode = 'build_mdb_list'
	if not service:
		if target_mode == 'build_trakt_list': service = 'trakt'
		elif target_mode == 'build_tmdb_list': service = 'tmdb'
		elif target_mode == 'build_mdb_list': service = 'mdblist'
	data = {'service': service, 'mode': target_mode}
	for key in ('user', 'slug', 'list_id', 'list_type', 'media_filter'):
		value = str(params.get(key) or '').strip()
		if value: data[key] = value
	# Une liste doit au minimum pouvoir être rouverte par son service et son identifiant.
	if not data.get('service') or not data.get('mode'): return ''
	if not data.get('list_id') and not (data.get('user') and data.get('slug')): return ''
	return json.dumps(data, ensure_ascii=False, sort_keys=True, separators=(',', ':'))


def _favourite_storage_id(params, media_type=None):
	media_type = _base_favourites_type(media_type or params.get('media_type'))
	if media_type == 'list':
		return _list_favourite_storage_id(params)
	base_id = params.get('tmdb_id') or params.get('media_id') or params.get('actor_id') or params.get('collection_id')
	if base_id in ('', 'None', None): return ''
	if media_type == 'season':
		season = params.get('season')
		if season in ('', 'None', None): return str(base_id)
		return '%s:%s' % (base_id, season)
	if media_type == 'episode':
		season, episode = params.get('season'), params.get('episode')
		if season in ('', 'None', None) or episode in ('', 'None', None): return str(base_id)
		return '%s:%s:%s' % (base_id, season, episode)
	return str(base_id)

# Demande une confirmation claire avant d'ajouter ou retirer un favori.
def _confirm_favourites_action(params, action):
	title = params.get('title') or params.get('name') or params.get('actor_name') or ''
	verb = 'Ajouter aux' if action else 'Retirer des'
	text = '%s favoris alkoFlix ?' % verb
	return confirm_dialog(text='%s[CR][CR]%s' % (title, text))

def _normal_favourites_media_type(media_type):
	media_type = str(media_type or '').strip()
	if media_type in ('tv', 'show', 'series', 'shows'): return 'tvshow'
	if media_type in ('movie', 'movies'): return 'movie'
	if media_type in ('person', 'people', 'actor', 'actress'): return 'person'
	if media_type in ('list', 'lists', 'liste', 'listes'): return 'list'
	if media_type in ('season', 'seasons'): return 'season'
	if media_type in ('episode', 'episodes'): return 'episode'
	if media_type in ('collection', 'collections', 'set', 'sets', 'saga', 'sagas'): return 'collection'
	return media_type

def _base_favourites_type(media_type):
	media_type = _normal_favourites_media_type(media_type)
	return 'tvshow' if media_type in ('tvshow', 'tv') else 'movie' if media_type == 'movie' else media_type

def _metadata_for_favourite(media_type, tmdb_id):
	if not tmdb_id or media_type not in ('movie', 'tvshow'): return None
	try:
		from indexers.metadata import movie_meta, tvshow_meta
		from modules import settings as _settings
		from modules.utils import get_datetime
		user_info = _settings.metadata_user_info()
		if media_type == 'movie': return movie_meta('tmdb_id', tmdb_id, user_info, get_datetime())
		return tvshow_meta('tmdb_id', tmdb_id, user_info, get_datetime())
	except Exception as e:
		try: kodi_utils.logger('alkoFlix favourites meta classify failed', '%s | %s: %s' % (media_type, type(e).__name__, e))
		except: pass
	return None


def _favourites_has_explicit_context(params):
	# Les menus spécialisés transmettent déjà leur destination. Dans ce cas, on ne
	# relit pas TMDb et on respecte strictement le contexte du menu.
	for key in ('family_type', 'documentary_type', 'native_type', 'fav_group', 'group', 'parent_fav_type', 'series_fav_type', 'tv_fav_type', 'saga_type'):
		if str(params.get(key) or '').strip(): return True
	fav_type = str(params.get('fav_type') or params.get('favourite_type') or params.get('favorite_type') or '').strip().lower()
	if fav_type and fav_type not in ('movie', 'tvshow'): return True
	action = str(params.get('action') or '').strip().lower()
	if not action: return False
	context_prefixes = (
		'favourites_movie_family', 'favourites_movie_animation', 'favourites_anime_movie', 'favourites_movie_documentary', 'favourites_movie_tv_movie',
		'favourites_tv_family', 'favourites_tv_animation', 'favourites_anime_tv', 'favourites_tv_documentary', 'favourites_tv_reality', 'favourites_tv_talkshow', 'favourites_tv_news',
		'tmdb_moviesanime_', 'trakt_moviesanime_', 'tmdb_movies_family_', 'tmdb_movies_documentary_', 'tmdb_movies_tv_movie_',
		'tmdb_tvanime_', 'trakt_tvanime_', 'tmdb_tv_family_', 'tmdb_tv_documentary_', 'tmdb_tv_reality_', 'tmdb_tv_talkshow_', 'tmdb_tv_news_'
	)
	return action.startswith(context_prefixes)


def _favourites_type_from_params(params):
	media_type = _base_favourites_type(params.get('media_type'))
	try:
		from caches.favourites_cache import favourite_type_from_context
		meta = None
		if media_type in ('movie', 'tvshow') and not _favourites_has_explicit_context(params):
			meta = _metadata_for_favourite(media_type, params.get('tmdb_id') or params.get('media_id'))
		# Le contexte transmis par le menu garde la priorité. Quand il n'y en a pas,
		# les genres TMDb servent à router les favoris hors arborescence.
		return favourite_type_from_context(media_type, params, params.get('action'), meta)
	except:
		return media_type


def _favourite_types_for_media(media_type):
	from caches.favourites_cache import MOVIE_FAVOURITE_TYPES, TV_FAVOURITE_TYPES, SEASON_FAVOURITE_TYPES, EPISODE_FAVOURITE_TYPES, COLLECTION_FAVOURITE_TYPES, LIST_FAVOURITE_TYPES
	media_type = _base_favourites_type(media_type)
	if media_type == 'movie': return MOVIE_FAVOURITE_TYPES
	if media_type == 'tvshow': return TV_FAVOURITE_TYPES
	if media_type == 'season': return SEASON_FAVOURITE_TYPES
	if media_type == 'episode': return EPISODE_FAVOURITE_TYPES
	if media_type == 'collection': return COLLECTION_FAVOURITE_TYPES
	if media_type == 'list': return LIST_FAVOURITE_TYPES
	if media_type == 'person': return ('person',)
	return (media_type,)


def _existing_favourite_types(media_type, storage_id):
	try:
		from caches.favourites_cache import favourites_cache
		existing = []
		for fav_type in _favourite_types_for_media(media_type):
			if any(str(i.get('tmdb_id')) == str(storage_id) for i in favourites_cache.get_favourites(fav_type) or []):
				existing.append(fav_type)
		return existing
	except:
		return []


def _default_favourite_label(fav_type, fallback='Favoris'):
	labels = {
		'movie': 'Favoris films', 'movie_family': 'Favoris Famille', 'movie_animation': 'Favoris Dessins animés', 'anime_movie': 'Favoris Animés japonais', 'movie_documentary': 'Favoris Docus & Télé',
		'tvshow': 'Favoris séries', 'tv_family': 'Favoris Famille', 'tv_animation': 'Favoris Dessins animés', 'anime_tv': 'Favoris Animés japonais', 'tv_documentary': 'Favoris Docus & Télé',
		'season': 'Favoris saisons', 'season_family': 'Favoris saisons Famille', 'season_animation': 'Favoris saisons Dessins animés', 'anime_season': 'Favoris saisons Animés japonais', 'season_documentary': 'Favoris saisons Docus & Télé',
		'episode': 'Favoris épisodes', 'episode_family': 'Favoris épisodes Famille', 'episode_animation': 'Favoris épisodes Dessins animés', 'anime_episode': 'Favoris épisodes Animés japonais', 'episode_documentary': 'Favoris épisodes Docus & Télé',
		'collection': 'Sagas de films', 'collection_family': 'Sagas Famille', 'collection_animation': 'Sagas films animés', 'collection_anime': 'Sagas animés japonais',
		'person': 'Favoris personnes', 'list': 'Favoris listes'
	}
	return labels.get(fav_type, fallback)


def _favourite_move_choices(media_type):
	media_type = _base_favourites_type(media_type)
	if media_type == 'movie':
		choices = [
			('movie', 'Favoris films', 'movies.png'),
			('movie_family', 'Favoris Famille', 'genre_family.png'),
			('movie_animation', 'Favoris Dessins animés', 'genre_animation.png'),
			('anime_movie', 'Favoris Animés japonais', 'anime.png'),
			('movie_documentary', 'Favoris Docus & Télé', 'genre_documentary.png')
		]
	elif media_type == 'tvshow':
		choices = [
			('tvshow', 'Favoris séries', 'tv.png'),
			('tv_family', 'Favoris Famille', 'genre_family.png'),
			('tv_animation', 'Favoris Dessins animés', 'genre_animation.png'),
			('anime_tv', 'Favoris Animés japonais', 'anime.png'),
			('tv_documentary', 'Favoris Docus & Télé', 'genre_documentary.png')
		]
	elif media_type == 'season':
		choices = [
			('season', 'Favoris saisons', 'tv.png'),
			('season_family', 'Favoris saisons Famille', 'genre_family.png'),
			('season_animation', 'Favoris saisons Dessins animés', 'genre_animation.png'),
			('anime_season', 'Favoris saisons Animés japonais', 'anime.png'),
			('season_documentary', 'Favoris saisons Docus & Télé', 'genre_documentary.png')
		]
	elif media_type == 'episode':
		choices = [
			('episode', 'Favoris épisodes', 'tv.png'),
			('episode_family', 'Favoris épisodes Famille', 'genre_family.png'),
			('episode_animation', 'Favoris épisodes Dessins animés', 'genre_animation.png'),
			('anime_episode', 'Favoris épisodes Animés japonais', 'anime.png'),
			('episode_documentary', 'Favoris épisodes Docus & Télé', 'genre_documentary.png')
		]
	elif media_type == 'collection':
		choices = [
			('collection', 'Sagas de films', 'sets.png'),
			('collection_family', 'Sagas Famille', 'genre_family.png'),
			('collection_animation', 'Sagas films animés', 'genre_animation.png'),
			('collection_anime', 'Sagas animés japonais', 'anime.png')
		]
	elif media_type == 'person': choices = [('person', 'Favoris personnes', 'favourites.png')]
	elif media_type == 'list': choices = [('list', 'Favoris listes', 'favourites.png')]
	else: choices = []
	return tuple(choices)


def _choose_favourite_target_type(params, media_type, fav_type, existing_types=None):
	choices = list(_favourite_move_choices(media_type))
	if not choices: return fav_type
	preselect = []
	for idx, item in enumerate(choices):
		if item[0] == fav_type: preselect = [idx]
	list_items = []
	for item in choices:
		line2 = ''
		if existing_types and item[0] in existing_types: line2 = 'Actuel'
		elif item[0] == fav_type: line2 = 'Suggéré'
		list_items.append({'line1': item[1], 'line2': line2, 'icon': media_path(item[2])})
	target_type = select_dialog([i[0] for i in choices], items=json.dumps(list_items), heading='Choisir le dossier favoris', use_details='true', preselect=preselect)
	return target_type


def _move_local_favourite(media_type, storage_id, title, existing_types, target_type):
	# Malgré le libellé historique « Déplacer », l'action sert surtout à classer
	# le même média dans un dossier favoris précis. On ne retire donc plus les
	# autres emplacements locaux : un film peut être à la fois dans Films et Famille.
	from caches.favourites_cache import favourites_cache
	try:
		return bool(favourites_cache.add_to_favourites(target_type, storage_id, title))
	except:
		return False

def _favourite_ids_payload(params, media_type):
	ids = _favourites_ids_from_params(params)
	if not ids.get('tmdb') and params.get('tmdb_id') not in ('', 'None', None):
		try: ids['tmdb'] = int(params.get('tmdb_id'))
		except: pass
	if not ids: return None
	if media_type == 'movie': return {'movies': [{'ids': ids}]}
	if media_type == 'tvshow': return {'shows': [{'ids': ids}]}
	if media_type == 'season':
		try: season = int(params.get('season'))
		except: return None
		return {'shows': [{'ids': ids, 'seasons': [{'number': season}]}]}
	if media_type == 'episode':
		try:
			season = int(params.get('season'))
			episode = int(params.get('episode'))
		except: return None
		return {'shows': [{'ids': ids, 'seasons': [{'number': season, 'episodes': [{'number': episode}]}]}]}
	return None

def _sync_trakt_favourite(params, action):
	try:
		if not get_setting('trakt_user', ''): return False
		from indexers import trakt_api
		media_type = _base_favourites_type(params.get('media_type'))
		payload = _favourite_ids_payload(params, media_type)
		if not payload: return False
		result = trakt_api.trakt_add_to_alkoflix_favourites(payload) if action else trakt_api.trakt_remove_from_alkoflix_favourites(payload)
		return bool(result)
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites Trakt sync failed', '%s: %s' % (type(e).__name__, e))
		return False

def _sync_mdblist_favourite(params, action):
	try:
		if not (get_setting('mdblist.access_token', '') or get_setting('mdblist.token', '')): return False
		from indexers import mdblist_api
		media_type = _base_favourites_type(params.get('media_type'))
		if media_type not in ('movie', 'tvshow'): return False
		payload = _favourite_ids_payload(params, media_type)
		if not payload: return False
		if mdblist_api.mdbl_modify_list('favorites', payload, action):
			clear_cache('mdblist', silent=True)
			return True
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites MDBList sync failed', '%s: %s' % (type(e).__name__, e))
	return False

def _sync_tmdb_favourite(params, action):
	try:
		if not get_setting('tmdb.token', ''): return False
		from indexers import tmdb_api
		media_type = _base_favourites_type(params.get('media_type'))
		if media_type not in ('movie', 'tvshow'): return False
		tmdb_media_type = 'tv' if media_type == 'tvshow' else 'movie'
		tmdb_id = int(params.get('tmdb_id') or params.get('media_id'))
		item = {'media_type': tmdb_media_type, 'media_id': tmdb_id, 'favorite': action}
		success = bool(tmdb_api.add_to_watchlist_favorite(item, 'favorite').get('success'))
		if success: tmdb_api.clear_tmdbl_cache()
		return success
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites TMDb sync failed', '%s: %s' % (type(e).__name__, e))
		return False

def _setting_enabled(setting_id):
	return str(get_setting(setting_id, 'false')).lower() == 'true'

def _has_active_external_favourites_sync():
	return (
		_setting_enabled('favourites.sync_trakt') or
		_setting_enabled('favourites.sync_tmdb') or
		_setting_enabled('alkopastes.favourites_sync_enabled')
	)

def _active_favourites_provider():
	# Conserve la compatibilité avec les anciennes sections qui attendent un fournisseur principal.
	return 4, 'alkoPastes', 'alkopastes.api_key_user'

def _sync_alkopastes_favourite(params, action):
	provider, provider_name, auth_setting = _active_favourites_provider()
	if auth_setting and not get_setting(auth_setting, ''): return False
	try:
		from modules.alkopastes_backup import favourites_sync_enabled
		sync_enabled = favourites_sync_enabled()
	except Exception:
		sync_enabled = _setting_enabled('alkopastes.favourites_sync_enabled')
	if not sync_enabled:
		try: kodi_utils.logger('alkoFlix favourites external sync', 'alkoPastes | %s | SKIPPED_DISABLED | tmdb=%s' % ('add' if action else 'remove', params.get('tmdb_id')))
		except: pass
		return False
	try:
		from modules.alkopastes_backup import mark_favourites_sync_pending
		reason = '%s:%s' % ('add' if action else 'remove', params.get('tmdb_id') or '')
		result = bool(mark_favourites_sync_pending(reason=reason))
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites alkoPastes sync failed', '%s: %s' % (type(e).__name__, e))
		result = False
	try:
		status = 'QUEUED' if result else 'SKIPPED_OR_FAILED'
		kodi_utils.logger('alkoFlix favourites external sync', '%s | %s | %s | tmdb=%s' % (provider_name, 'add' if action else 'remove', status, params.get('tmdb_id')))
	except: pass
	return result

def _sync_external_favourite(params, action, excluded_keys=None):
	# Les favoris locaux sont validés immédiatement. Les miroirs externes sont ensuite
	# mis en file d’attente pour éviter de bloquer le clic utilisateur sur les appels API.
	result = False
	try:
		from modules.favourites_background_sync import queue_external_favourite_sync
		result = bool(queue_external_favourite_sync(params, action)) or result
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites background queue failed', '%s: %s' % (type(e).__name__, e))
	result = _sync_alkopastes_favourite(params, action) or result
	return result


def _int_id(value):
	try: return int(value)
	except: return None

# Construit la liste des films/séries locaux qui doivent être retirés du
# service externe lors d'un nettoyage de favoris. Les doublons sont normaux en
# local (ex.: anime_movie + movie), donc ils sont dédupliqués ici.
def _local_external_payload_from_favourite_types(clear_types):
	from caches.favourites_cache import favourites_cache, MOVIE_FAVOURITE_TYPES, TV_FAVOURITE_TYPES, SEASON_FAVOURITE_TYPES, EPISODE_FAVOURITE_TYPES
	movies, shows = set(), set()
	season_items, episode_items = set(), set()
	for fav_type in clear_types or []:
		try: items = favourites_cache.get_favourites(fav_type)
		except: items = []
		if fav_type in MOVIE_FAVOURITE_TYPES:
			for item in items or []:
				media_id = _int_id(item.get('tmdb_id'))
				if media_id: movies.add(media_id)
		elif fav_type in TV_FAVOURITE_TYPES:
			for item in items or []:
				media_id = _int_id(item.get('tmdb_id'))
				if media_id: shows.add(media_id)
		elif fav_type in SEASON_FAVOURITE_TYPES:
			for item in items or []:
				parts = str(item.get('tmdb_id') or '').split(':')
				if len(parts) >= 2:
					show_id, season_no = _int_id(parts[0]), _int_id(parts[1])
					if show_id and season_no is not None: season_items.add((show_id, season_no))
		elif fav_type in EPISODE_FAVOURITE_TYPES:
			for item in items or []:
				parts = str(item.get('tmdb_id') or '').split(':')
				if len(parts) >= 3:
					show_id, season_no, episode_no = _int_id(parts[0]), _int_id(parts[1]), _int_id(parts[2])
					if show_id and season_no is not None and episode_no is not None: episode_items.add((show_id, season_no, episode_no))
	payload = {}
	if movies: payload['movies'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(movies)]
	show_payload = [{'ids': {'tmdb': media_id}} for media_id in sorted(shows)]
	for show_id, season_no in sorted(season_items):
		show_payload.append({'ids': {'tmdb': show_id}, 'seasons': [{'number': season_no}]})
	for show_id, season_no, episode_no in sorted(episode_items):
		show_payload.append({'ids': {'tmdb': show_id}, 'seasons': [{'number': season_no, 'episodes': [{'number': episode_no}]}]})
	if show_payload: payload['shows'] = show_payload
	return payload


# Pour les nettoyages globaux (films, séries, tout), on peut vider réellement la
# sauvegarde externe globale du service actif. Pour un dossier spécialisé
# (documentaires, famille, anime, etc.), on ne retire que les IDs locaux connus.
def _external_scope_from_clear_types(clear_types):
	from caches.favourites_cache import MOVIE_FAVOURITE_TYPES, TV_FAVOURITE_TYPES, SEASON_FAVOURITE_TYPES, EPISODE_FAVOURITE_TYPES
	clear_set = set(clear_types or [])
	movie_all = set(MOVIE_FAVOURITE_TYPES).issubset(clear_set)
	tv_all = set(TV_FAVOURITE_TYPES + SEASON_FAVOURITE_TYPES + EPISODE_FAVOURITE_TYPES).issubset(clear_set)
	return movie_all, tv_all

def _provider_payload_from_current_external_favourites(provider, include_movies=False, include_shows=False):
	payload = {}
	try:
		if provider == 4:
			# alkoPastes est un miroir de la base locale : aucun état distant à reconstruire ici.
			return {}
		if provider == 1:
			from indexers import trakt_api
			# Trakt : le nettoyage externe vise les listes personnelles alkoFlix,
			# pas les favoris natifs Trakt. Les favoris natifs servent seulement
			# à l'import manuel vers la base locale.
			if include_movies:
				movie_ids = set()
				for item in trakt_api.trakt_fetch_alkoflix_favourites_list('movie') or []:
					media_id = _int_id((item.get('media_ids') or {}).get('tmdb'))
					if media_id: movie_ids.add(media_id)
				if movie_ids: payload['movies'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(movie_ids)]
			if include_shows:
				show_payload = []
				show_ids = set()
				for item in trakt_api.trakt_fetch_alkoflix_favourites_list('tvshow') or []:
					media_id = _int_id((item.get('media_ids') or {}).get('tmdb'))
					if media_id: show_ids.add(media_id)
				show_payload.extend([{'ids': {'tmdb': media_id}} for media_id in sorted(show_ids)])
				for item in trakt_api.trakt_fetch_alkoflix_favourites_list('season') or []:
					media_id = _int_id((item.get('media_ids') or {}).get('tmdb'))
					season_no = _int_id(item.get('season'))
					if media_id and season_no is not None: show_payload.append({'ids': {'tmdb': media_id}, 'seasons': [{'number': season_no}]})
				for item in trakt_api.trakt_fetch_alkoflix_favourites_list('episode') or []:
					media_id = _int_id((item.get('media_ids') or {}).get('tmdb'))
					season_no = _int_id(item.get('season'))
					episode_no = _int_id(item.get('episode'))
					if media_id and season_no is not None and episode_no is not None: show_payload.append({'ids': {'tmdb': media_id}, 'seasons': [{'number': season_no, 'episodes': [{'number': episode_no}]}]})
				if show_payload: payload['shows'] = show_payload
		elif provider == 2:
			from indexers import mdblist_api
			data = mdblist_api.mdblist_favorites('all', None, '') or []
			if isinstance(data, tuple): data = data[0]
			movie_ids, show_ids = set(), set()
			for item in data or []:
				if not isinstance(item, dict): continue
				ids = item.get('ids') or {}
				media_id = _int_id(ids.get('tmdb') or item.get('tmdb') or item.get('tmdb_id') or item.get('id'))
				if not media_id: continue
				media_type = str(item.get('mediatype') or item.get('media_type') or '').lower()
				if include_movies and media_type in ('movie', 'movies'): movie_ids.add(media_id)
				elif include_shows and media_type in ('show', 'shows', 'series', 'tvshow', 'tv'): show_ids.add(media_id)
			if movie_ids: payload['movies'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(movie_ids)]
			if show_ids: payload['shows'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(show_ids)]
		elif provider == 3:
			from indexers import tmdb_api
			if include_movies:
				movie_ids = set()
				for item in tmdb_api.all_items(tmdb_api.favorite, 'movie') or []:
					media_id = _int_id(item.get('id'))
					if media_id: movie_ids.add(media_id)
				if movie_ids: payload['movies'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(movie_ids)]
			if include_shows:
				show_ids = set()
				for item in tmdb_api.all_items(tmdb_api.favorite, 'tv') or []:
					media_id = _int_id(item.get('id'))
					if media_id: show_ids.add(media_id)
				if show_ids: payload['shows'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(show_ids)]
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites external payload failed', '%s: %s' % (type(e).__name__, e))
	return payload

def _merge_external_payloads(*payloads):
	movies, shows = set(), {}
	for payload in payloads:
		for item in (payload or {}).get('movies') or []:
			media_id = _int_id((item.get('ids') or {}).get('tmdb'))
			if media_id: movies.add(media_id)
		for item in (payload or {}).get('shows') or []:
			ids = item.get('ids') or {}
			media_id = _int_id(ids.get('tmdb'))
			if not media_id: continue
			# Ne pas convertir une saison/épisode en série entière : Trakt utilise des listes séparées.
			if item.get('seasons'):
				key = repr(item)
				shows[key] = item
			else:
				shows[('show', media_id)] = {'ids': {'tmdb': media_id}}
	merged = {}
	if movies: merged['movies'] = [{'ids': {'tmdb': media_id}} for media_id in sorted(movies)]
	if shows: merged['shows'] = list(shows.values())
	return merged

def _sync_trakt_favourites_bulk_clear(clear_types, clear_payload=None):
	if not _setting_enabled('favourites.sync_trakt') or not get_setting('trakt_user', ''): return False
	payload = clear_payload or _local_external_payload_from_favourite_types(clear_types)
	if not payload: return False
	try:
		from indexers import trakt_api
		result = bool(trakt_api.trakt_remove_from_alkoflix_favourites(payload))
		kodi_utils.logger('alkoFlix favourites Trakt bulk sync', '%s' % ('OK' if result else 'FAILED'))
		return result
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites Trakt bulk sync failed', '%s: %s' % (type(e).__name__, e))
	return False

def _sync_tmdb_favourites_bulk_clear(clear_types, clear_payload=None):
	if not _setting_enabled('favourites.sync_tmdb') or not get_setting('tmdb.token', ''): return False
	payload = clear_payload or _local_external_payload_from_favourite_types(clear_types)
	movies = payload.get('movies') or []
	shows = [i for i in payload.get('shows') or [] if not i.get('seasons')]
	if not movies and not shows: return False
	deleted, failed = 0, 0
	try:
		from indexers import tmdb_api
		for item in movies:
			tmdb_id = ((item.get('ids') or {}).get('tmdb'))
			if not tmdb_id: continue
			payload_item = {'media_type': 'movie', 'media_id': tmdb_id, 'favorite': False}
			try:
				if tmdb_api.add_to_watchlist_favorite(payload_item, 'favorite').get('success'): deleted += 1
				else: failed += 1
			except Exception: failed += 1
		for item in shows:
			tmdb_id = ((item.get('ids') or {}).get('tmdb'))
			if not tmdb_id: continue
			payload_item = {'media_type': 'tv', 'media_id': tmdb_id, 'favorite': False}
			try:
				if tmdb_api.add_to_watchlist_favorite(payload_item, 'favorite').get('success'): deleted += 1
				else: failed += 1
			except Exception: failed += 1
		try: tmdb_api.clear_tmdbl_cache()
		except Exception: pass
		result = deleted > 0 and failed == 0
		kodi_utils.logger('alkoFlix favourites TMDb bulk sync', 'deleted=%s | failed=%s' % (deleted, failed))
		return result
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites TMDb bulk sync failed', '%s: %s' % (type(e).__name__, e))
	return False

def _sync_alkopastes_favourites_bulk(clear_types):
	# Après un nettoyage local, le miroir alkoPastes est seulement marqué comme
	# à synchroniser. Le service fera l’envoi plus tard, sans bloquer l’interface.
	provider, provider_name, auth_setting = _active_favourites_provider()
	if auth_setting and not get_setting(auth_setting, ''): return False
	try:
		from modules.alkopastes_backup import favourites_sync_enabled
		sync_enabled = favourites_sync_enabled()
	except Exception:
		sync_enabled = _setting_enabled('alkopastes.favourites_sync_enabled')
	if not sync_enabled:
		kodi_utils.logger('alkoFlix favourites alkoPastes bulk sync', 'SKIPPED_DISABLED')
		return False
	try:
		from modules.alkopastes_backup import mark_favourites_sync_pending
		result = bool(mark_favourites_sync_pending(reason='bulk_clear:%s' % len(clear_types or [])))
		kodi_utils.logger('alkoFlix favourites alkoPastes bulk sync', '%s' % ('QUEUED' if result else 'FAILED'))
		return result
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites alkoPastes bulk sync failed', '%s: %s' % (type(e).__name__, e))
		return False

def _sync_external_favourites_bulk(clear_types, clear_payload=None):
	result = False
	try:
		from modules.favourites_background_sync import queue_external_favourites_bulk_clear
		result = bool(queue_external_favourites_bulk_clear(clear_types, clear_payload)) or result
	except Exception as e:
		kodi_utils.logger('alkoFlix favourites background bulk queue failed', '%s: %s' % (type(e).__name__, e))
	result = _sync_alkopastes_favourites_bulk(clear_types) or result
	return result


def _external_clear_prompt_text(clear_types, provider_name):
	from caches.favourites_cache import MOVIE_FAVOURITE_TYPES, TV_FAVOURITE_TYPES, SEASON_FAVOURITE_TYPES, EPISODE_FAVOURITE_TYPES
	clear_set = set(clear_types or [])
	parts = []
	if clear_set.intersection(MOVIE_FAVOURITE_TYPES): parts.append('films')
	if clear_set.intersection(TV_FAVOURITE_TYPES): parts.append('séries')
	if provider_name == 'Trakt' and clear_set.intersection(SEASON_FAVOURITE_TYPES): parts.append('saisons')
	if provider_name == 'Trakt' and clear_set.intersection(EPISODE_FAVOURITE_TYPES): parts.append('épisodes')
	if not parts: return ''
	if len(parts) == 1: what = parts[0]
	else: what = ', '.join(parts[:-1]) + ' et ' + parts[-1]
	if provider_name == 'alkoPastes': return 'Mettre aussi à jour votre backup alkoPastes ?'
	return 'Vider aussi vos favoris %s sur %s ?' % (what, provider_name)

def _store_local_favourite(params, action):
	from caches.favourites_cache import favourites_cache, local_favourite_keys
	media_type = _base_favourites_type(params.get('media_type'))
	fav_type = _favourites_type_from_params(params)
	storage_id = _favourite_storage_id(params, media_type)
	title = params.get('title') or params.get('name') or params.get('actor_name') or ''
	if not storage_id or not media_type: return False
	success = False
	for key in local_favourite_keys(media_type, fav_type):
		if action: success = favourites_cache.add_to_favourites(key, storage_id, title) or success
		else: success = favourites_cache.remove_from_favourites(key, storage_id, title) or success
	return success

# Ajout, retrait ou nettoyage des favoris.
# Les favoris sont toujours conservés localement. Trakt/MDBList/TMDb servent
# seulement de sauvegarde globale films/séries, synchronisée en arrière-plan.
def favourites_choice(params):
	from caches.favourites_cache import favourites_cache, all_local_favourite_types, MOVIE_FAVOURITE_TYPES, TV_FAVOURITE_TYPES, SEASON_FAVOURITE_TYPES, EPISODE_FAVOURITE_TYPES, TV_OBJECT_FAVOURITE_TYPES, COLLECTION_FAVOURITE_TYPES, LIST_FAVOURITE_TYPES
	icon = media_path('favourites.png')
	if params.get('cache'):
		TV_SERIES_TYPES = TV_FAVOURITE_TYPES + SEASON_FAVOURITE_TYPES + EPISODE_FAVOURITE_TYPES
		SERIES_ROOT_TYPES = ('tvshow', 'season', 'episode')
		SERIES_FAMILY_TYPES = ('tv_family', 'season_family', 'episode_family')
		SERIES_ANIMATION_TYPES = ('tv_animation', 'season_animation', 'episode_animation')
		SERIES_ANIME_TYPES = ('anime_tv', 'anime_season', 'anime_episode')
		SERIES_DOCUMENTARY_TYPES = ('tv_documentary', 'season_documentary', 'episode_documentary')
		DOCU_TYPES = ('movie_documentary', 'movie_tv_movie', 'tv_documentary', 'tv_reality', 'tv_talkshow', 'tv_news', 'season_documentary', 'episode_documentary')
		FAMILY_TYPES = ('movie_family', 'tv_family', 'season_family', 'episode_family', 'collection_family')
		ANIMATION_TYPES = ('movie_animation', 'tv_animation', 'season_animation', 'episode_animation', 'collection_animation')
		ANIME_TYPES = ('anime_movie', 'anime_tv', 'anime_season', 'anime_episode', 'collection_anime')
		ROOT_STANDARD_MOVIE_TYPES = ('movie',)
		ROOT_STANDARD_SERIES_TYPES = ('tvshow', 'season', 'episode')
		ROOT_CLEAR_CHOICES = [
			('Vider favoris films', ROOT_STANDARD_MOVIE_TYPES),
			('Vider favoris séries', ROOT_STANDARD_SERIES_TYPES),
			('Vider favoris Docus & Télé', DOCU_TYPES),
			('Vider favoris Famille', FAMILY_TYPES),
			('Vider favoris Dessins animés', ANIMATION_TYPES),
			('Vider favoris Animés japonais', ANIME_TYPES),
			('Vider favoris personnes', ('person',)),
			('Vider favoris listes', LIST_FAVOURITE_TYPES),
			('Vider favoris sagas', COLLECTION_FAVOURITE_TYPES),
			('Vider tout', all_local_favourite_types())
		]
		clear_groups = {
			'root': [('Vider favoris films', ROOT_STANDARD_MOVIE_TYPES), ('Vider favoris séries', ROOT_STANDARD_SERIES_TYPES), ('Vider favoris listes', LIST_FAVOURITE_TYPES), ('Vider tout', ROOT_STANDARD_MOVIE_TYPES + ROOT_STANDARD_SERIES_TYPES + LIST_FAVOURITE_TYPES)],
			'all': ROOT_CLEAR_CHOICES,
			'series_root': [('Vider séries', ('tvshow',)), ('Vider saisons', ('season',)), ('Vider épisodes', ('episode',)), ('Vider tout', SERIES_ROOT_TYPES)],
			'documentary': [('Vider docu-films', ('movie_documentary', 'movie_tv_movie')), ('Vider docu-séries', SERIES_DOCUMENTARY_TYPES + ('tv_reality', 'tv_talkshow', 'tv_news')), ('Vider tout', DOCU_TYPES)],
			'series_documentary': [('Vider séries', ('tv_documentary', 'tv_reality', 'tv_talkshow', 'tv_news')), ('Vider saisons', ('season_documentary',)), ('Vider épisodes', ('episode_documentary',)), ('Vider tout', SERIES_DOCUMENTARY_TYPES + ('tv_reality', 'tv_talkshow', 'tv_news'))],
			'family': [('Vider films familiaux', ('movie_family',)), ('Vider séries familiales', SERIES_FAMILY_TYPES), ('Vider sagas familiales', ('collection_family',)), ('Vider tout', FAMILY_TYPES)],
			'series_family': [('Vider séries', ('tv_family',)), ('Vider saisons', ('season_family',)), ('Vider épisodes', ('episode_family',)), ('Vider tout', SERIES_FAMILY_TYPES)],
			'animation': [('Vider films d’animation', ('movie_animation',)), ('Vider séries d’animation', SERIES_ANIMATION_TYPES), ('Vider sagas d’animation', ('collection_animation',)), ('Vider tout', ANIMATION_TYPES)],
			'series_animation': [('Vider séries', ('tv_animation',)), ('Vider saisons', ('season_animation',)), ('Vider épisodes', ('episode_animation',)), ('Vider tout', SERIES_ANIMATION_TYPES)],
			'anime': [('Vider films animés japonais', ('anime_movie',)), ('Vider séries animées japonaises', SERIES_ANIME_TYPES), ('Vider sagas animées japonaises', ('collection_anime',)), ('Vider tout', ANIME_TYPES)],
			'series_anime': [('Vider séries', ('anime_tv',)), ('Vider saisons', ('anime_season',)), ('Vider épisodes', ('anime_episode',)), ('Vider tout', SERIES_ANIME_TYPES)],
			'people': [('Vider favoris personnes', ('person',))],
			'person': [('Vider favoris personnes', ('person',))],
			'lists': [('Vider favoris listes', LIST_FAVOURITE_TYPES)],
			'list': [('Vider favoris listes', LIST_FAVOURITE_TYPES)],
			'sagas': [('Vider favoris sagas', COLLECTION_FAVOURITE_TYPES)],
			'collection': [('Vider favoris sagas', COLLECTION_FAVOURITE_TYPES)]
		}
		group = str(params.get('fav_group') or params.get('group') or '').strip()
		if group == 'custom' and params.get('fav_types'):
			custom_types = tuple(i for i in str(params.get('fav_types') or '').split('|') if i)
			choices = [('Vider ce dossier', custom_types)]
		else:
			choices = clear_groups.get(group) or ROOT_CLEAR_CHOICES
		try:
			from caches.favourites_cache import has_favourites_types
			if group != 'custom':
				filtered_choices = []
				for label, fav_types in choices:
					if label == 'Vider tout':
						continue
					if has_favourites_types(fav_types):
						filtered_choices.append((label, fav_types))
				if filtered_choices and has_favourites_types(choices[-1][1]):
					filtered_choices.append(('Vider tout', choices[-1][1]))
				if filtered_choices:
					choices = filtered_choices
		except Exception:
			pass
		list_items = [{'line1': item[0], 'icon': icon} for item in choices]
		kwargs = {'items': json.dumps(list_items), 'heading': 'Vider les favoris'}
		selection = select_dialog([item[1] for item in choices], **kwargs)
		if selection is None: return
		try:
			import builtins as _builtins
			if isinstance(selection, (_builtins.list, _builtins.tuple, _builtins.set)):
				clear_types = _builtins.list(selection)
			else:
				clear_types = [selection]
		except Exception:
			clear_types = [selection]
		if not clear_types: return
		if not confirm_dialog(text='Vider les favoris sélectionnés ?'): return
		clear_payload = _local_external_payload_from_favourite_types(clear_types)
		from caches.favourites_cache import clear_favourites_many
		if clear_favourites_many(clear_types):
			if _has_active_external_favourites_sync():
				_sync_external_favourites_bulk(clear_types, clear_payload)
			notification(32576)
			container_refresh()
		else: notification(32574)
		return

	media_type = _base_favourites_type(params.get('media_type'))
	fav_type = _favourites_type_from_params(params)
	storage_id = _favourite_storage_id(params, media_type)
	title = params.get('title') or params.get('name') or params.get('actor_name') or ''
	if not storage_id: return notification(32574)

	existing_types = _existing_favourite_types(media_type, storage_id)
	if existing_types:
		move_choices = _favourite_move_choices(media_type)
		menu_choices = [('remove', 'Retirer des favoris', '', 'favourites.png')]
		if move_choices: menu_choices.append(('move', 'Ajouter à un autre dossier favoris', '', 'folder-plus.png'))
		if len(menu_choices) == 1:
			if not _confirm_favourites_action(params, False): return
			choice = 'remove'
		else:
			list_items = [{'line1': i[1], 'line2': i[2], 'icon': media_path(i[3])} for i in menu_choices]
			choice = select_dialog([i[0] for i in menu_choices], items=json.dumps(list_items), heading='Favoris alkoFlix', use_details='true')
			if choice is None: return
		if choice == 'remove':
			if not confirm_dialog(text='%s[CR][CR]Retirer des favoris alkoFlix ?' % title): return
			success = False
			for existing_type in existing_types:
				try: success = favourites_cache.remove_from_favourites(existing_type, storage_id, title) or success
				except: pass
			if success:
				_sync_external_favourite(params, False, excluded_keys=[(i, storage_id) for i in existing_types])
				notification(32576)
				container_refresh()
			else: notification(32574)
			return
		if choice == 'move':
			list_items = []
			preselect = []
			for idx, item in enumerate(move_choices):
				line2 = 'Actuel' if item[0] in existing_types else ''
				if item[0] in existing_types and not preselect: preselect = [idx]
				list_items.append({'line1': item[1], 'line2': line2, 'icon': media_path(item[2])})
			target_type = select_dialog([i[0] for i in move_choices], items=json.dumps(list_items), heading='Ajouter à un dossier favoris', use_details='true', preselect=preselect)
			if target_type is None: return
			if _move_local_favourite(media_type, storage_id, title, existing_types, target_type):
				excluded = [(i, storage_id) for i in existing_types if i != target_type]
				_sync_external_favourite(params, True, excluded_keys=excluded)
				notification(32576)
				container_refresh()
			else: notification(32574)
			return

	target_type = _choose_favourite_target_type(params, media_type, fav_type)
	if target_type is None: return
	if favourites_cache.add_to_favourites(target_type, storage_id, title):
		# Les miroirs optionnels actifs sont mis à jour après l'ajout local.
		_sync_external_favourite(params, True)
		notification(32576)
		container_refresh()
	else: notification(32574)

# Lance une bande-annonce depuis une action Kodi, sans passer par la fenêtre Extras.
# Avant la lecture, l'extension YouTube est vérifiée afin d'éviter un clic sans effet.
def play_trailer(content, meta):
	try:
		media_type = 'movie' if content == 'movie' else 'tvshow'
		poster = meta.get('poster')
		tmdb_id = meta.get('tmdb_id')
		trailer_url = meta.get('trailer') or meta.get('trailer_url') or ''
		url = trailer_choice(media_type, poster, tmdb_id, trailer_url, prefer_french=True)
		if not url or url == 'canceled': return notification(32760, 1500)
		if url.startswith('plugin://plugin.video.youtube'):
			purpose = 'Pour pouvoir lire la bande-annonce, l’extension YouTube doit être installée et activée.'
			if not kodi_utils.ensure_addon_installed('plugin.video.youtube', 'YouTube', purpose): return
		return execute_builtin('PlayMedia(%s)' % url)
	except Exception as e:
		kodi_utils.logger('erreur lecture bande annonce', str(e))
		return notification(32574, 1500)

# Point d'entrée utilisé par le menu contextuel natif de Kodi.
# Les métadonnées sont relues au moment du clic pour éviter de transporter
# un gros dictionnaire JSON dans l'URL du menu contextuel.
def play_trailer_choice(params):
	try:
		media_type = params.get('media_type') or params.get('content') or 'movie'
		if media_type in ('movies', 'movie'): media_type = 'movie'
		else: media_type = 'tvshow'
		function = metadata.movie_meta if media_type == 'movie' else metadata.tvshow_meta
		meta = function('tmdb_id', params['tmdb_id'], settings.metadata_user_info(), get_datetime())
		if not meta: return notification(32760, 1500)
		return play_trailer(media_type, meta)
	except Exception as e:
		kodi_utils.logger('erreur menu contextuel bande annonce', str(e))
		return notification(32574, 1500)

# Menu contextuel principal d'un média ou d'un widget.
def options_menu(params, meta=None):
	def _builder():
		for item in listing:
			kwargs = {'line1': item[1]}
			if item[2] and item[2] != item[1]: kwargs['line2'] = item[2]
			if len(item) == 4: kwargs['icon'] = item[3]
			yield kwargs
	is_widget = params.get('is_widget', 'false').lower() == 'true'
	content = params.get('content') or container_content()[:-1]
	season, episode = params.get('season'), params.get('episode')
	if not meta:
		function = metadata.movie_meta if content == 'movie' else metadata.tvshow_meta
		meta = function('tmdb_id', params['tmdb_id'], settings.metadata_user_info(), get_datetime())
	watched_indicators = settings.watched_indicators()
	on_str, off_str, currently_str, open_str, settings_str = ls(32090), ls(32027), ls(32598), ls(32641), ls(32247)
	results_xml_style_status = get_setting('results.xml_style', 'Par défaut')
	active_internal_scrapers = [i.replace('_', '') for i in settings.active_internal_scrapers()]
	uncached_torrents_status, uncached_torrents_toggle = (on_str, 'false') if settings.display_uncached_torrents() else (off_str, 'true')
	base_str1, base_str2 = '%s%s', '%s: %s' % (currently_str, '%s')
	multi_line = 'true' if content in ('movie', 'episode') else 'false'
	listing = (
		('scrape_from_episode_group', "Scraper depuis le groupe d’épisodes", '', meta['poster']) if content == 'episode' else None,
		('clear_and_rescrape', ls(32014), '', meta['poster']) if multi_line == 'true' else None,
		('rescrape_with_disabled', ls(32006), '', meta['poster']) if multi_line == 'true' else None,
		('scrape_with_filters_ignored', ls(32807), '', meta['poster']) if multi_line == 'true' else None,
		('scrape_with_custom_values', ls(32135), '', meta['poster']) if multi_line == 'true' else None,
		('play_random', ls(32541), '', meta['poster']) if content in ('tvshow') and meta else None,
		('play_random_continual', ls(32542), '', meta['poster']) if content in ('tvshow') and meta else None,
		('clear_scrapers_cache', ls(32637), '') if content in ('movie', 'episode') else None,
		('open_external_scrapers_choice', ls(32118), ''),
		('toggle_torrents_display_uncached', base_str1 % ('', ls(32160)), base_str2 % uncached_torrents_status) if multi_line == 'true' and 'external' in active_internal_scrapers else None,
		('set_results_xml_display', 'Sélecteur', base_str2 % results_xml_style_status) if multi_line == 'true' else None,
		('clear_trakt_cache', ls(32497) % ls(32037), '') if watched_indicators == 1 else None,
		('clear_mdbl_cache', ls(32497) % 'MDBList', '') if watched_indicators == 2 else None,
		('clear_media_cache', ls(32604) % (ls(32028) if content in ('movie') else ls(32029)), '', meta['poster']) if content in ('movie', 'tvshow') and meta else None,
		('open_alkoflix_settings', 'Ouvrir les paramètres alkoFlix', ''),
		('reload_widgets', 'alkoFlix : rafraîchir les widgets', '') if is_widget else None
	)
	listing = [item for item in listing if item]
	list_items = list(_builder())
	heading = ls(32646).replace('[B]', '').replace('[/B]', '')
	kwargs = {'items': json.dumps(list_items), 'heading': heading, 'multi_line': multi_line}
	choice = select_dialog([i[0] for i in listing], **kwargs)
	if   choice in (None, 'save_and_exit'): return
	elif choice == 'clear_and_rescrape': return clear_and_rescrape(content, meta, season, episode)
	elif choice == 'rescrape_with_disabled': return rescrape_with_disabled(content, meta, season, episode)
	elif choice == 'scrape_with_filters_ignored': return scrape_with_filters_ignored(content, meta, season, episode)
	elif choice == 'scrape_with_custom_values': return scrape_with_custom_values(content, meta, season, episode)
	elif choice == 'scrape_from_episode_group': return scrape_from_episode_group(meta, season, episode)
	elif choice == 'play_random': return random_choice(choice, meta)
	elif choice == 'play_random_continual': return random_choice(choice, meta)
	elif choice == 'clear_scrapers_cache': return clear_scrapers_cache()
	elif choice == 'open_external_scrapers_choice': return source_utils.enable_disable('all')
	elif choice == 'toggle_torrents_display_uncached': set_setting('torrent.display.uncached', uncached_torrents_toggle)
	elif choice == 'set_results_xml_display': results_layout_choice()
	elif choice == 'clear_trakt_cache': return clear_cache('trakt')
	elif choice == 'clear_mdbl_cache': return clear_cache('mdblist')
	elif choice == 'clear_media_cache': return refresh_cached_meta(meta)
	elif choice == 'open_alkoflix_settings': return kodi_utils.open_settings('0.0')
	# Ancienne méthode conservée comme référence : rafraîchissement ciblé des widgets.
#	elif choice == 'reload_widgets': return kodi_utils.widget_refresh()
	elif choice == 'reload_widgets': return execute_builtin('ReloadSkin()')
	if   choice in ('clear_trakt_cache', 'clear_mdbl_cache') and content in ('movie', 'tvshow', 'season', 'episode'): container_refresh()
	show_busy_dialog()
	sleep(200)
	hide_busy_dialog()
	options_menu(params, meta=meta)



def _collection_meta(collection_id, fav_type='collection'):
	data = tmdb_api.tmdb_movies_collection(collection_id) or {}
	parts = data.get('parts') or []
	poster_resolution = settings.get_resolution().get('poster', 'w342')
	fanart_resolution = settings.get_resolution().get('fanart', 'w780')
	poster_path = data.get('poster_path')
	backdrop_path = data.get('backdrop_path')
	poster = tmdb_api.tmdb_image_base % (poster_resolution, poster_path) if poster_path else media_path('box_office.png')
	fanart = tmdb_api.tmdb_image_base % (fanart_resolution, backdrop_path) if backdrop_path else media_path('transparent.png')
	parts_sorted = sorted(parts, key=lambda k: k.get('release_date') or '2050')
	years = [str(i.get('release_date') or '')[:4] for i in parts_sorted if i.get('release_date')]
	ratings = []
	for item in parts_sorted:
		try:
			value = float(item.get('vote_average') or 0)
			if value: ratings.append(value)
		except: pass
	rating = round(sum(ratings) / len(ratings), 2) if ratings else 0
	name = data.get('name') or data.get('title') or 'Saga'
	return {
		'tmdb_id': collection_id, 'imdb_id': 'None', 'tvdb_id': 'None', 'imdbnumber': 'None',
		'mediatype': 'collection', 'fav_type': fav_type or 'collection',
		'title': name, 'rootname': name, 'original_title': name, 'english_title': name,
		'poster': poster, 'poster_text': poster, 'fanart': fanart, 'landscape': fanart or poster,
		'landscape_tmdb': fanart, 'landscape_tmdb_text': fanart, 'clearlogo': '', 'tmdblogo': '',
		'year': years[0] if years else 'N/A', 'premiered': years[0] if years else 'N/A',
		'plot': data.get('overview') or '', 'tvshow_plot': data.get('overview') or '',
		'mpaa': '', 'status': 'Saga', 'studio': '%s films' % len(parts_sorted) if parts_sorted else '',
		'director': '', 'writer': '', 'duration': 0, 'genre': 'Saga', 'genre_ids': [],
		'rating': rating, 'votes': '', 'userrating': '', 'user_rating': '',
		'trailer': '', 'all_trailers': [], 'cast': [], 'parts': parts_sorted, 'parts_count': len(parts_sorted),
		'extra_info': {'status': 'Saga', 'homepage': '', 'collection_name': name, 'collection_id': collection_id},
		'meta_language': settings.get_language(), 'fanart_lang_fixed': True
	}

# Ouverture de la fenêtre Extras historique pour le média sélectionné.
def extras_menu(params):
	from windows import open_window
	try:
		media_type = params['media_type']
		if media_type in ('collection', 'collections', 'set', 'sets', 'saga', 'sagas'):
			collection_id = params.get('collection_id') or params.get('tmdb_id')
			meta = _collection_meta(collection_id, params.get('fav_type'))
		else:
			function = metadata.movie_meta if media_type == 'movie' else metadata.tvshow_meta
			meta = function('tmdb_id', params['tmdb_id'], settings.metadata_user_info(), get_datetime())
		if not meta: return execute_builtin('Action(Info)')
		open_window(('windows.extras', 'Extras'), extras_window_xml(media_type), meta=meta, is_widget=params.get('is_widget', 'false'), is_home=params.get('is_home', 'false'))
	except Exception as e:
		kodi_utils.logger('info extra alkoFlix fallback', str(e))
		return execute_builtin('Action(Info)')

# Ouverture de l'information extra depuis le menu contextuel.
# Selon le réglage utilisateur, on utilise la fenêtre native du skin ou la fenêtre moderne alkoFlix.
def info_extra_choice(params):
	if settings.use_alkoflix_info_window():
		return extras_menu(params)
	return execute_builtin('Action(Info)')


# Permet de sauter directement vers une page depuis le menu contextuel de "Page suivante".
def page_jump_choice(params):
	try:
		target_params = json.loads(params.get('target', '{}'))
	except:
		target_params = {}
	if not target_params: return
	current_page = str(target_params.get('new_page') or '1')
	page = kodi_utils.dialog.input('Aller à la page', defaultt=current_page, type=kodi_utils.xbmcgui.INPUT_NUMERIC)
	if not page: return
	try:
		page = str(max(1, int(page)))
	except:
		return notification('Numéro de page invalide.', 2500)
	target_params['new_page'] = page
	for key in ('handle', 'new_letter', 'tmdb_full_skip'):
		target_params.pop(key, None)
	kodi_utils.navigation_update(target_params)

# Rafraîchit uniquement les métadonnées en cache du média ciblé.
def refresh_cached_meta(meta):
	from caches.meta_cache import MetaCache
	try:
		media_type, tmdb_id = meta['mediatype'], meta['tmdb_id']
		MetaCache().delete(media_type, 'tmdb_id', tmdb_id, meta)
		if media_type == 'tvshow': MetaCache().delete_all_seasons_memory_cache(tmdb_id)
		notification(32576, 1500)
		container_refresh()
	except: notification(32574)

# Construction du dialogue permettant de sauter vers une page ou une lettre.
def build_navigate_to_page(params):
	use_alphabet = settings.nav_jump_use_alphabet() == 2
	icon = media_path('item_jump.png')
	media_type = params.get('media_type')
	def _builder(use_alphabet):
		for i in start_list:
			if use_alphabet: line1, line2 = i.upper(), ls(32821) % (media_type, i.upper())
			else: line1, line2 = '%s %s' % (ls(32022), i), ls(32822) % i
			yield {'line1': line1, 'line2': line2, 'icon': icon}
	if use_alphabet:
		start_list = [chr(i) for i in range(97,123)]
	else:
		start_list = [str(i) for i in range(1, int(params.get('total_pages'))+1)]
		start_list.remove(params.get('current_page'))
	list_items = list(_builder(use_alphabet))
	kwargs = {'items': json.dumps(list_items), 'heading': 'alkoFlix'}
	new_start = select_dialog(start_list, **kwargs)
	sleep(100)
	if new_start is None: return
	if use_alphabet: new_page, new_letter = '', new_start
	else: new_page, new_letter = new_start, None
	url_params = {'mode': params.get('transfer_mode', ''), 'action': params.get('transfer_action', ''), 'new_page': new_page, 'new_letter': new_letter,
				'media_type': params.get('media_type', ''), 'query': params.get('query', ''), 'actor_id': params.get('actor_id', ''),
				'user': params.get('user', ''), 'slug': params.get('slug', ''), 'list_id': params.get('list_id', ''),
				'list_type': params.get('list_type', ''), 'media_filter': params.get('media_filter', ''), 'name': params.get('name', '')}
	for key in ('list_sort', 'list_random_seed', 'regional_filter', 'genre_sort'):
		if params.get(key): url_params[key] = params.get(key)
	execute_builtin('Container.Update(%s)' % build_url(url_params))

# Vide les caches des scrapers internes et externes.
def clear_scrapers_cache(silent=False):
	for item in ('internal_scrapers', 'external_scrapers'): clear_cache(item, silent=True)
	if not silent: notification(32576)

# Supprime le cache de sources du média, puis relance un scrape propre.
def clear_and_rescrape(media_type, meta, season=None, episode=None):
	from caches.providers_cache import ExternalProvidersCache
	from modules.sources import SourceSelect
	show_busy_dialog()
	deleted = ExternalProvidersCache().delete_cache_single(media_type, str(meta['tmdb_id']))
	hide_busy_dialog()
	if not deleted: return notification(32574)
	play_params = {'mode': 'play_media', 'tmdb_id': meta['tmdb_id'], 'autoplay': 'false'}
	if media_type == 'movie': play_params.update({'media_type': 'movie'})
	else: play_params.update({'media_type': 'episode', 'season': season, 'episode': episode})
	SourceSelect().playback_prep(play_params)

# Relance le scrape en incluant aussi les sources désactivées.
def rescrape_with_disabled(media_type, meta, season=None, episode=None):
	from modules.sources import SourceSelect
	play_params = {'mode': 'play_media', 'tmdb_id': meta['tmdb_id'], 'autoplay': 'false', 'disabled_ignored': 'true', 'prescrape': 'false'}
	if media_type == 'movie': play_params.update({'media_type': 'movie'})
	else: play_params.update({'media_type': 'episode', 'season': season, 'episode': episode})
	SourceSelect().playback_prep(play_params)

# Relance le scrape en ignorant les filtres configurés.
def scrape_with_filters_ignored(media_type, meta, season=None, episode=None):
	from modules.sources import SourceSelect
	play_params = {'mode': 'play_media', 'tmdb_id': meta['tmdb_id'], 'autoplay': 'false', 'ignore_scrape_filters': 'true'}
	if media_type == 'movie': play_params.update({'media_type': 'movie'})
	else: play_params.update({'media_type': 'episode', 'season': season, 'episode': episode})
	set_property('fs_filterless_search', 'true')
	SourceSelect().playback_prep(play_params)

# Relance le scrape avec un titre, une année, une saison ou un épisode personnalisés.
def scrape_with_custom_values(media_type, meta, season=None, episode=None):
	from modules.sources import SourceSelect
	play_params = {'mode': 'play_media', 'tmdb_id': meta['tmdb_id'], 'autoplay': 'false'}
	if media_type in ('movie', 'movies'): play_params.update({'media_type': 'movie'})
	else: play_params.update({'media_type': 'episode', 'season': season, 'episode': episode})
	custom_title = kodi_utils.dialog.input(ls(32228), defaultt=meta['title'])
	if not custom_title: return
	play_params['custom_title'] = custom_title
	if media_type in ('movie', 'movies'):
		custom_year = kodi_utils.dialog.numeric(0, '%s (%s)' % (ls(32543), ls(32669)), defaultt=str(meta['year']))
		if custom_year: play_params.update({'custom_year': custom_year})
	else:
		custom_season = kodi_utils.dialog.numeric(0, '%s (%s)' % (ls(32537).title(), ls(32669)), defaultt=str(season))
		custom_episode = kodi_utils.dialog.numeric(0, '%s (%s)' % (ls(32203).title(), ls(32669)), defaultt=str(episode))
		if custom_season and custom_episode: play_params.update({'custom_season': custom_season, 'custom_episode': custom_episode})
	choice = confirm_dialog(heading='alkoFlix', text='%s?' % ls(32006), ok_label=ls(32824), cancel_label=ls(32828))
	if choice: play_params['disabled_ignored'] = 'true'
	choice = confirm_dialog(heading='alkoFlix', text=ls(32808), ok_label=ls(32824), cancel_label=ls(32828))
	if choice:
		play_params['ignore_scrape_filters'] = 'true'
		set_property('fs_filterless_search', 'true')
	SourceSelect().playback_prep(play_params)

# Permet de scraper un épisode selon un groupe d'épisodes TMDb alternatif.
def scrape_from_episode_group(meta, season=None, episode=None):
	from indexers.tmdb_api import episode_groups, episode_group_details
	from modules.sources import SourceSelect
	user_info = settings.metadata_user_info()
	tmdb_id, heading, poster = meta['tmdb_id'], meta['tvshowtitle'], meta['poster']
	groups = episode_groups(tmdb_id, user_info['tmdb_api'])
	choices = [
		(item['id'], '%s (%s)' % (item['name'], item['type']), '%s groupes, %s épisodes' % (item['group_count'], item['episode_count']))
		for item in groups
	]
	if not choices: return notification(32760)
	list_items = [{'line1': item[1], 'line2': item[2], 'icon': poster} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': heading, 'enumerate': 'true', 'multi_line': 'true'}
	choice = select_dialog([i[0] for i in choices], **kwargs)
	if choice is None: return
	# Récupère l'épisode original afin de prépositionner la sélection autour de l'épisode courant.
	episodes_data = metadata.season_episodes_meta(season, meta, user_info)
	orig_ep = next((i for i in episodes_data if i['season'] == int(season) and i['episode'] == int(episode)), {})
	title, premiered = orig_ep.get('title', ''), orig_ep.get('premiered', '')
	episodes = episode_group_details(choice, user_info['tmdb_api'])
	if not episodes: return notification(32760)
	episodes = [
		{**episode, 'custom_episode': episode['order'] + 1, 'custom_season': group['order'],
		'custom_name': f"S{group['order']}xE{episode['order'] + 1:02d} - {episode['name']}",
		'custom_title': f"S{episode['season_number']}xE{episode['episode_number']:02d} - {episode['name']}"}
		for group in episodes for episode in group['episodes']
	]
	index = next((
		episodes.index(i) for i in episodes
		if i['season_number'] == int(season) and i['episode_number'] == int(episode)
	), None)
	if index is None: preselect = []
	else: episodes, preselect = episodes[index:] + episodes[:index], [0]
	choices = [(item['custom_season'], item['custom_episode'], item['custom_name'], item['custom_title']) for item in episodes]
	if not choices: return
	list_items = [{'line1': item[2], 'line2': item[3], 'icon': poster} for item in choices]
	kwargs = {'items': json.dumps(list_items), 'heading': title, 'multi_line': 'true', 'preselect': preselect}
	choice = select_dialog([(i[0], i[1]) for i in choices], **kwargs)
	if choice is None: return
	play_params = {'mode': 'play_media', 'tmdb_id': tmdb_id, 'media_type': 'episode', 'season': season, 'episode': episode}
	play_params.update({'custom_season': choice[0], 'custom_episode': choice[1]})
	SourceSelect().playback_prep(play_params)
