diff --git a/README.md b/README.md index 113eb4eb..c92ad2a8 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,7 @@ * Insert an _All_ category when missing so you can easily **search your entire catalog**. * Update ***[Mpv](https://mpv.io/)*** and ***[Sfvip Player](https://github.com/K4L4Uz/SFVIP-Player/tree/master)*** so you can enjoy their latest features. +* Translated in all ***Sfvip Player*** languages. * Support an **external EPG**[^1]. [^1]: External EPG doesn't work with **local** m3u accounts. @@ -42,12 +43,12 @@ The logs go **in pairs** (one for each process: ***main*** & ***mitmproxy***) an # Build [![version](https://custom-icon-badges.demolab.com/badge/Build%201.4.12.40-informational?logo=github)](/build_config.py#L27) -[![Sloc](https://custom-icon-badges.demolab.com/badge/Sloc%208.2k-informational?logo=file-code)](https://api.codetabs.com/v1/loc/?github=sebdelsol/sfvip-all) +[![Sloc](https://custom-icon-badges.demolab.com/badge/Sloc%208.3k-informational?logo=file-code)](https://api.codetabs.com/v1/loc/?github=sebdelsol/sfvip-all) [![Ruff](https://custom-icon-badges.demolab.com/badge/Ruff-informational?logo=ruff-color)](https://docs.astral.sh/ruff/) [![Python](https://custom-icon-badges.demolab.com/badge/Python%203.11.8-linen?logo=python-color)](https://www.python.org/downloads/release/python-3118/) [![mitmproxy](https://custom-icon-badges.demolab.com/badge/Mitmproxy%2010.2.4-linen?logo=mitmproxy-black)](https://mitmproxy.org/) [![Nsis](https://custom-icon-badges.demolab.com/badge/Nsis%203.09-linen?logo=nsis-color)](https://nsis.sourceforge.io/Download) -[![Nuitka](https://custom-icon-badges.demolab.com/badge/Nuitka%202.1.1-linen?logo=nuitka)](https://nuitka.net/) +[![Nuitka](https://custom-icon-badges.demolab.com/badge/Nuitka%202.1.2-linen?logo=nuitka)](https://nuitka.net/) [![PyInstaller](https://custom-icon-badges.demolab.com/badge/PyInstaller%206.5.0-linen?logo=pyinstaller-windowed)](https://pyinstaller.org/en/stable/) * [***NSIS***](https://nsis.sourceforge.io/Download) will be automatically installed if missing. diff --git a/build/changelog.md b/build/changelog.md index d7f888ad..9889fe11 100644 --- a/build/changelog.md +++ b/build/changelog.md @@ -1,7 +1,8 @@ ## 1.4.12.40 -* Fix EPG channels with wrong programmes. -* Fix EPG processing restarting when closing the UI. +* Option to prefer the IPTV provider EPG over external EPG. * Faster EPG processing and loading that use less memory. +* Fix EPG processing restarting when closing the UI. +* Better fuzzy match for external EPG. ## 1.4.12.39 * Bump _mitmproxy_ to 10.2.4. diff --git a/resources/README_template.md b/resources/README_template.md index 6c9ba377..a17d1409 100644 --- a/resources/README_template.md +++ b/resources/README_template.md @@ -3,6 +3,7 @@ * Insert an _All_ category when missing so you can easily **search your entire catalog**. * Update ***[Mpv](https://mpv.io/)*** and ***[Sfvip Player](https://github.com/K4L4Uz/SFVIP-Player/tree/master)*** so you can enjoy their latest features. +* Translated in all ***Sfvip Player*** languages. * Support an **external EPG**[^1]. [^1]: External EPG doesn't work with **local** m3u accounts. diff --git a/sfvip_all_config.py b/sfvip_all_config.py index 3a5dbd6f..45bbf38e 100644 --- a/sfvip_all_config.py +++ b/sfvip_all_config.py @@ -24,6 +24,7 @@ class EPG: url: str | None = None confidence: int = 30 requests_timeout: int = 5 + prefer_internal: bool = True class AllCategory: inject_in_live: bool = False diff --git a/src/mitm/addon/__init__.py b/src/mitm/addon/__init__.py index 430a1c86..fc60751c 100644 --- a/src/mitm/addon/__init__.py +++ b/src/mitm/addon/__init__.py @@ -35,6 +35,10 @@ def get_short_epg(flow: http.HTTPFlow, epg: EPG, api: APItype) -> None: if response := flow.response: def set_response(stream_id: str, limit: str, programmes: str) -> None: + # already an epg ? + if epg.prefer_updater.prefer_internal and (json_response := response_json(flow.response)): + if isinstance(json_response, dict) and json_response.get(programmes): + return server = flow.request.host_header if _id := get_query_key(flow, stream_id): _limit = get_query_key(flow, limit) @@ -153,12 +157,15 @@ def __init__( self.m3u_stream = M3UStream(self.epg) self.panels = AllPanels(all_config.all_name) - def epg_update(self, url: str): + def epg_update(self, url: str) -> None: self.epg.ask_update(url) - def epg_confidence_update(self, confidence: int): + def epg_confidence_update(self, confidence: int) -> None: self.epg.update_confidence(confidence) + def epg_prefer_update(self, prefer_internal: bool) -> None: + self.epg.update_prefer(prefer_internal) + def running(self) -> None: self.epg.start() diff --git a/src/mitm/epg/__init__.py b/src/mitm/epg/__init__.py index 19231b39..348239e3 100644 --- a/src/mitm/epg/__init__.py +++ b/src/mitm/epg/__init__.py @@ -54,6 +54,23 @@ def confidence(self) -> Optional[int]: return self._confidence +class PreferUpdater(JobRunner[bool]): + def __init__(self) -> None: + self._prefer_internal_lock = multiprocessing.Lock() + self._prefer_internal: Optional[bool] = None + super().__init__(self._updating, "Epg prefer internal updater") + + def _updating(self, prefer_internal: bool) -> None: + with self._prefer_internal_lock: + self._prefer_internal = prefer_internal + + @property + def prefer_internal(self) -> Optional[bool]: + with self._prefer_internal_lock: + return self._prefer_internal + + +# pylint: disable=too-many-instance-attributes class EPG: _programme_type = {APItype.XC: EPGprogrammeXC, APItype.MAC: EPGprogrammeMAC, APItype.M3U: EPGprogrammeM3U} _m3u_server = "m3u.server" @@ -63,6 +80,7 @@ def __init__(self, roaming: Path, callbacks: EpgCallbacks, timeout: int) -> None self.servers: dict[str, EPGserverChannels] = {} self.updater = EPGupdater(roaming, callbacks.update_status, timeout) self.confidence_updater = ConfidenceUpdater() + self.prefer_updater = PreferUpdater() self.show_channel = callbacks.show_channel self.channel_shown = False self.show_epg = callbacks.show_epg @@ -74,15 +92,20 @@ def ask_update(self, url: str) -> None: def update_confidence(self, confidence: int) -> None: self.confidence_updater.add_job(confidence) + def update_prefer(self, prefer_internal: bool) -> None: + self.prefer_updater.add_job(prefer_internal) + def wait_running(self, timeout: int) -> bool: return self.updater.wait_running(timeout) def start(self) -> None: self.confidence_updater.start() + self.prefer_updater.start() self.updater.start() def stop(self) -> None: self.updater.stop() + self.prefer_updater.stop() self.confidence_updater.stop() def set_server_channels(self, server: Optional[str], channels: Any, api: APItype) -> None: diff --git a/src/mitm/epg/programme.py b/src/mitm/epg/programme.py index 56ec43ed..b729e097 100644 --- a/src/mitm/epg/programme.py +++ b/src/mitm/epg/programme.py @@ -26,7 +26,6 @@ def from_programme(cls, programme: InternalProgramme, now: float) -> Optional[Se return cls(start, end) return None - # TODO check time (CNN bug) @staticmethod def _get_timestamp(date: str) -> Optional[int]: try: diff --git a/src/mitm/epg/update.py b/src/mitm/epg/update.py index 03689ecb..bb2bd62c 100644 --- a/src/mitm/epg/update.py +++ b/src/mitm/epg/update.py @@ -5,9 +5,17 @@ import re import tempfile from contextlib import contextmanager -from enum import Enum, auto +from enum import Enum, auto, member from pathlib import Path -from typing import IO, Callable, Iterator, NamedTuple, Optional, Self +from typing import ( + IO, + Callable, + Container, + Iterator, + NamedTuple, + Optional, + Self, +) from urllib.parse import urlparse import lxml.etree as ET @@ -50,8 +58,11 @@ class EPGProcess(NamedTuple): def _normalize(name: str) -> str: name = re.sub(r"(\.)([\d]+)", r"\2", name) # turn channel.2 into channel2 - for sub, repl in (".+", "plus"), ("+", "plus"), ("*", "star"): + for sub, repl in ("+", "plus"), ("*", "star"): name = name.replace(sub, repl) + for char in ".|()[]-": + name = name.replace(char, " ") + name = re.sub(r"\s+", " ", name).strip() # remove extra white spaces return name @@ -66,6 +77,7 @@ def _valid_url(url: str) -> bool: def parse_programme(file_obj: IO[bytes] | gzip.GzipFile, epg_process: EPGProcess) -> Iterator[NamedProgrammes]: current_programmes: dict[InternalProgramme, bool] = {} current_channel_id: Optional[str] = None + normalized: dict[str, str] = {} progress_step = ProgressStep() elem: ET.ElementBase title: str = "" @@ -88,12 +100,13 @@ def parse_programme(file_obj: IO[bytes] | gzip.GzipFile, epg_process: EPGProcess desc = elem.text or "" case "programme": if channel_id := elem.get("channel", None): - channel_id = _normalize(channel_id) - if channel_id != current_channel_id: + if not (norm_channel_id := normalized.get(channel_id)): + norm_channel_id = normalized[channel_id] = _normalize(channel_id) + if norm_channel_id != current_channel_id: if current_channel_id and current_programmes: yield NamedProgrammes(tuple(current_programmes), current_channel_id) current_programmes = {} - current_channel_id = channel_id + current_channel_id = norm_channel_id if progress := progress_step.increment_progress(1): epg_process.update_status(EPGProgress(EPGstatus.PROCESSING, progress)) start = elem.get("start", "") @@ -112,6 +125,74 @@ class FoundProgammes(NamedTuple): confidence: int +class FuzzResult(NamedTuple): + name: str + score: float + + @classmethod + def from_result(cls, result: tuple) -> Self: + return cls(*result[:2]) + + +class Scorer(Enum): + RATIO = member(fuzz.ratio) + PARTIAL_RATIO = member(fuzz.partial_ratio) + TOKEN_SET_RATIO = member(fuzz.token_set_ratio) + TOKEN_SORT_RATIO = member(fuzz.token_sort_ratio) + PARTIAL_TOKEN_SET_RATIO = member(fuzz.partial_token_set_ratio) + PARTIAL_TOKEN_SORT_RATIO = member(fuzz.partial_token_sort_ratio) + + +class FuzzBest: + _scorers = ( + # used for cuttoff and overall score + (Scorer.TOKEN_SET_RATIO, 0.5), + # used overal score + (Scorer.TOKEN_SORT_RATIO, 1), + (Scorer.PARTIAL_TOKEN_SORT_RATIO, 0.5), + (Scorer.RATIO, 1), + ) + _limit = 5 + + def __init__(self, choices: Container[str]) -> None: + self._choices = choices + + @staticmethod + def _extract(query: str, choices: Container[str], scorer: Scorer, cutoff: int = 0) -> list[FuzzResult]: + results = process.extractBests( + query, choices, limit=FuzzBest._limit, scorer=scorer.value, score_cutoff=cutoff + ) + return [FuzzResult.from_result(result) for result in results] + + def _best(self, query: str, confidence: int) -> Optional[FuzzResult]: + # cutoff using the 1st scorer + scorer, weight = FuzzBest._scorers[0] + if results := self._extract(query, self._choices, scorer=scorer, cutoff=100 - confidence): + # print(f"{query=}") + # print("found", results) + if len(results) == 1: + return results[0] + # get the accumulated score with weight + scores = {result.name: result.score for result in results} + accumulated_scores = {name: score * weight for name, score in scores.items()} + for scorer, weight in FuzzBest._scorers[1:]: + for result in self._extract(query, scores.keys(), scorer=scorer): + accumulated_scores[result.name] += result.score * weight + # print(accumulated_scores) + best = sorted( + (FuzzResult(name, score) for name, score in accumulated_scores.items()), + key=lambda result: result.score, + reverse=True, + )[0] + return FuzzResult(best.name, scores[best.name]) + return None + + def get(self, query: str, confidence: int) -> Optional[FuzzResult]: + if query in self._choices: + return FuzzResult(query, 100) + return self._best(query, confidence) + + class EPGupdate(NamedTuple): _chunk_size = 1024 * 128 url: str @@ -199,23 +280,16 @@ def from_url(cls, url: str, cache: ChannelsCache, epg_process: EPGProcess, timeo def get_programmes(self, epg_id: str, confidence: int) -> Optional[FoundProgammes]: if self.programmes: - normalized_epg_id = _normalize(epg_id) - if result := process.extractOne( - normalized_epg_id, - self.programmes.all_names, - scorer=fuzz.token_sort_ratio, - score_cutoff=100 - confidence, - ): - normalized_epg_id, score = result[:2] - if programmes := self.programmes.get_programmes(normalized_epg_id): + if found := FuzzBest(self.programmes.all_names).get(_normalize(epg_id), confidence): + if programmes := self.programmes.get_programmes(found.name): logger.info( - "Found Epg %s for %s with confidence %s%% (cut off @%s%%)", - normalized_epg_id, + "Found Epg '%s' for %s with confidence %s%% (cut off @%s%%)", + found.name, epg_id, - score, + found.score, 100 - confidence, ) - return FoundProgammes(programmes, int(score)) + return FoundProgammes(programmes, int(found.score)) return None diff --git a/src/sfvip/epg.py b/src/sfvip/epg.py index 578c226c..d884a6fc 100644 --- a/src/sfvip/epg.py +++ b/src/sfvip/epg.py @@ -1,5 +1,5 @@ import time -from typing import Callable +from typing import Callable, NamedTuple from shared.job_runner import JobRunner from translations.loc import LOC @@ -48,15 +48,15 @@ def on_key_pressed(self, _: str) -> None: self.ui.hover_message.hide() +class EPGUpdates(NamedTuple): + confidence: Callable[[int], None] + prefer: Callable[[bool], None] + url: Callable[[str], None] + + class EpgUpdater: # pylint: disable=too-many-instance-attributes - def __init__( - self, - config: AppConfig, - epg_update: Callable[[str], None], - epg_confidence_update: Callable[[int], None], - ui: UI, - ) -> None: + def __init__(self, config: AppConfig, epg_updates: EPGUpdates, ui: UI) -> None: self.hover_epg = HoverEPG(ui) self.keyboard_watcher = KeyboardWatcher("e", self.hover_epg.on_key_pressed) self.show_epg_job = JobRunner[ShowEpg](self.hover_epg.show_epg, "Show epg job", check_new=False) @@ -64,8 +64,7 @@ def __init__( self.hover_epg.show_channel, "Show channel job", check_new=False ) self.status_job = JobRunner[EPGProgress](ui.set_epg_status, "Epg status job") - self.epg_confidence_update = epg_confidence_update - self.epg_update = epg_update + self.epg_updates = epg_updates self.config = config self.ui = ui @@ -87,9 +86,11 @@ def start(self) -> None: self.show_epg_job.start() self.show_channel_job.start() self.ui.set_epg_url_update(self.config.EPG.url, self.on_epg_url_changed) - self.epg_update(self.config.EPG.url or "") + self.epg_updates.url(self.config.EPG.url or "") self.ui.set_epg_confidence_update(self.config.EPG.confidence, self.on_epg_confidence_changed) - self.epg_confidence_update(self.config.EPG.confidence) + self.epg_updates.confidence(self.config.EPG.confidence) + self.ui.set_epg_prefer_update(self.config.EPG.prefer_internal, self.on_epg_prefer_changed) + self.epg_updates.prefer(self.config.EPG.prefer_internal) def stop(self) -> None: self.show_channel_job.stop() @@ -99,8 +100,12 @@ def stop(self) -> None: def on_epg_url_changed(self, epg_url: str) -> None: self.config.EPG.url = epg_url if epg_url else None - self.epg_update(epg_url) + self.epg_updates.url(epg_url) def on_epg_confidence_changed(self, epg_confidence: int) -> None: self.config.EPG.confidence = epg_confidence - self.epg_confidence_update(epg_confidence) + self.epg_updates.confidence(epg_confidence) + + def on_epg_prefer_changed(self, prefer_internal: bool) -> None: + self.config.EPG.prefer_internal = prefer_internal + self.epg_updates.prefer(prefer_internal) diff --git a/src/sfvip/proxies.py b/src/sfvip/proxies.py index 46000128..caa0151e 100644 --- a/src/sfvip/proxies.py +++ b/src/sfvip/proxies.py @@ -12,7 +12,7 @@ from .accounts import AccountsProxies from .app_info import AppInfo from .cache import CacheProgressListener -from .epg import EpgUpdater +from .epg import EpgUpdater, EPGUpdates from .exceptions import LocalproxyError from .ui import UI @@ -76,7 +76,15 @@ class LocalProxies: _find_ports_retry = 10 def __init__(self, app_info: AppInfo, inject_in_live: bool, accounts_proxies: AccountsProxies, ui: UI) -> None: - self._epg_updater = EpgUpdater(app_info.config, self.epg_update, self.epg_confidence_update, ui) + self._epg_updater = EpgUpdater( + app_info.config, + EPGUpdates( + self.epg_confidence_update, + self.epg_prefer_update, + self.epg_update, + ), + ui, + ) self._cache_progress = CacheProgressListener(ui) self._addon = SfVipAddOn( accounts_proxies.urls, @@ -105,6 +113,9 @@ def epg_update(self, url: str) -> None: def epg_confidence_update(self, confidence: int) -> None: self._addon.epg_confidence_update(confidence) + def epg_prefer_update(self, prefer_internal: bool) -> None: + self._addon.epg_prefer_update(prefer_internal) + def __enter__(self) -> Self: with self._bind_free_ports: modes: set[Mode] = set() diff --git a/src/sfvip/ui/__init__.py b/src/sfvip/ui/__init__.py index 2d918c39..7508bd42 100644 --- a/src/sfvip/ui/__init__.py +++ b/src/sfvip/ui/__init__.py @@ -64,6 +64,9 @@ def set_epg_status(self, epg_status: EPGProgress) -> None: def set_epg_confidence_update(self, confidence: int, callback: Callable[[int], None]) -> None: self._infos.set_epg_confidence_update(confidence, callback) + def set_epg_prefer_update(self, prefer_external: bool, callback: Callable[[bool], None]) -> None: + self._infos.set_epg_prefer_update(prefer_external, callback) + def set_app_auto_update(self, is_checked: bool, callback: Callable[[bool], None]) -> None: self._infos.set_app_auto_update(is_checked, callback) diff --git a/src/sfvip/ui/infos.py b/src/sfvip/ui/infos.py index 832a1f64..98bdf18e 100644 --- a/src/sfvip/ui/infos.py +++ b/src/sfvip/ui/infos.py @@ -244,6 +244,19 @@ def _epg_status(epg_status: EPGProgress) -> Style: return _epg_status_styles(progress).get(epg_status.status, _InfoStyle.app("").grey) +def _get_epg_prefer_update(on: bool = True) -> Style: + return _InfoStyle.app(LOC.Yes).grey if on else _InfoStyle.app(LOC.No).grey + + +def _get_epg_prefer_label() -> Style: + return _InfoStyle.app(LOC.EPGPrefer).grey.no_truncate + + +def _epg_prefer_tooltip() -> Style: + msg = "".join((f"\n\n • {text}" for text in (LOC.EPGPreferYes, LOC.EPGPreferNo))) + return _InfoStyle.tooltip(f"{LOC.EPGPrefer}:{msg}") + + def _player_changelog_tooltip() -> Style: return _InfoStyle.tooltip() @@ -434,6 +447,11 @@ def __init__(self, app_info: AppInfo) -> None: bg=_InfoTheme.bg_rows, **_InfoTheme.confidence_slider, # type:ignore ) + epg_prefer_frame = tk.Frame(frame, bg=_InfoTheme.bg_rows) + epg_prefer_label = tk.Label(epg_prefer_frame, bg=_InfoTheme.bg_rows, **_get_epg_prefer_label().to_tk) + self._epg_prefer_check = CheckBox( + epg_prefer_frame, bg=_InfoTheme.bg_rows, **_InfoTheme.checkbox, **_get_epg_prefer_update().to_tk + ) separator5 = tk.Frame(frame, bg=_InfoTheme.separator) # layout pad = _InfoTheme.pad @@ -470,12 +488,16 @@ def __init__(self, app_info: AppInfo) -> None: self._epg_url.pack(side=tk.LEFT, padx=pad, fill=tk.X, expand=True) self._epg_status.pack(side=tk.LEFT) row += 1 - epg_confidence_frame.grid(row=row, columnspan=3, padx=pad, pady=(0, pad), sticky=tk.NSEW) + epg_confidence_frame.grid(row=row, columnspan=3, padx=pad, pady=0, sticky=tk.NSEW) epg_confidence_label.pack(side=tk.LEFT) self._epg_confidence.pack(side=tk.LEFT) epg_confidence_percent.pack(side=tk.LEFT) self._epg_confidence_slider.pack(padx=(pad, 0), side=tk.LEFT, fill=tk.X, expand=True) row += 1 + epg_prefer_frame.grid(row=row, columnspan=3, padx=pad, pady=pad, sticky=tk.NSEW) + epg_prefer_label.pack(side=tk.LEFT) + self._epg_prefer_check.pack(side=tk.LEFT) + row += 1 separator5.grid(row=row, columnspan=3, sticky=tk.EW) row += 1 super()._layout(row=row) @@ -485,6 +507,7 @@ def __init__(self, app_info: AppInfo) -> None: set_tooltip(_player_changelog_tooltip(), self._player_version, msg_refresh=self.get_changelog) set_tooltip(_app_version_tooltip(app_info), app_version) set_tooltip(_epg_confidence_tooltip(), epg_confidence_label) + set_tooltip(_epg_prefer_tooltip(), epg_prefer_label) def get_changelog(self) -> str: return self._changelog_callback() if self._changelog_callback else "" @@ -550,6 +573,9 @@ def _validate(entry: str) -> bool: self._epg_confidence_slider.bind("", _callback_slider) self._epg_confidence.bind("", _callback_entry) + def set_epg_prefer_update(self, is_checked: bool, callback: Callable[[bool], None]) -> None: + self._epg_prefer_check.set_callback(is_checked, callback, _get_epg_prefer_update) + def set_app_update( self, action_name: Optional[str], diff --git a/translations/bulgarian.json b/translations/bulgarian.json index f5e13875..00b048a2 100644 --- a/translations/bulgarian.json +++ b/translations/bulgarian.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Актуализиране на всички серии", "UpdateAllMovies": "Актуализиране на всички филми", "Confidence0": "0 %: Не се доверявате на EPG и ще получите само точно съвпадение и често никакво.", - "Confidence100": "100 %: Доверявате се напълно на EPG и винаги получавате съвпадение, дори и с лошо качество" + "Confidence100": "100 %: Доверявате се напълно на EPG и винаги получавате съвпадение, дори и с лошо качество", + "EPGPrefer": "Търсене първо при доставчика на IPTV", + "Yes": "Да", + "No": "Не", + "EPGPreferYes": "Да: Първо търсите в EPG на IPTV доставчика. Използвайте външния EPG само когато той не успее.", + "EPGPreferNo": "Не: Търсете първо във външния EPG. Използвайте EPG на доставчика на IPTV само когато той не успее." } \ No newline at end of file diff --git a/translations/english.json b/translations/english.json index 6a7d481e..dd4799b6 100644 --- a/translations/english.json +++ b/translations/english.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Update All series", "UpdateAllMovies": "Update All movies", "Confidence0": "0 %: You don't trust the EPG and you'll only get an exact match and often none", - "Confidence100": "100 %: You completely trust the EPG and you'll always get a match even one of poor quality" + "Confidence100": "100 %: You completely trust the EPG and you'll always get a match even one of poor quality", + "EPGPrefer": "Search the IPTV provider first", + "Yes": "Yes", + "No": "No", + "EPGPreferYes": "Yes: Search the IPTV provider EPG first. Use the external EPG only when it fails.", + "EPGPreferNo": "No: Search the external EPG first. Use the IPTV provider EPG only when it fails." } \ No newline at end of file diff --git a/translations/french.json b/translations/french.json index bf8fe96e..f26a32a9 100644 --- a/translations/french.json +++ b/translations/french.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Mise à jour de toutes les séries", "UpdateAllMovies": "Mise à jour de tous les films", "Confidence0": "0 % : Vous ne faites pas confiance à l'EPG et vous n'obtiendrez qu'une correspondance exacte et souvent aucune.", - "Confidence100": "100 % : Vous faites entièrement confiance à l'EPG et vous obtiendrez toujours une correspondance, même de mauvaise qualité." + "Confidence100": "100 % : Vous faites entièrement confiance à l'EPG et vous obtiendrez toujours une correspondance, même de mauvaise qualité.", + "EPGPrefer": "Chercher d'abord chez le fournisseur IPTV", + "Yes": "Oui", + "No": "Non", + "EPGPreferYes": "Oui : Cherchez d'abord dans l'EPG du fournisseur de télévision par internet. N'utilisez l'EPG externe qu'en cas d'échec.", + "EPGPreferNo": "Non : rechercher d'abord l'EPG externe. Utiliser l'EPG du fournisseur IPTV uniquement en cas d'échec." } \ No newline at end of file diff --git a/translations/german.json b/translations/german.json index bab54951..6b5fe018 100644 --- a/translations/german.json +++ b/translations/german.json @@ -21,7 +21,7 @@ "PleaseWait": "Bitte warten", "CheckLastestLibmpv": "Letzte libmpv-Aktualisierung prüfen", "RestartInstall": "Möchten Sie neu starten, um %s zu installieren?", - "SearchOrDownload": "Möchten Sie es suchen oder herunterladen ?", + "SearchOrDownload": "Möchten Sie es suchen oder herunterladen?", "NoSocketPort": "Kein Socket-Port verfügbar !", "CantStartProxies": "Lokale Proxys können nicht gestartet werden !", "NotFound": "%s nicht gefunden", @@ -50,5 +50,10 @@ "UpdateAllSeries": "Alle Serien aktualisieren", "UpdateAllMovies": "Alle Filme aktualisieren", "Confidence0": "0 %: Sie vertrauen dem EPG nicht und erhalten nur eine exakte Übereinstimmung und oft keine", - "Confidence100": "100 %: Sie vertrauen dem EPG voll und ganz und erhalten immer einen Treffer, auch wenn er von schlechter Qualität ist" + "Confidence100": "100 %: Sie vertrauen dem EPG voll und ganz und erhalten immer einen Treffer, auch wenn er von schlechter Qualität ist", + "EPGPrefer": "Suchen Sie zuerst den IPTV-Anbieter", + "Yes": "Ja", + "No": "Nein", + "EPGPreferYes": "Ja: Suchen Sie zuerst im EPG des IPTV-Anbieters. Verwenden Sie den externen EPG nur, wenn er nicht funktioniert.", + "EPGPreferNo": "Nein: Zuerst im externen EPG suchen. Verwenden Sie den EPG des IPTV-Anbieters nur, wenn dieser nicht funktioniert." } \ No newline at end of file diff --git a/translations/greek.json b/translations/greek.json index b3eedef8..0ec81ad9 100644 --- a/translations/greek.json +++ b/translations/greek.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Ενημέρωση Όλες οι σειρές", "UpdateAllMovies": "Ενημέρωση όλων των ταινιών", "Confidence0": "0 %: Δεν εμπιστεύεστε το EPG και θα έχετε μόνο μια ακριβή αντιστοιχία και συχνά καμία.", - "Confidence100": "100 %: Εμπιστεύεστε πλήρως το EPG και θα έχετε πάντα μια αντιστοιχία, ακόμη και κακής ποιότητας." + "Confidence100": "100 %: Εμπιστεύεστε πλήρως το EPG και θα έχετε πάντα μια αντιστοιχία, ακόμη και κακής ποιότητας.", + "EPGPrefer": "Αναζητήστε πρώτα τον πάροχο IPTV", + "Yes": "Ναι", + "No": "Όχι", + "EPGPreferYes": "Ναι: Αναζητήστε πρώτα το EPG του παρόχου IPTV. Χρησιμοποιείτε το εξωτερικό EPG μόνο όταν αυτό αποτυγχάνει.", + "EPGPreferNo": "Όχι: Αναζητήστε πρώτα το εξωτερικό EPG. Χρησιμοποιήστε το EPG του παρόχου IPTV μόνο όταν αυτό αποτύχει." } \ No newline at end of file diff --git a/translations/italian.json b/translations/italian.json index 84da067e..690c6427 100644 --- a/translations/italian.json +++ b/translations/italian.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Aggiorna tutte le serie", "UpdateAllMovies": "Aggiorna tutti i film", "Confidence0": "0 %: Non ci si fida dell'EPG e si ottiene solo una corrispondenza esatta e spesso nessuna.", - "Confidence100": "100 %: Ci si fida completamente dell'EPG e si ottiene sempre una corrispondenza, anche se di scarsa qualità." + "Confidence100": "100 %: ci si fida completamente dell'EPG e si ottiene sempre una corrispondenza, anche se di scarsa qualità.", + "EPGPrefer": "Cercare prima il provider IPTV", + "Yes": "Si", + "No": "No", + "EPGPreferYes": "Sì: cercate prima l'EPG del provider IPTV. Utilizzare l'EPG esterno solo in caso di errore.", + "EPGPreferNo": "No: Cerca prima l'EPG esterno. Utilizzare l'EPG del provider IPTV solo in caso di errore." } \ No newline at end of file diff --git a/translations/loc/texts.py b/translations/loc/texts.py index 3c8f533e..a0dbdc28 100644 --- a/translations/loc/texts.py +++ b/translations/loc/texts.py @@ -58,10 +58,14 @@ class Texts: UpdateAllSeries: str = "Update All series" UpdateAllMovies: str = "Update All movies" Confidence0: str = "0 %: You don't trust the EPG and you'll only get an exact match and often none" - # fmt: off - # pylint: disable=line-too-long - Confidence100: str = "100 %: You completely trust the EPG and you'll always get a match even one of poor quality" - # fmt: on + Confidence100: str = ( + "100 %: You completely trust the EPG and you'll always get a match even one of poor quality" + ) + EPGPrefer: str = "Search the IPTV provider first" + Yes: str = "Yes" + No: str = "No" + EPGPreferYes: str = "Yes: Search the IPTV provider EPG first. Use the external EPG only when it fails." + EPGPreferNo: str = "No: Search the external EPG first. Use the IPTV provider EPG only when it fails." def as_dict(self) -> dict[str, str]: return dataclasses.asdict(self) diff --git a/translations/polish.json b/translations/polish.json index ca0292f4..301360ff 100644 --- a/translations/polish.json +++ b/translations/polish.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Aktualizacja wszystkich seriali", "UpdateAllMovies": "Aktualizacja wszystkich filmów", "Confidence0": "0%: Nie ufasz EPG i otrzymasz tylko dokładne dopasowanie, a często żadnego.", - "Confidence100": "100%: Całkowicie ufasz EPG i zawsze otrzymasz dopasowanie, nawet słabej jakości." + "Confidence100": "100%: Całkowicie ufasz EPG i zawsze otrzymasz dopasowanie, nawet słabej jakości.", + "EPGPrefer": "Najpierw wyszukaj dostawcę IPTV", + "Yes": "Tak", + "No": "Nie", + "EPGPreferYes": "Tak: Najpierw należy przeszukać EPG dostawcy IPTV. Używaj zewnętrznego EPG tylko wtedy, gdy zawiedzie.", + "EPGPreferNo": "Nie: Najpierw należy przeszukać zewnętrzne EPG. Użyj EPG dostawcy IPTV tylko w przypadku niepowodzenia." } \ No newline at end of file diff --git a/translations/russian.json b/translations/russian.json index 7e04c8d2..1ef0fa80 100644 --- a/translations/russian.json +++ b/translations/russian.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Обновить все серии", "UpdateAllMovies": "Обновить все фильмы", "Confidence0": "0 %: Вы не доверяете EPG и получаете только точные совпадения, а часто и вовсе их не получаете", - "Confidence100": "100 %: Вы полностью доверяете EPG и всегда получите совпадение, даже если оно плохого качества" + "Confidence100": "100 %: Вы полностью доверяете EPG и всегда получите совпадение, даже если оно будет плохого качества.", + "EPGPrefer": "Поиск IPTV-провайдера в первую очередь", + "Yes": "Да", + "No": "Нет", + "EPGPreferYes": "Да: Сначала ищите EPG провайдера IPTV. Используйте внешний EPG только в случае неудачи.", + "EPGPreferNo": "Нет: Поиск внешнего EPG первым. Используйте EPG провайдера IPTV только в случае неудачи." } \ No newline at end of file diff --git a/translations/serbian.json b/translations/serbian.json index 54a1353d..7cfe7f38 100644 --- a/translations/serbian.json +++ b/translations/serbian.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Ажурирајте све серије", "UpdateAllMovies": "Ажурирајте све филмове", "Confidence0": "0 %: Не верујете ЕПГ-у и добићете само потпуно подударање, а често ни једно", - "Confidence100": "100 %: У потпуности верујете ЕПГ-у и увек ћете добити меч, чак и онај лошег квалитета" + "Confidence100": "100 %: У потпуности верујете ЕПГ-у и увек ћете добити меч, чак и онај лошег квалитета", + "EPGPrefer": "Прво потражите ИПТВ провајдера", + "Yes": "да", + "No": "Не", + "EPGPreferYes": "Да: Прво претражите ЕПГ ИПТВ провајдера. Користите екстерни ЕПГ само када не успе.", + "EPGPreferNo": "Не: прво претражите екстерни ЕПГ. Користите ЕПГ ИПТВ провајдера само када не успе." } \ No newline at end of file diff --git a/translations/slovenian.json b/translations/slovenian.json index 1fbd1a52..a01891d5 100644 --- a/translations/slovenian.json +++ b/translations/slovenian.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Posodobitev vseh serij", "UpdateAllMovies": "Posodobitev vseh filmov", "Confidence0": "0 %: Ne zaupate EPG in dobite le natančno ujemanje, pogosto pa nobenega.", - "Confidence100": "100 %: popolnoma zaupate EPG in vedno dobite ujemanje, tudi če je slabe kakovosti" + "Confidence100": "100 %: popolnoma zaupate EPG in vedno dobite ujemanje, tudi če je slabe kakovosti", + "EPGPrefer": "Najprej poiščite ponudnika IPTV", + "Yes": "Da", + "No": "Ne", + "EPGPreferYes": "Da: najprej poiščete EPG ponudnika IPTV. Zunanji program EPG uporabite le, kadar ta ne uspe.", + "EPGPreferNo": "Ne: Najprej poiščite zunanji EPG. EPG ponudnika IPTV uporabite le, če je neuspešen." } \ No newline at end of file diff --git a/translations/spanish.json b/translations/spanish.json index ac3e316b..9b461843 100644 --- a/translations/spanish.json +++ b/translations/spanish.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Actualizar todas las series", "UpdateAllMovies": "Actualizar todas las películas", "Confidence0": "0 %: No confías en la EPG y sólo obtendrás una coincidencia exacta y a menudo ninguna", - "Confidence100": "100 %: Confías plenamente en la EPG y siempre obtendrás una coincidencia, aunque sea de mala calidad." + "Confidence100": "100 %: Confías plenamente en la EPG y siempre obtendrás una coincidencia, aunque sea de mala calidad.", + "EPGPrefer": "Busca primero en el proveedor de IPTV", + "Yes": "Sí", + "No": "No", + "EPGPreferYes": "Sí: Busca primero en la EPG del proveedor de IPTV. Utiliza la EPG externa sólo cuando falle.", + "EPGPreferNo": "No: Buscar primero en la EPG externa. Utiliza la EPG del proveedor de IPTV sólo cuando falla." } \ No newline at end of file diff --git a/translations/turkish.json b/translations/turkish.json index 4d0d61bf..69e73c48 100644 --- a/translations/turkish.json +++ b/translations/turkish.json @@ -50,5 +50,10 @@ "UpdateAllSeries": "Tüm serileri güncelle", "UpdateAllMovies": "Tüm filmleri güncelle", "Confidence0": "0 %: EPG'ye güvenmiyorsunuz ve yalnızca tam bir eşleşme elde edeceksiniz ve genellikle hiçbir eşleşme elde edemeyeceksiniz", - "Confidence100": "100: EPG'ye tamamen güvenirsiniz ve düşük kalitede bile olsa her zaman bir eşleşme elde edersiniz" + "Confidence100": "100: EPG'ye tamamen güvenirsiniz ve düşük kalitede bile olsa her zaman bir eşleşme elde edersiniz", + "EPGPrefer": "Önce IPTV sağlayıcısını arayın", + "Yes": "Evet", + "No": "Hayır", + "EPGPreferYes": "Evet: Önce IPTV sağlayıcısı EPG'sini arayın. Harici EPG'yi yalnızca başarısız olduğunda kullanın.", + "EPGPreferNo": "Hayır: Önce harici EPG'yi arayın. IPTV sağlayıcısı EPG'sini yalnızca başarısız olduğunda kullanın." } \ No newline at end of file