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

import json
from threading import Thread
from caches import watched_cache as ws
from windows import open_window
from modules import kodi_utils, settings
from modules.utils import sec2time, make_title_slug
# from modules.kodi_utils import logger  # Décommenter temporairement pour diagnostiquer la lecture.

KODI_VERSION, make_cast_list = kodi_utils.get_kodi_version(), kodi_utils.make_cast_list
ls, get_setting = kodi_utils.local_string, kodi_utils.get_setting
fanart_empty = kodi_utils.media_path('transparent.png')
poster_empty = kodi_utils.media_path('box_office.png')

def _player_log(message):
	try: kodi_utils.logger('alkoFlix Player', message)
	except: pass

class AlkoFlixPlayer(kodi_utils.xbmc_player):
	def __init__(self):
		kodi_utils.xbmc_player.__init__(self)
		self.set_resume = 5
		self.set_watched_movie = 90
		self.set_watched_episode = 90
		self.set_watched = self.set_watched_episode
		self.playback_event, self.progress_media = None, None
		self.playback_started = False
		self.media_marked, self.nextep_info_gathered = False, False
		self.subs_searched, self.stingers_checked = False, False
		self.nextep_started, self.play_random_continual = False, False
		self.trakt_scrobble_started, self.trakt_scrobble_stopped = False, False
		self.trakt_initial_progress = 0.1
		self.total_time, self.curr_time, self.current_point = 0.0, 0.0, 0.0
		self._last_total_time, self._last_curr_time, self._last_current_point = 0.0, 0.0, 0.0
		self._playback_finalized = False
		self.autoplay_next_episode = False
		self.autoplay_nextep = settings.autoplay_next_episode()
		self.autoscrape_next_episode = False
		self.autoscrape_nextep = settings.autoscrape_next_episode()
		self.stinger_enabled = get_setting('stingers.enable') == 'true'
		self.stinger_check = int(get_setting('stingers.threshold', '30'))
		self.volume_check = get_setting('volumecheck.enabled', 'false') == 'true'

	def run(self, url=None, meta=None, progress_media=None):
		if not url: return False
		try:
			self.meta = meta or {}
			self.meta_get = self.meta.get
			self.tmdb_id, self.imdb_id, self.tvdb_id = self.meta_get('tmdb_id'), self.meta_get('imdb_id'), self.meta_get('tvdb_id')
			self.media_type, self.title, self.year = self.meta_get('media_type'), self.meta_get('title'), self.meta_get('year')
			self.set_watched = self._watched_threshold()
			self.season, self.episode = self.meta_get('season', ''), self.meta_get('episode', '')
			self.media_marked, self.nextep_info_gathered = False, False
			self.subs_searched, self.stingers_checked = False, False
			self.nextep_started, self.play_random_continual = False, False
			self.trakt_scrobble_started, self.trakt_scrobble_stopped = False, False
			self.trakt_initial_progress = 0.1
			self.total_time, self.curr_time, self.current_point = 0.0, 0.0, 0.0
			self._last_total_time, self._last_curr_time, self._last_current_point = 0.0, 0.0, 0.0
			self._playback_finalized = False
			self.nextep_handoff_key = '%s:%s:%s' % (self.tmdb_id, self.season, self.episode)
			if any(i in self.meta for i in ('random', 'random_continual')): bookmark = 0
			else: bookmark = self.bookmarkAlkoFlix()
			if bookmark == 'cancel': return None
			self.trakt_initial_progress = self._safe_initial_trakt_progress(bookmark)
			self.meta.update({'url': url, 'bookmark': bookmark})
			listitem = self._make_listitem()
			listitem.setProperty('StartPercent', str(bookmark))
			try:
				trakt_ids = {'tmdb': self.tmdb_id, 'imdb': self.imdb_id, 'slug': make_title_slug(self.title)}
				if self.media_type == 'episode':
					trakt_ids['tvdb'] = self.tvdb_id
					trakt_ids['season'] = self.season
					trakt_ids['episode'] = self.episode
				kodi_utils.clear_property('script.trakt.ids')
				kodi_utils.set_property('script.trakt.ids', json.dumps(trakt_ids))
			except: pass
			self.playback_event = False
			self.playback_started = False
			self.play(url, listitem)
			_player_log('Playback requested -> media_type=%s | title=%s | imdb_id=%s | subtitles_action=%s' % (self.media_type, self.title, self.imdb_id, get_setting('subtitles.subs_action', '2')))
			Thread(target=self.run_subtitles_delayed, args=(1500, 12000)).start()

			if self.media_type == 'episode':
				self.play_random_continual = 'random_continual' in self.meta
				if not self.play_random_continual and self.autoplay_nextep: self.autoplay_next_episode = 'random' not in self.meta
				if not self.play_random_continual and self.autoscrape_nextep: self.autoscrape_next_episode = 'random' not in self.meta
				if not self.play_random_continual and self.autoplay_nextep and self.autoscrape_nextep: self.autoscrape_next_episode = False
			waited_ms = 0
			while not self.playback_event and waited_ms < 15000:
				kodi_utils.sleep(100)
				waited_ms += 100
			# Kodi peut déclencher onPlayBackStopped très vite sur une URL invalide
			# sans jamais atteindre onAVStarted. Dans ce cas, la lecture ne doit pas
			# être considérée comme réussie : on renvoie False afin que play_file()
			# tente le lien suivant disponible.
			if not self.playback_started:
				try: kodi_utils.hide_busy_dialog()
				except: pass
				try: kodi_utils.close_all_dialog()
				except: pass
				reason = 'arrêt avant démarrage vidéo' if self.playback_event == 'stop' else 'aucun flux vidéo actif'
				_player_log('Lecture abandonnée -> %s après %s ms | title=%s' % (reason, waited_ms, self.title))
				return False
			if callable(progress_media): progress_media()
			kodi_utils.close_all_dialog()
			if self.volume_check: kodi_utils.volume_checker(get_setting('volumecheck.percent', '100'))
			kodi_utils.sleep(1000)
			while self.isPlayingVideo():
				try:
					if kodi_utils.get_property('alkoflix_nextep_handoff') == self.nextep_handoff_key:
						_player_log('Transition Up Next détectée -> finalisation/scrobble de l’épisode courant')
						self._capture_playback_position('upnext_handoff')
						self._trakt_scrobble('stop_progress', force=True, wait=True)
						self._finalize_playback_progress('upnext_handoff')
						self._refresh_external_trakt_ratings('upnext_handoff')
						kodi_utils.clear_property('alkoflix_nextep_handoff')
						break
					kodi_utils.sleep(1000)
					if not self._capture_playback_position('playback_loop'):
						continue
					if self.curr_time > self.stinger_check and not self.stingers_checked:
						self.run_stingers()
					if self.current_point >= self.set_watched and not self.media_marked:
						self.media_watched_marker()
					if self.play_random_continual:
						if not self.nextep_info_gathered: self.info_next_ep()
						self.remaining_time = round(self.total_time - self.curr_time)
						if self.remaining_time <= self.start_prep:
							if not self.nextep_started: self.run_random_continual()
					if self.autoplay_next_episode:
						if not self.nextep_info_gathered: self.info_next_ep()
						self.remaining_time = round(self.total_time - self.curr_time)
						if self.remaining_time <= self.start_prep:
							if not self.nextep_started and self.autoplay_nextep: self.run_next_ep()
					if self.autoscrape_next_episode:
						if not self.nextep_info_gathered: self.info_next_ep()
						self.remaining_time = round(self.total_time - self.curr_time)
						if self.remaining_time <= self.autoscrape_next_window_time:
							if not self.nextep_started and self.autoscrape_nextep: self.run_scrape_next_ep()
				except: pass
				if not self.subs_searched: self.run_subtitles()
			# Filet de sécurité : certains arrêts rapides ne déclenchent pas
			# toujours onPlayBackStopped/onPlayBackEnded côté Kodi. Si un start
			# Trakt a été envoyé, on force donc l'envoi du stop quand la boucle
			# de lecture se termine afin d'éviter un Watching Now fantôme.
			if self.trakt_scrobble_started and not self.trakt_scrobble_stopped:
				self._trakt_scrobble('stop_progress', wait=True)
			self._finalize_playback_progress('playback_loop_end')
			self._refresh_external_trakt_ratings('playback_loop_end')
			ws.clear_local_bookmarks()
			return True
		except Exception as e:
			try: _player_log('Erreur inattendue pendant la lecture -> %s: %s' % (type(e).__name__, e))
			except: pass
			return False

	def _watched_threshold(self):
		try:
			return self.set_watched_movie if self.media_type in ('movie', 'movies') else self.set_watched_episode
		except:
			return 90

	def _capture_playback_position(self, reason=''):
		try:
			# Kodi écrit une erreur EXCEPTION dans le log si getTime()/getTotalTime()
			# est appelé après la fermeture du player. On vérifie donc l'état
			# avant toute lecture de position, puis on laisse la finalisation
			# reprendre la dernière position fiable déjà mémorisée.
			if not self.isPlayingVideo():
				return False
		except:
			return False
		try:
			total_time = float(self.getTotalTime())
			curr_time = float(self.getTime())
			if total_time <= 0: return False
			current_point = round(float(curr_time / total_time * 100), 1)
			self.total_time, self.curr_time, self.current_point = total_time, curr_time, current_point
			self._last_total_time, self._last_curr_time, self._last_current_point = total_time, curr_time, current_point
			return True
		except Exception as e:
			if reason not in ('playback_loop', 'playback_loop_end', 'onPlayBackEnded', 'onPlayBackStopped'):
				_player_log('Capture position impossible -> reason=%s | %s: %s' % (reason, type(e).__name__, e))
			return False

	def _restore_last_playback_position(self):
		try:
			if float(self._last_total_time) <= 0: return False
			self.total_time = self._last_total_time
			self.curr_time = self._last_curr_time
			self.current_point = self._last_current_point
			return True
		except: return False

	def _finalize_playback_progress(self, reason=''):
		try:
			if self._playback_finalized or self.media_marked: return
			if not self._capture_playback_position(reason):
				self._restore_last_playback_position()
			if not float(getattr(self, 'total_time', 0) or 0):
				_player_log('Finalisation ignorée -> aucune position fiable | reason=%s | title=%s' % (reason, self.title))
				return
			_player_log('Finalisation lecture -> reason=%s | title=%s | progress=%s%% | time=%ss/%ss' % (reason, self.title, self.current_point, round(float(self.curr_time), 1), round(float(self.total_time), 1)))
			self.media_watched_marker()
			if self.media_marked: self._playback_finalized = True
		except Exception as e:
			_player_log('Finalisation lecture impossible -> reason=%s | %s: %s' % (reason, type(e).__name__, e))

	def _make_listitem(self):
		listitem = kodi_utils.make_listitem()
		try:
			duration, plot, genre, trailer = self.meta_get('duration'), self.meta_get('plot'), self.meta_get('genre'), self.meta_get('trailer')
			rating, votes, premiered, studio = self.meta_get('rating'), self.meta_get('votes'), self.meta_get('premiered'), self.meta_get('studio')
			poster_main, poster_backup, fanart_main, fanart_backup = settings.get_art_provider()
			poster = self.meta_get(poster_main) or self.meta_get(poster_backup) or poster_empty
			fanart = self.meta_get(fanart_main) or self.meta_get(fanart_backup) or fanart_empty
			clearlogo = self.meta_get('clearlogo') or self.meta_get('tmdblogo') or ''
			if self.media_type == 'movie':
				if KODI_VERSION < 20:
					listitem.setCast(self.meta_get('cast', []))
					listitem.setUniqueIDs({'imdb': self.imdb_id, 'tmdb': str(self.tmdb_id)})
					listitem.setInfo('video', {'mediatype': 'movie', 'trailer': trailer, 'title': self.title, 'size': '0', 'duration': duration,
						'plot': plot, 'premiered': premiered, 'studio': studio, 'year': self.year, 'genre': genre, 'tagline': self.meta_get('tagline'),
						'imdbnumber': self.imdb_id, 'director': self.meta_get('director'), 'writer': self.meta_get('writer'), 'rating': rating, 'votes': votes})
				else:
					videoinfo = infoTagger(listitem, self.meta)
					videoinfo.setCast(make_cast_list(self.meta_get('cast', [])))
					videoinfo.setUniqueIDs({'imdb': self.imdb_id, 'tmdb': str(self.tmdb_id)})
					videoinfo.setMediaType('movie')
			else:
				if KODI_VERSION < 20:
					listitem.setCast(self.meta_get('cast', []))
					listitem.setUniqueIDs({'imdb': self.imdb_id, 'tmdb': str(self.tmdb_id), 'tvdb': str(self.tvdb_id)})
					listitem.setInfo('video', {'mediatype': 'episode', 'trailer': trailer, 'title': self.meta_get('ep_name'), 'size': '0', 'duration': duration,
						'plot': plot, 'premiered': premiered, 'studio': studio, 'year': self.year, 'genre': genre, 'tvshowtitle': self.title,
						'imdbnumber': self.imdb_id, 'season': self.season, 'episode': self.episode, 'rating': rating, 'votes': votes})
				else:
					videoinfo = infoTagger(listitem, self.meta)
					videoinfo.setCast(make_cast_list(self.meta_get('cast', [])))
					videoinfo.setUniqueIDs({'imdb': self.imdb_id, 'tmdb': str(self.tmdb_id), 'tvdb': str(self.tvdb_id)})
					videoinfo.setMediaType('episode')
			if settings.get_fanart_data():
				banner, clearart, landscape = self.meta_get('banner'), self.meta_get('clearart'), self.meta_get('landscape')
			else: banner, clearart, landscape = '', '', ''
			listitem.setArt({'poster': poster, 'fanart': fanart, 'icon': poster, 'banner': banner, 'clearart': clearart, 'clearlogo': clearlogo, 'landscape': landscape,
							'tvshow.clearart': clearart, 'tvshow.clearlogo': clearlogo, 'tvshow.landscape': landscape, 'tvshow.banner': banner})
		except: pass
		return listitem

	def bookmarkAlkoFlix(self):
		bookmark = 0
		watched_indicators = settings.watched_indicators()
		try: resume_point, curr_time, resume_id = ws.detect_bookmark(ws.get_bookmarks(watched_indicators, self.media_type), self.tmdb_id, self.season, self.episode)
		except: resume_point, curr_time = 0, 0
		resume_check = float(resume_point)
		if resume_check > 0:
			percent = str(resume_point)
			raw_time = float(curr_time)
			if watched_indicators in (1, 2): resume_point = '%s%%' % str(percent)
			else: resume_point = sec2time(raw_time, n_msec=0)
			bookmark = self.getResumeStatus(resume_point, percent, bookmark)
			if bookmark == 0: ws.erase_bookmark(self.media_type, self.tmdb_id, self.season, self.episode)
		return bookmark

	def getResumeStatus(self, resume_point, percent, bookmark):
		if settings.auto_resume(self.media_type): return percent
		try:
			choice = kodi_utils.dialog.contextmenu([ls(32832), ls(32833)])
			if choice == 0: return percent
			if choice == 1: return bookmark
			return 'cancel'
		except:
			return bookmark

	def getStingers(self, tmdb_id, poster):
		if not tmdb_id: return
		from indexers import tmdb_api
		stingers = {'duringcreditsstinger': 'Scène pendant le générique', 'aftercreditsstinger': 'Scène après le générique'}
		keywords = tmdb_api.movie_keywords(tmdb_id) or []
		keywords = [str(i['name']) for i in keywords]
		if all((i in keywords for i in stingers.keys())): stinger = 'Scènes pendant et après le générique'
		else: stinger = next((v for k, v in stingers.items() if k in keywords), None)
		return kodi_utils.notification(stinger, time=6000, icon=poster) if stinger else ''

	def media_watched_marker(self):
		self.media_marked = True
		try:
			if not float(getattr(self, 'total_time', 0) or 0):
				_player_log('Marquage ignoré -> durée inconnue | title=%s' % self.title)
				return
			if self.current_point >= self.set_watched:
				_player_log('Marquage vu demandé -> title=%s | progress=%s%% | threshold=%s%%' % (self.title, self.current_point, self.set_watched))
				if self.media_type == 'movie': watched_function, watched_params = ws.mark_as_watched_unwatched_movie, {
					'mode': 'mark_as_watched_unwatched_movie', 'action': 'mark_as_watched', 'refresh': 'false', 'from_playback': 'true',
					'tmdb_id': self.tmdb_id, 'title': self.title, 'year': self.year
				}
				else: watched_function, watched_params = ws.mark_as_watched_unwatched_episode, {
					'mode': 'mark_as_watched_unwatched_episode', 'action': 'mark_as_watched', 'refresh': 'false', 'from_playback': 'true',
					'tmdb_id': self.tmdb_id, 'title': self.title, 'year': self.year, 'tvdb_id': self.tvdb_id, 'season': self.season, 'episode': self.episode
				}
				Thread(target=self.run_media_watched, args=(watched_function, watched_params)).start()
			else:
				kodi_utils.clear_property('alkoflix_total_autoplays')
				if not self.current_point >= self.set_resume:
					_player_log('Progression ignorée -> sous le seuil de reprise | title=%s | progress=%s%%' % (self.title, self.current_point))
					return
				_player_log('Progression sauvegardée -> title=%s | progress=%s%% | time=%ss/%ss' % (self.title, self.current_point, round(float(self.curr_time), 1), round(float(self.total_time), 1)))
				ws.set_bookmark(self.media_type, self.tmdb_id, self.curr_time, self.total_time, self.title, self.season, self.episode)
		except Exception as e:
			_player_log('Marquage/progression impossible -> %s: %s' % (type(e).__name__, e))

	def run_media_watched(self, function, params):
		try:
			function(params)
			_player_log('Marquage vu terminé -> title=%s | media_type=%s | season=%s | episode=%s' % (self.title, self.media_type, self.season, self.episode))
		except Exception as e:
			_player_log('Marquage vu impossible -> %s: %s' % (type(e).__name__, e))

	def run_scrape_next_ep(self):
		self.nextep_started = True
		try:
			from modules.episode_tools import execute_scrape_nextep
			execute_scrape_nextep(self.meta)
		except: pass

	def run_next_ep(self):
		self.nextep_started = True
		try:
			from modules.episode_tools import execute_nextep
			Thread(target=execute_nextep, args=(self.meta, self.nextep_settings)).start()
		except: pass

	def run_random_continual(self):
		self.nextep_started = True
		try:
			from modules.episode_tools import execute_nextep
			Thread(target=execute_nextep, args=(self.meta, self.nextep_settings)).start()
		except: pass

	def run_subtitles_delayed(self, delay_ms=1500, max_wait_ms=12000):
		try:
			_player_log('Subtitles delayed runner -> waiting for active video playback')
			waited = 0
			while waited < max_wait_ms:
				if self.isPlayingVideo(): break
				kodi_utils.sleep(500)
				waited += 500
			if not self.isPlayingVideo():
				_player_log('Subtitles delayed runner aborted -> no video playback after %d ms' % waited)
				return
			kodi_utils.sleep(delay_ms)
			if self.subs_searched:
				_player_log('Subtitles delayed runner skipped -> subtitles already searched')
				return
			_player_log('Subtitles delayed runner -> calling run_subtitles()')
			self.run_subtitles()
		except Exception as e:
			_player_log('Subtitles delayed runner exception -> %s: %s' % (type(e).__name__, e))

	def run_subtitles(self):
		self.subs_searched = True
		try:
			poster = self.meta.get('poster') or poster_empty
			season = self.season if self.media_type == 'episode' else None
			episode = self.episode if self.media_type == 'episode' else None
			_player_log('run_subtitles called -> title=%s | imdb_id=%s | media_type=%s | season=%s | episode=%s' % (self.title, self.imdb_id, self.media_type, season, episode))
			from indexers.subtitles import Subtitles
			Thread(target=Subtitles().run, args=(self.title, self.imdb_id, season, episode, poster)).start()
			_player_log('run_subtitles -> Subtitles().run thread started')
		except Exception as e:
			_player_log('run_subtitles exception -> %s: %s' % (type(e).__name__, e))

	def run_stingers(self):
		self.stingers_checked = True
		try:
			poster = self.meta.get('poster') or poster_empty
			tmdb_id = self.tmdb_id if self.media_type == 'movie' and self.stinger_enabled else None
			Thread(target=self.getStingers, args=(tmdb_id, poster)).start()
		except: pass

	def _disable_upnext_integration(self, reason=''):
		try:
			kodi_utils.set_setting('autoplay_next_episode', 'false')
			kodi_utils.set_setting('autoscrape_next_episode', 'false')
			kodi_utils.clear_property('alkoflix_settings')
		except: pass
		self.autoplay_nextep = False
		self.autoscrape_nextep = False
		self.autoplay_next_episode = False
		self.autoscrape_next_episode = False
		_player_log('Intégration Up Next désactivée dans alkoFlix -> %s' % (reason or 'aucune raison précisée'))

	def _ensure_upnext_available(self):
		addon_id = 'service.upnext'
		try:
			installed = kodi_utils.addon_installed(addon_id)
			enabled = kodi_utils.addon_enabled(addon_id) if installed else False
			if installed and enabled: return True

			missing_state = 'activée' if installed else 'installée'
			action = 'activer' if installed else 'installer'
			text = (
				'Vous avez activé l’intégration [B]Up Next[/B] dans les réglages d’alkoFlix, '
				'mais l’extension [B]Up Next[/B] n’est pas %s dans Kodi.[CR][CR]'
				'Cette extension est nécessaire pour afficher la fenêtre de l’épisode suivant.[CR][CR]'
				'Désirez-vous %s [B]Up Next[/B] maintenant ?[CR][CR]'
				'Si vous choisissez [B]Non[/B], l’intégration Up Next sera désactivée dans alkoFlix.'
			) % (missing_state, action)

			if not kodi_utils.confirm_dialog(text=text, ok_label='Oui', cancel_label='Non', top_space=True):
				self._disable_upnext_integration('refus utilisateur')
				return False

			if installed:
				kodi_utils.set_addon_enabled(addon_id, True)
			else:
				kodi_utils.execute_builtin('InstallAddon(%s)' % addon_id)

			kodi_utils.sleep(1000)
			installed = kodi_utils.addon_installed(addon_id)
			enabled = kodi_utils.addon_enabled(addon_id) if installed else False
			available = installed and enabled
			if not available:
				_player_log('Up Next toujours indisponible après demande %s' % action)
			return available
		except Exception as e:
			_player_log('Vérification Up Next impossible -> %s: %s' % (type(e).__name__, e))
			return False

	def info_next_ep(self):
		self.nextep_info_gathered = True
		try:
			self.nextep_settings = settings.autoplay_next_settings()
			if not self.nextep_settings['run_popup']:
				window_time = round(0.02 * self.total_time)
				self.nextep_settings['window_time'] = window_time
			elif self.nextep_settings['timer_method'] == 'percentage':
				percentage = self.nextep_settings['window_percentage']
				window_time = round((percentage/100) * self.total_time)
				self.nextep_settings['window_time'] = window_time
			else:
				window_time = self.nextep_settings['window_time']
			threshold_check = window_time + 21
			self.start_prep = self.nextep_settings['scraper_time'] + threshold_check
			self.nextep_settings.update({'threshold_check': threshold_check, 'start_prep': self.start_prep})
			self.autoscrape_next_window_time = self.nextep_settings['autoscrape_next_window_time']
			if self.nextep_settings['run_popup'] and not self.play_random_continual:
				def _send_upnext(autoplay, notification_time):
					try:
						from modules.upnext_integration import send as send_upnext
						upnext_result = send_upnext(self.meta, notification_time, autoplay=autoplay)
						if upnext_result == 'unavailable':
							if not self._ensure_upnext_available(): return False
							upnext_result = send_upnext(self.meta, notification_time, autoplay=autoplay)
						return upnext_result is True
					except: return False

				if self.autoplay_next_episode:
					if _send_upnext(True, window_time):
						self.autoplay_next_episode = False
						self.nextep_started = True
				elif self.autoscrape_next_episode:
					# Mode sélection de source : Up Next affiche la même fenêtre,
					# mais son action ouvre la sélection des liens au lieu de lancer
					# automatiquement le premier résultat.
					if _send_upnext(False, self.autoscrape_next_window_time):
						self.autoscrape_next_episode = False
						self.nextep_started = True
		except: pass


	def _refresh_external_trakt_ratings(self, reason=''):
		try:
			if self.media_type not in ('movie', 'movies', 'episode'): return
			from modules import user_ratings
			user_ratings.refresh_trakt_ratings_after_external_dialog(self.media_type, reason)
		except Exception as e:
			_player_log('Rafraîchissement notes Trakt externe impossible -> reason=%s | %s: %s' % (reason, type(e).__name__, e))

	def _trakt_scrobble_allowed(self):
		try:
			if settings.watched_indicators() != 1:
				return False
			if not settings.trakt_token():
				return False
			if not self.tmdb_id or self.media_type not in ('movie', 'movies', 'episode'):
				return False
			return True
		except:
			return False

	def _safe_initial_trakt_progress(self, value):
		try:
			percent = round(float(value or 0), 1)
		except:
			percent = 0.0
		threshold = self._watched_threshold()
		# Au démarrage d'une lecture, Kodi peut encore retourner la position
		# de l'ancien épisode pendant quelques millisecondes. On évite donc
		# d'envoyer un start Trakt à 90-100 %, ce qui fait disparaître
		# le « Watching Now » après un rafraîchissement du site Trakt.
		if percent < 0 or percent >= threshold: percent = 0.0
		return 0.1 if percent <= 0 else percent

	def _trakt_scrobble_percent(self, action=None, initial_start=False):
		if action == 'start_progress' and initial_start:
			percent = self._safe_initial_trakt_progress(getattr(self, 'trakt_initial_progress', 0.1))
			self.current_point = percent
			return percent
		try:
			if self.isPlayingVideo():
				total_time = float(self.getTotalTime())
				curr_time = float(self.getTime())
				if total_time > 0:
					percent = round(float(curr_time/total_time * 100), 1)
					self.total_time, self.curr_time, self.current_point = total_time, curr_time, percent
					self._last_total_time, self._last_curr_time, self._last_current_point = total_time, curr_time, percent
					return percent
		except: pass
		try:
			if float(self._last_total_time) > 0: return float(self._last_current_point)
		except: pass
		try: return float(self.meta_get('bookmark', 0) or 0)
		except: return 0.0

	def _trakt_scrobble(self, action, force=False, wait=False):
		try:
			if not self._trakt_scrobble_allowed(): return
			initial_start = False
			if action == 'start_progress':
				if self.trakt_scrobble_started and not force: return
				initial_start = not self.trakt_scrobble_started
				self.trakt_scrobble_started = True
				self.trakt_scrobble_stopped = False
			elif action == 'stop_progress':
				if self.trakt_scrobble_stopped and not force: return
				if not self.trakt_scrobble_started and not force: return
				self.trakt_scrobble_stopped = True
			percent = self._trakt_scrobble_percent(action, initial_start)
			# Une pause à moins de 1 % n'a aucune valeur utile pour la reprise.
			# Par contre, Trakt exige au moins 1 % pour accepter un stop.
			# On force donc un minimum technique afin de fermer le Watching Now
			# même lorsque l'utilisateur arrête la lecture très rapidement.
			if action == 'pause_progress' and percent < 1: return
			if action == 'stop_progress' and percent < 1:
				percent = 1.0
			_player_log('Trakt scrobble -> action=%s | title=%s | progress=%s%% | season=%s | episode=%s' % (action, self.title, percent, self.season, self.episode))
			thread = Thread(target=ws.trakt_progress, args=(action, self.media_type, self.tmdb_id, percent, self.season, self.episode))
			thread.start()
			if wait: thread.join(2)
		except Exception as e:
			_player_log('Trakt scrobble impossible -> action=%s | %s: %s' % (action, type(e).__name__, e))

	def onAVStarted(self):
		_player_log('onAVStarted callback -> playback_event=True')
		self.playback_started = True
		self.playback_event = True
		self._trakt_scrobble('start_progress')

	def onPlayBackStarted(self):
		_player_log('onPlayBackStarted callback')
		if not self.subs_searched: Thread(target=self.run_subtitles_delayed, args=(500, 5000)).start()
		try: kodi_utils.hide_busy_dialog()
		except: pass
		try:
			from indexers.subtitles import Subtitles
			Thread(target=Subtitles().select_embedded_subtitle, kwargs={'retries': 1, 'sleep_ms': 2500}).start()
		except: pass

	def onPlayBackPaused(self):
		self._trakt_scrobble('pause_progress')

	def onPlayBackResumed(self):
		self._trakt_scrobble('start_progress', force=True)

	def onPlayBackEnded(self):
		self._capture_playback_position('onPlayBackEnded')
		self._trakt_scrobble('stop_progress')
		self._refresh_external_trakt_ratings('onPlayBackEnded')
		self.playback_event = 'stop'

	def onPlayBackStopped(self):
		# Si ce callback arrive avant onAVStarted, play_file() doit encore pouvoir
		# essayer le lien suivant au lieu de croire que la lecture a réussi.
		self._capture_playback_position('onPlayBackStopped')
		self._trakt_scrobble('stop_progress')
		self._refresh_external_trakt_ratings('onPlayBackStopped')
		self.playback_event = 'stop'

def infoTagger(listitem, meta=None):
	infotag = listitem.getVideoInfoTag(offscreen=True)
	if not meta: return infotag
	for key, val in (
		('country', 'setCountries'), ('director', 'setDirectors'),
		('duration', 'setDuration'), ('genre', 'setGenres'),
		('imdbnumber', 'setIMDBNumber'), ('mediatype', 'setMediaType'),
		('mpaa', 'setMpaa'), ('original_title', 'setOriginalTitle'),
		('playcount', 'setPlaycount'), ('plot', 'setPlot'),
		('premiered', 'setFirstAired' if 'episode' in meta else 'setPremiered'),
		('rating', 'setRating'), ('studio', 'setStudios'),
		('tagline', 'setTagLine'), ('title', 'setTitle'),
		('trailer', 'setTrailer'), ('votes', 'setVotes'),
		('writer', 'setWriters'), ('year', 'setYear'),
		# Champs réservés aux séries et épisodes.
		('air_date', 'setPremiered'), ('aired', 'setFirstAired'),
		('ep_name', 'setTitle'), ('episode', 'setEpisode'), ('season', 'setSeason'),
		('status', 'setTvShowStatus'), ('tvshowtitle', 'setTvShowTitle')
	):
		try:
			if not key in meta or not (arg := meta[key]): continue
			if   key in {'director', 'genre', 'studio', 'writer'}: arg = arg.split(', ')
			elif key in {'episode', 'season', 'year'}: arg = int(arg)
			func = getattr(infotag, val)
			func(arg)
		except: pass
	return infotag
