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

import json
from threading import Thread
from debrids import alldebrid_api, real_debrid_api, torbox_api
from caches.debrid_cache import DebridCache
from caches.providers_cache import ExternalProvidersCache
from indexers import metadata
from modules.utils import clean_file_name
from modules.source_objects import _is_blocked_file_source, _blocked_file_value, CACHED_EXTERNAL_PROVIDERS
from modules import pack_reuse
from modules.source_result_utils import display_value
from modules.source_playback_utils import (
	direct_external_needs_cleanup, direct_external_url, is_external_source,
	is_hoster_source, is_internal_source, is_magnet_url, is_source_magnet,
	media_resolution_context
)
from modules import kodi_utils, settings
# from modules.kodi_utils import logger

ls, get_setting, notification = kodi_utils.local_string, kodi_utils.get_setting, kodi_utils.notification
show_busy_dialog, hide_busy_dialog = kodi_utils.show_busy_dialog, kodi_utils.hide_busy_dialog
ok_dialog, confirm_dialog, select_dialog = kodi_utils.ok_dialog, kodi_utils.confirm_dialog, kodi_utils.select_dialog
default_internal_scrapers, enabled_debrids_check = settings.default_internal_scrapers, settings.enabled_debrids_check
# Services capables de débrider directement des liens hoster classiques.
default_hosters_providers = ('real-debrid', 'alldebrid')
HOSTER_VALID_HOSTS = (
	'1fichier', 'rapidgator', 'turbobit', 'sendcm', 'darkibox',
	# Webshare n'est pas envoyé vers AllDebrid/Real-Debrid : non supporté dans ce flux.
	'nitroflare', 'katfile', 'fikper', 'ddownload', 'filejoker', 'filemoon',
	'streamtape', 'uptobox', 'alldebrid', 'hoster'
)
plswait_str, checking_debrid_str, remaining_debrid_str = ls(32577), ls(32578), ls(32579)

# Table centrale des services débrid supportés : nom interne, préfixe de réglage et classe API.
debrid_list = (
	('alldebrid', 'ad', alldebrid_api.AllDebridAPI),
	('real-debrid', 'rd', real_debrid_api.RealDebridAPI),
	('torbox', 'tb', torbox_api.TorBoxAPI)
)
# Instancie dynamiquement l'API correspondant au service débrid demandé.
def import_debrid(debrid_provider):
	cls = next((i[2] for i in debrid_list if i[0] == debrid_provider), None)
	return cls() if cls else cls

# Retourne uniquement les services débrid activés et authentifiés dans les réglages.
def debrid_enabled():
	return [i[0] for i in debrid_list if enabled_debrids_check(i[1])]

def debrid_type_enabled(debrid_type, enabled_debrids):
	# Les liens hosters natifs utilisent les services capables de déverrouiller une URL DDL.
	# Les anciens réglages cachés *.hoster.enabled peuvent rester à false chez certains profils :
	# on ne s'en sert donc pas comme interrupteur utilisateur pour cette couche.
	if debrid_type == 'hoster':
		return [i[0] for i in debrid_list if i[0] in enabled_debrids and i[0] in default_hosters_providers]
	return [i[0] for i in debrid_list if i[0] in enabled_debrids and get_setting('%s.%s.enabled' % (i[1], debrid_type)) == 'true']

def debrid_valid_hosts(enabled_debrids):
	# Les scrapers hosters renvoient des noms normalisés dans le champ `source`.
	# Cette liste permet d'attacher AllDebrid/Real-Debrid à ces liens sans passer par les manifests.
	valid_hosts = []
	try:
		for name in debrid_type_enabled('hoster', enabled_debrids):
			valid_hosts.append({name: list(HOSTER_VALID_HOSTS)})
	except Exception:
		pass
	return valid_hosts

# Représente une source de lecture et regroupe la logique de résolution associée.
class Source:
	def dumps(self, depth=1, width=172):
		from pprint import pformat
		return pformat(vars(self), depth=depth, width=width)

	def _safe_source_name(self, filename=None):
		try:
			return display_value(filename or getattr(self, 'filename', None) or getattr(self, 'name', None) or getattr(self, 'URLName', None))
		except:
			return 'N/A'

	def _log_source_event(self, heading, message, filename=None):
		try:
			kodi_utils.logger(heading, '%s | provider=%s | debrid=%s | source=%s | cache=%s | hash=%s | fichier=%s' % (
				message,
				display_value(getattr(self, 'provider', None)),
				display_value(getattr(self, 'debrid', None)),
				display_value(getattr(self, 'source', None)),
				display_value(getattr(self, 'cache_provider', None)),
				display_value(getattr(self, 'hash', None)),
				self._safe_source_name(filename)
			))
		except:
			pass

	def _provider_cache_key(self):
		try:
			for value in (getattr(self, 'provider', None), getattr(self, 'tracker', None), getattr(self, 'provider_name', None)):
				value = str(value or '').lower().replace('.me', '')
				if value in CACHED_EXTERNAL_PROVIDERS: return value
		except:
			pass
		return ''

	def _debrid_cache_key(self):
		try:
			value = str(getattr(self, 'debrid', '') or '').lower()
			for name, prefix, _api in debrid_list:
				if value == name or value == prefix:
					return prefix
		except:
			pass
		return ''

	def _mark_debrid_hash_cached(self, context='résolution'):
		# Un lien réellement résolu/lu doit être marqué comme exploitable côté badge AD/RD/TB.
		# Cela évite qu'un film fraîchement lancé reste en statut "Unchecked" au prochain affichage.
		try:
			item_hash = str(getattr(self, 'hash', '') or '').lower().strip()
			debrid_key = self._debrid_cache_key()
			if not item_hash or not debrid_key: return False
			DebridCache().set_many([(item_hash, 'True')], debrid_key)
			self._log_source_event('Cache débrideur', 'Hash marqué comme disponible après %s' % context)
			return True
		except:
			return False

	def _hard_tracker_hash_failure(self, error_text):
		# Ne nettoie pas la DB pour une erreur réseau/API temporaire.
		# On retire seulement les hashes qui ont échoué sur un refus clair ou une sélection impossible.
		try:
			text = str(error_text or '').lower()
			if not text: return False
			temporary_tokens = (
				'timeout', 'timed out', 'connection', 'connexion', 'network', 'réseau',
				'429', 'rate limit', 'too many requests', 'temporar', 'maintenance'
			)
			if any(token in text for token in temporary_tokens): return False
			hard_tokens = (
				'magnet non cache', 'non cache ou non prêt', 'non pret', 'non prêt',
				'uncached magnet', 'not cached', 'no cached', 'not ready',
				'upload magnet refus', 'invalid magnet', 'magnet invalide',
				'pack magnet sans liste', 'sans liste de fichiers',
				'aucun fichier vidéo compatible', 'aucun fichier video compatible',
				'blocked executable', 'exécutable', 'executable', '.m2ts'
			)
			return any(token in text for token in hard_tokens)
		except:
			return False

	def _invalidate_tracker_hash_if_needed(self, error_text, context='résolution'):
		try:
			provider = self._provider_cache_key()
			item_hash = str(getattr(self, 'hash', '') or '').lower()
			if not provider or not item_hash: return False
			if not self._hard_tracker_hash_failure(error_text): return False
			removed = ExternalProvidersCache().invalidate_hash(provider, item_hash, str(error_text or ''))
			if removed:
				self._log_source_event('Cache trackers', 'Hash retiré du cache local après échec dur (%s) | raison=%s' % (context, error_text or 'N/A'))
			return removed
		except:
			return False

	def _notify_blocked_file(self, context, filename=None, error=None):
		try:
			blocked_value = filename or _blocked_file_value(vars(self)) or self._safe_source_name()
			message = '%s : fichier exécutable/dangereux détecté, lien ignoré' % context
			if error: message = '%s | erreur=%s' % (message, error)
			self._log_source_event('Sécurité sources', message, blocked_value)
		except:
			pass
		try:
			notification('Fichier exécutable détecté - lien ignoré', 4000)
		except:
			pass

	def __init__(self, source_dict, meta=None):
		self.direct_debrid_link = False
		self.scrape_provider, self.url = '', ''
		for k, v in source_dict.items(): setattr(self, k, v)
		self.meta = meta or {}

	def _custom_direct_info_hash(self):
		try:
			import re
			for value in (getattr(self, 'hash', None), getattr(self, 'url', None)):
				if not value: continue
				value = str(value)
				for pattern in (r'btih:([A-Fa-f0-9]{40})', r'/alldebrid/([A-Fa-f0-9]{40})(?:/|$)', r'\b([A-Fa-f0-9]{40})\b'):
					match = re.search(pattern, value, re.I)
					if match: return match.group(1).lower()
		except:
			pass
		return None

	# Nettoie le cloud AllDebrid pour les liens directs issus d'une source custom.
	def _cleanup_custom_direct_alldebrid(self):
		try:
			if self.debrid != 'alldebrid': return
			if settings.store_resolved_torrent_to_cloud(self.debrid): return
			info_hash = self._custom_direct_info_hash()
			if not info_hash:
				kodi_utils.logger('AllDebrid cloud cleanup', 'Nettoyage annulé : hash introuvable | source=%s | fichier=%s' % (getattr(self, 'provider', 'N/A'), getattr(self, 'name', 'N/A')))
				return
			api = alldebrid_api.AllDebridAPI()
			# Le proxy Stremio/Custom peut créer le magnet quelques secondes après que Kodi a reçu l'URL.
			# Nettoyage volontairement poli avec l'API : attente initiale, puis quelques essais espacés.
			kodi_utils.sleep(6000)
			deleted = api.delete_torrent_by_hash(
				info_hash,
				getattr(self, 'name', None),
				retry_delays_ms=(3000, 5000, 7000, 9000)
			)
			if deleted:
				try: api.clear_cache()
				except: pass
		except Exception as e:
			kodi_utils.logger('Exception dans _cleanup_custom_direct_alldebrid', f'{e}\n{self.dumps()}')

	# Résout une source selon son origine : scraper externe, cache interne, hoster ou lien direct.
	def resolve_sources(self):
		try:
			self._log_source_event('Lecture source', 'Tentative de résolution')
			if _is_blocked_file_source(vars(self)):
				self._notify_blocked_file('Blocage avant résolution')
				return None

			if is_external_source(self):
				if getattr(self, 'pack_reuse', False):
					api = import_debrid(self.debrid)
					if api:
						api.store_to_cloud = settings.store_resolved_torrent_to_cloud(self.debrid)
						resolved_url = pack_reuse.resolve_reuse_source(self, api)
						if resolved_url: self._mark_debrid_hash_cached('réutilisation pack')
						return resolved_url
				direct_url = direct_external_url(self)
				if direct_url:
					if direct_external_needs_cleanup(self):
						Thread(target=self._cleanup_custom_direct_alldebrid).start()
					return direct_url

				title, season, episode = media_resolution_context(self.meta, metadata.get_title)
				api = import_debrid(self.debrid)
				api.store_to_cloud = settings.store_resolved_torrent_to_cloud(self.debrid)
				if is_source_magnet(self):
					return self.resolve_external_sources(api, title, season, episode)
				if is_hoster_source(self, default_hosters_providers):
					return api.unrestrict_link(self.url)
				store_to_cloud = settings.store_resolved_usenet_to_cloud(self.debrid)
				return api.resolve_nzb(self.url, self.hash, store_to_cloud, title, season, episode)

			if is_internal_source(self, default_internal_scrapers):
				return self.resolve_internal_sources(self.direct_debrid_link)
			if is_hoster_source(self, default_hosters_providers):
				return import_debrid(self.debrid).unrestrict_link(self.url)
			return self.url
		except Exception as e:
			self._log_source_event('Lecture source', 'Erreur silencieuse interceptée dans resolve_sources | erreur=%s' % e)

	# Analyse un pack magnet/NZB externe et sélectionne le fichier vidéo le plus pertinent.
	def resolve_external_sources(self, api, title, season, episode):
		from modules.source_utils import supported_video_extensions, seas_ep_filter, extras_filter
		files, torrent_id = [], None
		try:
			extensions = supported_video_extensions()
			extras_filtering_list = tuple(i for i in extras_filter() if not i in title.lower())
			if self.debrid in ('real-debrid', 'alldebrid'): args = self.url, self.hash, True
			else: args = self.url, self.hash
			self._log_source_event('Lecture source', 'Analyse du pack magnet via %s' % self.debrid)
			files = api.parse_magnet_pack(*args) or []
			self._log_source_event('Lecture source', 'Pack magnet analysé | fichiers=%s' % len(files))
			blocked_item = next((i for i in files if _is_blocked_file_source(i)), None)
			if blocked_item:
				torrent_id = blocked_item.get('torrent_id') or torrent_id
				self._notify_blocked_file('Blocage après analyse du pack', _blocked_file_value(blocked_item) or blocked_item.get('filename'))
				try:
					if torrent_id and self.debrid in ('real-debrid', 'alldebrid', 'torbox'):
						Thread(target=api.delete_torrent, args=(torrent_id,)).start()
				except:
					pass
				return None
			selected_files = []
			selected_files_append = selected_files.append
			for i in files or selected_files:
				if _is_blocked_file_source(i): continue
				torrent_id, filename = i.get('torrent_id'), i['filename'].lower()
				# Les fichiers .m2ts sont ignorés : ils posent souvent problème en lecture directe.
				if filename.endswith('.m2ts'): raise Exception('Vérification .m2ts échouée')
				if not filename.endswith(tuple(extensions)): continue
				if season and not seas_ep_filter(season, episode, filename): continue
				elif any(x in filename for x in extras_filtering_list): continue
				selected_files_append(i)
			if not selected_files: raise Exception('Aucun fichier vidéo compatible sélectionné')
			# Pour les films, le plus gros fichier est généralement le meilleur candidat.
			if not season: selected_files.sort(key=lambda k: k['size'], reverse=True)
			file_key = next((i['link'] for i in selected_files), None)
			file_url = api.unrestrict_link(file_key)
			if file_url:
				self._mark_debrid_hash_cached('résolution magnet')
				try: pack_reuse.store_pack_from_source(vars(self), self.meta, files, selected_files[0] if selected_files else None, getattr(api, 'store_to_cloud', None))
				except Exception: pass
			if self.debrid in ('real-debrid', 'alldebrid', 'torbox'):
				if not api.store_to_cloud: Thread(target=api.delete_torrent, args=(torrent_id,)).start()
			return file_url
		except Exception as e:
			error_text = str(e or '')
			error_text_lower = error_text.lower()
			self._invalidate_tracker_hash_if_needed(error_text, 'analyse magnet')
			if 'blocked executable' in error_text_lower or 'exécutable' in error_text_lower:
				self._notify_blocked_file('Blocage pendant analyse du pack', error=error_text)
			elif self.debrid == 'alldebrid' and (
				'allDebrid'.lower() in error_text_lower
				or 'magnets' in error_text_lower
				or 'magnet non cache' in error_text_lower
			):
				self._log_source_event('Lecture source', 'Lien non résolu par AllDebrid | raison=%s' % (error_text or 'réponse API inattendue'))
			else:
				kodi_utils.logger('Exception dans resolve_external_sources', f"{e}\n{self.dumps()}")
			try:
				if files and torrent_id: Thread(target=api.delete_torrent, args=(torrent_id,)).start()
			except:
				pass

	# Résout les éléments déjà présents dans le cloud débrid ou dans les dossiers locaux.
	def resolve_internal_sources(self, direct_debrid_link=False):
		try:
			if self.scrape_provider == 'tb_cloud':
				if direct_debrid_link == 'usenet': function = 'unrestrict_usenet'
				elif direct_debrid_link == 'webdl': function = 'unrestrict_webdl'
				else: function = 'unrestrict_link'
				function = getattr(torbox_api.TorBoxAPI(), function)
				url = function(self.id)
			elif self.scrape_provider == 'rd_cloud':
				if direct_debrid_link: url = self.url_dl
				else: url = real_debrid_api.RealDebridAPI().unrestrict_link(self.id)
			elif self.scrape_provider == 'ad_cloud':
				api = alldebrid_api.AllDebridAPI()
				raw_url = alldebrid_api.AllDebridAPI.normalize_download_link(self.url_dl or self.id)
				# Les liens sauvegardés AllDebrid (/user/links) ressemblent à des URLs,
				# mais ce sont parfois des pages /f/... et non des flux vidéo. On les
				# déverrouille donc avant de les donner au lecteur Kodi.
				if getattr(self, 'ad_requires_unlock', False) or alldebrid_api.AllDebridAPI.is_alldebrid_file_link(raw_url):
					url = api.unrestrict_link(raw_url)
				elif direct_debrid_link:
					url = raw_url
				else:
					url = api.unrestrict_link(self.id) or raw_url
				url = alldebrid_api.AllDebridAPI.normalize_download_link(url)
				if not url or not alldebrid_api.AllDebridAPI.is_probably_playable_link(url):
					kodi_utils.logger('Lecture source', 'Lien AD cloud non lisible après résolution | section=%s | entrée=%s | sortie=%s' % (getattr(self, 'ad_cloud_section', 'N/A'), raw_url or 'N/A', url or 'N/A'))
					try: notification('Lien AllDebrid indisponible ou expiré', 5000)
					except: pass
					return None
			elif self.scrape_provider == 'easynews':
				from menus.easynews import resolve_easynews
				url = resolve_easynews({'url_dl': self.url_dl, 'play': 'false'})
				if not direct_debrid_link: url += '|seekable=0'
			elif self.scrape_provider == 'folders':
				if self.url_dl.endswith('.strm'):
					with kodi_utils.open_file(self.url_dl) as f: url = f.read()
				else: url = self.url_dl
			else: url = self.url_dl
			return url
		except Exception as e:
			kodi_utils.logger('Exception dans resolve_internal_sources', f"{e}\n{self.dumps()}")

	# Affiche le contenu d'un pack pour permettre à l'utilisateur de choisir un fichier précis.
	def browse_packs(self, highlight=None, download=False):
		api, pack_choices = None, []
		try:
			if not is_magnet_url(getattr(self, 'url', '')):
				return None if download else 'cancel'
			if not getattr(self, 'hash', None):
				return None if download else 'cancel'
			api = import_debrid(self.debrid)
			if not api or not hasattr(api, 'parse_magnet_pack'):
				return None if download else 'cancel'
			show_busy_dialog()
			try:
				pack_choices = api.parse_magnet_pack(self.url, self.hash)
			except Exception as e:
				self._invalidate_tracker_hash_if_needed(str(e or ''), 'parcours pack')
				return None if download else 'cancel'
			blocked_item = next((i for i in (pack_choices or []) if _is_blocked_file_source(i)), None)
			if blocked_item:
				self._notify_blocked_file('Parcours du pack bloqué', _blocked_file_value(blocked_item) or blocked_item.get('filename'))
				self._invalidate_tracker_hash_if_needed('blocked executable file in magnet pack', 'parcours pack')
				return None if download else 'cancel'
		finally:
			hide_busy_dialog()
		if not pack_choices:
			return None if download else 'cancel'
		pack_choices.sort(key=lambda k: k.get('filename', '').lower())
		for item in pack_choices:
			size = float(item.get('size', 0) or 0)
			item.update({
				'icon': self.meta.get('poster') or getattr(api, 'icon', self.meta.get('poster', '')),
				'line1': clean_file_name(item.get('filename', 'N/A')),
				'line2': '%s: %.2f GB' % (ls(32584), size/1073741824)
			})
		if download: return pack_choices
		kwargs = {'enumerate': 'true', 'multi_line': 'true'}
		kwargs.update({'items': json.dumps(pack_choices), 'heading': self.name, 'highlight': highlight})
		chosen_result = select_dialog(pack_choices, **kwargs)
		if chosen_result is None: return 'cancel'
		url_dl = chosen_result.get('link')
		if not url_dl: return 'cancel'
		file_url = api.unrestrict_link(url_dl)
		if file_url:
			self._mark_debrid_hash_cached('parcours pack')
			try: pack_reuse.store_pack_from_source(vars(self), self.meta, pack_choices, chosen_result, settings.store_resolved_torrent_to_cloud(self.debrid))
			except Exception: pass
		return file_url

	# Vérifie manuellement si un magnet non contrôlé est déjà en cache chez le service débrid.
	def unchecked_magnet_status(self):
		show_busy_dialog()
		api = import_debrid(self.debrid)
		result = api.parse_magnet_pack(self.url, self.hash)
		hide_busy_dialog()
		if not result: return ok_dialog(text='Non mis en cache par [B]%s[/B]' % self.debrid.upper(), top_space=True)
		torrent_id = next((i['torrent_id'] for i in result if 'torrent_id' in i), None)
		if torrent_id: Thread(target=api.delete_torrent, args=(torrent_id,)).start()
		ok_dialog(text='Mis en cache par [B]%s[/B]' % self.debrid.upper(), top_space=True)

	# Ajoute un NZB au cloud, surveille sa progression, puis retourne le lien lisible.
	def nzb_cache_and_play(self):
		line, status_str = '%s[CR]%s[CR]STATUT : %s', '[B]%s[/B] (%2d%%)'
		title, season, episode = self.meta['title'], self.meta['season'], self.meta['episode']
		if season and episode: line1 = '%s (%02dx%02d)' % (title, season, episode)
		else: line1 = '%s (%s)' % (title, self.meta['year'])
		kodi_utils.progressDialog.create('alkoFlix', '')
		kodi_utils.progressDialog.update(0, line % (line1, '', '[B]RÉCUPÉRATION...[/B]'))
		try:
			store_to_cloud = get_setting('store_usenet.torbox') == 'true'
			api = import_debrid(self.debrid)
			api.clear_cache()
			nzb_id = api.create_transfer(self.url, self.name)
			if not nzb_id: return kodi_utils.notification(32574)
			resolved_link = None
			data = {'files': []}
			while not data['files']:
				if kodi_utils.progressDialog.iscanceled(): return kodi_utils.notification(32736)
				line2 = 'Temps restant : %s' % data.get('eta', 'N/D')
				progress = int(float(data.get('progress', '0')) * 100)
				status = status_str % (data.get('download_state', '...').upper(), progress)
				kodi_utils.progressDialog.update(progress, line % (line1, line2, status))
				kodi_utils.sleep(500)
				result = api.nzb_info(nzb_id)
				if result and 'id' in result: data = result
			else: resolved_link = api.resolve_nzb(
				self.url, self.hash, store_to_cloud, title, season, episode, nzb_info=result
			)
		finally: kodi_utils.progressDialog.close()
		return kodi_utils.notification(32574) if not resolved_link else resolved_link

	# Ajout manuel d'un NZB au cloud débrid, avec option spéciale Cache/Lecture pour TorBox.
	def manual_add_nzb_to_cloud(self):
		if self.debrid in ('torbox',) and self.meta:
			args = 'alkoFlix', '[CR]%s' % ls(32831) % self.debrid.upper()
			choice = kodi_utils.dialog.yesnocustom(*args, customlabel='Mettre en cache / Lire')
		else: choice = confirm_dialog(text=ls(32831) % self.debrid.upper(), top_space=True)
		if choice == 2: return self.nzb_cache_and_play()
		if choice in (-1, 0, False): return
		show_busy_dialog()
		api = import_debrid(self.debrid)
		api.clear_cache()
		result = api.create_transfer(self.url, self.name)
		hide_busy_dialog()
		if result: notification(32576)
		else: notification(32575)

	# Ajout manuel d'un magnet au cloud débrid.
	def manual_add_magnet_to_cloud(self):
		if _is_blocked_file_source(vars(self)):
			self._notify_blocked_file('Ajout manuel magnet bloqué')
			return None
		if not confirm_dialog(text=ls(32831) % self.debrid.upper(), top_space=True): return
		show_busy_dialog()
		api = import_debrid(self.debrid)
		api.clear_cache()
		result = api.create_transfer(self.url)
		hide_busy_dialog()
		if result: notification(32576)
		else: notification(32575)

# Vérifie et mémorise l'état de cache des hashes auprès des services débrid.
class DebridCheck:
	def __init__(self, meta, name):
		self.cached_list = []
		self.name, self.debrid, self.function = self._debrid_dict[name]
		self.imdb, self.season, self.episode = meta.get('imdb_id'), meta.get('season'), meta.get('episode')

	# Utilise d'abord le cache local, puis interroge les services seulement pour les hashes inconnus.
	def cache_check(self):
		try:
			self.cached_list.extend(i[0] for i in self.cached_hashes if i[1] == self.debrid and i[2] == 'True')
			unchecked_filter = {h[0] for h in self.cached_hashes if h[1] == self.debrid}
			unchecked_hashes = [i for i in self.hash_list if not i in unchecked_filter]
			if not unchecked_hashes: return
			if self.debrid in ('rd', 'ad'): checked_hashes = self.external_check_cache(unchecked_hashes)
			else: checked_hashes = self.function().check_cache(unchecked_hashes)
			if not checked_hashes: return
			hashes_to_cache = []
			process_append = hashes_to_cache.append
			cached_append = self.cached_list.append
			try:
				for h in unchecked_hashes:
					if h in checked_hashes:
						cached_append(h)
						cached = 'True'
					else: cached = 'False'
					process_append((h, cached))
			except:
				for i in unchecked_hashes: process_append((i, 'False'))
			if hashes_to_cache: Thread(target=self.cache_write, args=(hashes_to_cache,)).start()
		finally: return self.cached_list

	# Pour Real-Debrid/AllDebrid, complète la vérification avec des sources externes spécialisées.
	def external_check_cache(self, unchecked_hashes):
		checked_hashes = []
		if self.debrid == 'ad': threads = (
			Thread(target=aio_check_cache, args=(self.imdb, self.season, self.episode, checked_hashes)),
		)
		else: threads = (
			Thread(target=tio_check_cache, args=(self.imdb, self.season, self.episode, checked_hashes)),
			Thread(target=dmm_check_cache, args=(unchecked_hashes, self.imdb, checked_hashes))
		)
		for i in threads: i.start()
		for i in threads: i.join()
		return list(set(checked_hashes))

	def cache_write(self, hashes):
		DebridCache().set_many(hashes, self.debrid)

	@classmethod
	def set_cached_hashes(cls, hash_list):
		cls.hash_list = hash_list
		cls.cached_hashes = DebridCache().get_many(hash_list) or []

	_debrid_dict = {i[0]: i for i in debrid_list}
	hash_list, cached_hashes = [], []

import re, random, requests
from alko.client import randomagent

# Session HTTP réutilisée pour les vérifications externes de cache.
session = requests.session()
session.headers.update({'User-Agent': randomagent(), 'Accept': 'application/json'})

# Vérifie le cache via AIOStreams et ajoute les infoHash trouvés au collecteur partagé.
def aio_check_cache(imdb, season, episode, collector):
	if not imdb: return
	# Les valeurs 'series' et 'movie' sont imposées par l'API appelée : ne pas traduire.
	if str(season).isdigit(): params = {'type': 'series', 'id': '%s:%s:%s' % (imdb, season, episode)}
	else: params = {'type': 'movie', 'id': '%s' % imdb}
	headers, url = {'x-aiostreams-user-data': (
		'ewogICJzZXJ2aWNlcyI6IFsKICAgIHsKICAgICAgImlkIjogImFsbGRlYnJpZCIsCiAgICAgICJlbmFi'
		'bGVkIjogdHJ1ZSwKICAgICAgImNyZWRlbnRpYWxzIjogeyJhcGlLZXkiOiAic3RhdGljRGVtb0FwaWtl'
		'eVByZW0ifQogICAgfQogIF0sCiAgInByZXNldHMiOiBbCiAgICB7CiAgICAgICJ0eXBlIjogIm1lZGlh'
		'ZnVzaW9uIiwKICAgICAgImluc3RhbmNlSWQiOiAiNWI4IiwKICAgICAgImVuYWJsZWQiOiB0cnVlLAog'
		'ICAgICAib3B0aW9ucyI6IHsKICAgICAgICAibmFtZSI6ICJNZWRpYUZ1c2lvbiIsCiAgICAgICAgInRp'
		'bWVvdXQiOiA2NTAwLAogICAgICAgICJyZXNvdXJjZXMiOiBbInN0cmVhbSJdLAogICAgICAgICJ1c2VD'
		'YWNoZWRSZXN1bHRzT25seSI6IHRydWUsCiAgICAgICAgImVuYWJsZVdhdGNobGlzdENhdGFsb2dzIjog'
		'ZmFsc2UsCiAgICAgICAgImRvd25sb2FkVmlhQnJvd3NlciI6IGZhbHNlLAogICAgICAgICJjb250cmli'
		'dXRvclN0cmVhbXMiOiBmYWxzZSwKICAgICAgICAiY2VydGlmaWNhdGlvbkxldmVsc0ZpbHRlciI6IFtd'
		'LAogICAgICAgICJudWRpdHlGaWx0ZXIiOiBbXSwKICAgICAgICAibWVkaWFUeXBlcyI6IFtdCiAgICAg'
		'IH0KICAgIH0sCiAgICB7CiAgICAgICJ0eXBlIjogInN0cmVtdGhydVRvcnoiLAogICAgICAiaW5zdGFu'
		'Y2VJZCI6ICI1NDgiLAogICAgICAiZW5hYmxlZCI6IHRydWUsCiAgICAgICJvcHRpb25zIjogewogICAg'
		'ICAgICJuYW1lIjogIlN0cmVtVGhydSBUb3J6IiwKICAgICAgICAidGltZW91dCI6IDY1MDAsCiAgICAg'
		'ICAgInJlc291cmNlcyI6IFsic3RyZWFtIl0sCiAgICAgICAgIm1lZGlhVHlwZXMiOiBbXSwKICAgICAg'
		'ICAiaW5jbHVkZVAyUCI6IGZhbHNlLAogICAgICAgICJ1c2VNdWx0aXBsZUluc3RhbmNlcyI6IGZhbHNl'
		'CiAgICAgIH0KICAgIH0KICBdLAogICJmb3JtYXR0ZXIiOiB7CiAgICAiaWQiOiAidG9ycmVudGlvIiwK'
		'ICAgICJkZWZpbml0aW9uIjogewogICAgICAibmFtZSI6ICIiLAogICAgICAiZGVzY3JpcHRpb24iOiAi'
		'IgogICAgfQogIH0sCiAgInNvcnRDcml0ZXJpYSI6IHsKICAgICJnbG9iYWwiOiBbXQogIH0sCiAgImRl'
		'ZHVwbGljYXRvciI6IHsKICAgICJlbmFibGVkIjogZmFsc2UsCiAgICAia2V5cyI6IFsiaW5mb0hhc2gi'
		'XSwKICAgICJtdWx0aUdyb3VwQmVoYXZpb3VyIjogImFnZ3Jlc3NpdmUiLAogICAgImNhY2hlZCI6ICJz'
		'aW5nbGVfcmVzdWx0IiwKICAgICJ1bmNhY2hlZCI6ICJwZXJfc2VydmljZSIsCiAgICAicDJwIjogInNp'
		'bmdsZV9yZXN1bHQiLAogICAgImV4Y2x1ZGVBZGRvbnMiOiBbXQogIH0sCiAgImV4Y2x1ZGVVbmNhY2hl'
		'ZCI6IHRydWUKfQ=='
	)}, 'https://aiostreamsfortheweak.nhyira.dev/api/v1/search'
	try:
		results = session.get(url, params=params, headers=headers, timeout=7.05)
		if not results.ok: return
		if 'json' not in results.headers.get('Content-Type', '').lower(): return
		payload = results.json()
		files = ((payload.get('data') or {}).get('results') or [])
		collector.extend(file['infoHash'] for file in files if isinstance(file, dict) and file.get('infoHash'))
	except ValueError:
		return
	except Exception as e: kodi_utils.logger('Erreur cache AIO', str(e))

# Vérifie le cache via Torrentio et extrait les hashes présents dans les URLs retournées.
def tio_check_cache(imdb, season, episode, collector):
	# Les segments 'series' et 'movie' font partie du format d'URL Torrentio : ne pas traduire.
	if str(season).isdigit(): url = 'series/%s:%s:%s.json' % (imdb, season, episode)
	else: url = 'movie/%s.json' % (imdb)
	params = 'realdebrid=T2iZoymNCCD1T5c2sX5u8tIZVcgcFWlCsCJ72rCmrU2mDdmvgieM'
	url = 'https://torrentio.strem.fun/debridoptions=nodownloadlinks,nocatalog|%s/stream/%s' % (params, url)
	pattern = re.compile(r'\b\w{40}\b')
	try:
		results = session.get(url, timeout=7.05)
		files = results.json()['streams']
		collector.extend(pattern.findall(file['url'])[-1] for file in files if '+' in file['name'] and 'url' in file)
	except Exception as e: kodi_utils.logger('Erreur Torrentio', str(e))

# Vérifie la disponibilité des hashes via Debrid Media Manager.
def dmm_check_cache(unchecked_hashes_chunk, imdb, collector):
	# L'API DMM autorise un maximum de 100 hachages par requête.
	# Ne pas enchaîner plusieurs appels : toute utilisation abusive peut entraîner sa désactivation.
	# Un échantillon de 100 éléments est amplement suffisant.
	from magneto.dmm import get_secret
	unchecked_hashes_chunk = [i for i in unchecked_hashes_chunk if len(i) == 40]
	if len(unchecked_hashes_chunk) > 100: unchecked_hashes_chunk = random.sample(unchecked_hashes_chunk, 100)
	url = 'https://debridmediamanager.com/api/availability/check'
	dmmProblemKey, solution = get_secret()
	data = {'dmmProblemKey': dmmProblemKey, 'solution': solution, 'imdbId': imdb, 'hashes': unchecked_hashes_chunk}
	try:
		results = session.post(url, json=data, timeout=7.05)
		files = results.json()['available']
		collector.extend(file['hash'] for file in files if 'hash' in file)
	except Exception as e: kodi_utils.logger('Erreur DMM', str(e))
