From 15b6df81533ec32aaf016a680148bbbe0762f402 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Wed, 27 Nov 2024 22:45:41 +0000 Subject: [PATCH 01/11] Fix for Issue #348 - Xstream config erroring out on load --- usr/lib/hypnotix/xtream.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/usr/lib/hypnotix/xtream.py b/usr/lib/hypnotix/xtream.py index 1efab4ab..569d2f0e 100644 --- a/usr/lib/hypnotix/xtream.py +++ b/usr/lib/hypnotix/xtream.py @@ -626,7 +626,7 @@ def load_iptv(self): if not skip_stream: # Some channels have no group, # so let's add them to the catch all group - if stream_channel["category_id"] is None: + if stream_channel["category_id"] == "": stream_channel["category_id"] = "9999" elif stream_channel["category_id"] != "1": pass From 54ca3dda5d89c7d37618272a99c44babed747e4c Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Sat, 30 Nov 2024 09:20:15 +0000 Subject: [PATCH 02/11] Issue #348 Reworked the fix based on the suggestion --- usr/lib/hypnotix/xtream.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/usr/lib/hypnotix/xtream.py b/usr/lib/hypnotix/xtream.py index 569d2f0e..9da1532c 100644 --- a/usr/lib/hypnotix/xtream.py +++ b/usr/lib/hypnotix/xtream.py @@ -626,10 +626,8 @@ def load_iptv(self): if not skip_stream: # Some channels have no group, # so let's add them to the catch all group - if stream_channel["category_id"] == "": + if not stream_channel["category_id"]: stream_channel["category_id"] = "9999" - elif stream_channel["category_id"] != "1": - pass # Find the first occurence of the group that the # Channel or Stream is pointing to From 7176fdae4ea68ff853f2b2d9b1c8ac82ed0ad6b4 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Sun, 1 Dec 2024 11:04:40 +0530 Subject: [PATCH 03/11] Initial windows implementation w/ debug statements --- .gitignore | 6 +- usr/lib/hypnotix/common.py | 29 ++++++-- usr/lib/hypnotix/hypnotix.py | 123 ++++++++++++++++++++++++++------- usr/lib/hypnotix/mpv.py | 9 ++- usr/share/hypnotix/hypnotix.ui | 8 ++- 5 files changed, 136 insertions(+), 39 deletions(-) diff --git a/.gitignore b/.gitignore index c4a6e209..60ad87ac 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,8 @@ debian/*debhelper* debian/files debian/hypnotix.substvars usr/share/locale -usr/share/hypnotix/hypnotix.ui~ \ No newline at end of file +usr/share/hypnotix/hypnotix.ui~ +.venv +.vscode +usr/share/data/ +usr/lib/hypnotix/_pycache_/* \ No newline at end of file diff --git a/usr/lib/hypnotix/common.py b/usr/lib/hypnotix/common.py index f2c6ac0f..e3647b28 100755 --- a/usr/lib/hypnotix/common.py +++ b/usr/lib/hypnotix/common.py @@ -11,11 +11,14 @@ EXTINF = re.compile(r'^#EXTINF:(?P-?\d+?) ?(?P.*),(?P.*?)$') SERIES = re.compile(r"(?P<series>.*?) S(?P<season>.\d{1,2}).*E(?P<episode>.\d{1,2}.*)$", re.IGNORECASE) -PROVIDERS_PATH = os.path.join(GLib.get_user_cache_dir(), "hypnotix", "providers") - +# PROVIDERS_PATH = os.path.join(GLib.get_user_cache_dir(), "hypnotix", "providers") +PROVIDERS_PATH = "usr/share/data/hypnotix/providers" TV_GROUP, MOVIES_GROUP, SERIES_GROUP = range(3) +print("PROVIDERS_PATH:", PROVIDERS_PATH) -FAVORITES_PATH = os.path.join(GLib.get_user_cache_dir(), "hypnotix", "favorites", "list") +# FAVORITES_PATH = os.path.join(GLib.get_user_cache_dir(), "hypnotix", "favorites", "list") +FAVORITES_PATH = "usr/share/data/hypnotix/favorites/list" +print("FAVORITES_PATH:", FAVORITES_PATH) # Used as a decorator to run things in the background def async_function(func): @@ -133,7 +136,10 @@ def __init__(self, provider, info): class Manager: def __init__(self, settings): - os.system("mkdir -p '%s'" % PROVIDERS_PATH) + print("directory creation") + # os.system("mkdir -p '%s'" % PROVIDERS_PATH) + os.makedirs(os.path.dirname(PROVIDERS_PATH), exist_ok=True) #Windows + os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) #Windows self.verbose = False self.settings = settings @@ -288,9 +294,18 @@ def load_channels(self, provider): def load_favorites(self): favorites = [] - with open(FAVORITES_PATH, 'r', encoding="utf-8", errors="ignore") as f: - for line in f: - favorites.append(line.strip()) + print("Loading favorites") + try: + with open(FAVORITES_PATH, 'r', encoding="utf-8", errors="ignore") as f: + print(f"Opening favorites list: {f.name}") + print("Loaded favorites") + for line in f: + favorites.append(line.strip()) + except FileNotFoundError: + print(f"Creating new favorites file at: {FAVORITES_PATH}") + os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) + with open(FAVORITES_PATH, 'w', encoding="utf-8") as f: + pass # Create empty file return favorites def save_favorites(self, favorites): diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 9612aa94..0cd0d21f 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1,4 +1,5 @@ #!/usr/bin/python3 +import ctypes import gettext import locale import os @@ -11,6 +12,30 @@ from functools import partial from pathlib import Path +# Set C locale for MPV - using environment variables and explicit locale names +os.environ['LC_ALL'] = 'C' +os.environ['LC_NUMERIC'] = 'C' +os.environ['LANG'] = 'C' + +# Try different locale names that might work on Windows +for loc in ['C', 'C.UTF-8', 'en_US.UTF-8', 'English_United States.1252']: + try: + locale.setlocale(locale.LC_ALL, loc) + locale.setlocale(locale.LC_NUMERIC, loc) + print(f"Successfully set locale to: {loc}") + break + except locale.Error: + continue + +# Debug output for locale and environment +print("Environment:") +print("LC_ALL:", os.environ.get('LC_ALL')) +print("LC_NUMERIC:", os.environ.get('LC_NUMERIC')) +print("LANG:", os.environ.get('LANG')) +print("\nLocale settings:") +print("Current LC_ALL:", locale.getlocale(locale.LC_ALL)) +print("Current LC_NUMERIC:", locale.getlocale(locale.LC_NUMERIC)) + # Force X11 on a Wayland session if "WAYLAND_DISPLAY" in os.environ: os.environ["WAYLAND_DISPLAY"] = "" @@ -18,16 +43,30 @@ # Suppress GTK deprecation warnings warnings.filterwarnings("ignore") +import platform +IS_WINDOWS = platform.system() == "Windows" + +""" # Add MPV DLL path for Windows +if IS_WINDOWS: + print("Windows detected") + mpv_path = "C:/tools/msys64/ucrt64/bin" + os.environ["PATH"] = mpv_path + os.pathsep + os.environ["PATH"] + print(os.environ["PATH"]) """ + import gi gi.require_version("Gtk", "3.0") -gi.require_version("XApp", "1.0") -from gi.repository import Gtk, Gdk, Gio, XApp, GdkPixbuf, GLib, Pango +# Conditionally import XApp based on platform +if not IS_WINDOWS: + gi.require_version("XApp", "1.0") + from gi.repository import Gtk, Gdk, Gio, XApp, GdkPixbuf, GLib, Pango +else: + from gi.repository import Gtk, Gdk, Gio, GdkPixbuf, GLib, Pango import mpv import requests import setproctitle -from imdb import IMDb +#from imdb import Cinemagoer from unidecode import unidecode from common import Manager, Provider, Channel, MOVIES_GROUP, PROVIDERS_PATH, SERIES_GROUP, TV_GROUP,\ @@ -39,9 +78,17 @@ # i18n APP = "hypnotix" LOCALE_DIR = "/usr/share/locale" -locale.bindtextdomain(APP, LOCALE_DIR) -gettext.bindtextdomain(APP, LOCALE_DIR) -gettext.textdomain(APP) +print("LOCALE_DIR:", LOCALE_DIR) + +if not IS_WINDOWS: + locale.bindtextdomain(APP, LOCALE_DIR) + gettext.bindtextdomain(APP, LOCALE_DIR) + gettext.textdomain(APP) +else: + # Windows doesn't support bindtextdomain through locale + gettext.bindtextdomain(APP, LOCALE_DIR) + gettext.textdomain(APP) + _ = gettext.gettext @@ -72,7 +119,9 @@ } COUNTRY_CODES = {} -with open("/usr/share/hypnotix/countries.list") as f: +with open("usr/share/hypnotix/countries.list") as f: + print(f"Opening countries.list: {f.name}") + print("Loading countries.list") for line in f: line = line.strip() code, name = line.split(":") @@ -139,7 +188,7 @@ def __init__(self, application): self.latest_search_bar_text = None self.visible_search_results = 0 self.mpv = None - self.ia = IMDb() + # self.ia = IMDb() self.page_is_loading = False # used to ignore signals while we set widget states @@ -149,7 +198,7 @@ def __init__(self, application): # Used for redownloading timer self.reload_timeout_sec = 60 * 5 self._timerid = -1 - gladefile = "/usr/share/hypnotix/hypnotix.ui" + gladefile = "usr/share/hypnotix/hypnotix.ui" self.builder = Gtk.Builder() self.builder.set_translation_domain(APP) self.builder.add_from_file(gladefile) @@ -161,7 +210,7 @@ def __init__(self, application): self.info_window = self.builder.get_object("stream_info_window") provider = Gtk.CssProvider() - provider.load_from_path("/usr/share/hypnotix/hypnotix.css") + provider.load_from_path("usr/share/hypnotix/hypnotix.css") screen = Gdk.Display.get_default_screen(Gdk.Display.get_default()) # I was unable to found instrospected version of this Gtk.StyleContext.add_provider_for_screen( @@ -354,10 +403,11 @@ def __init__(self, application): # Dark mode manager # keep a reference to it (otherwise it gets randomly garbage collected) - try: - self.dark_mode_manager = XApp.DarkModeManager.new(prefer_dark_mode=True) - except Exception: - pass + if not IS_WINDOWS: + try: + self.dark_mode_manager = XApp.DarkModeManager.new(prefer_dark_mode=True) + except Exception: + pass # Menubar accel_group = Gtk.AccelGroup() @@ -409,9 +459,9 @@ def __init__(self, application): self.provider_type_combo.set_active(0) # Select 1st type self.provider_type_combo.connect("changed", self.on_provider_type_combo_changed) - self.tv_logo.set_from_surface(self.get_surface_for_file("/usr/share/hypnotix/pictures/tv.svg", 258, 258)) - self.movies_logo.set_from_surface(self.get_surface_for_file("/usr/share/hypnotix/pictures/movies.svg", 258, 258)) - self.series_logo.set_from_surface(self.get_surface_for_file("/usr/share/hypnotix/pictures/series.svg", 258, 258)) + self.tv_logo.set_from_surface(self.get_surface_for_file("usr/share/hypnotix/pictures/tv.svg", 258, 258)) + self.movies_logo.set_from_surface(self.get_surface_for_file("usr/share/hypnotix/pictures/movies.svg", 258, 258)) + self.series_logo.set_from_surface(self.get_surface_for_file("usr/share/hypnotix/pictures/series.svg", 258, 258)) self.reload(page="landing_page") @@ -443,7 +493,7 @@ def get_surf_based_image(self, filename, width, height): return Gtk.Image.new_from_surface(surf) def add_flag(self, code, box): - path = f"/usr/share/circle-flags-svg/{code.lower()}.svg" + path = f"usr/share/circle-flags-svg/{code.lower()}.svg" if os.path.exists(path): try: image = self.get_surf_based_image(path, -1, 32) @@ -457,7 +507,7 @@ def add_flag(self, code, box): def add_badge(self, word, box, added_words): if word not in added_words: for extension in ["svg", "png"]: - path = "/usr/share/hypnotix/pictures/badges/%s.%s" % (word, extension) + path = "usr/share/hypnotix/pictures/badges/%s.%s" % (word, extension) if os.path.exists(path): try: image = self.get_surf_based_image(path, -1, 32) @@ -496,7 +546,8 @@ def show_groups(self, widget, content_type): for country_name in COUNTRY_CODES.keys(): if country_name.lower() == group.name.lower(): found_flag = True - self.add_flag(COUNTRY_CODES[country_name], box) + # commenting out as circle-flags is not added + # self.add_flag(COUNTRY_CODES[country_name], box) break if not found_flag: @@ -708,7 +759,7 @@ def get_channel_surface(self, path): else: surface = self.get_surface_for_file(path, 200, 200) except Exception: - surface = self.get_surface_for_file("/usr/share/hypnotix/generic_tv_logo.png", 22, 22) + surface = self.get_surface_for_file("usr/share/hypnotix/generic_tv_logo.png", 22, 22) return surface def on_go_back_button(self, widget): @@ -864,7 +915,7 @@ def navigate_to(self, page, name="", favorites=False): self.headerbar.set_subtitle(_("Reset providers")) def open_keyboard_shortcuts(self, widget): - gladefile = "/usr/share/hypnotix/shortcuts.ui" + gladefile = "usr/share/hypnotix/shortcuts.ui" builder = Gtk.Builder() builder.set_translation_domain(APP) builder.add_from_file(gladefile) @@ -1484,7 +1535,7 @@ def open_about(self, widget): dlg.set_program_name(_("Hypnotix")) dlg.set_comments(_("Watch TV")) try: - h = open("/usr/share/common-licenses/GPL", encoding="utf-8") + h = open("usr/share/common-licenses/GPL", encoding="utf-8") s = h.readlines() gpl = "" for line in s: @@ -1551,10 +1602,12 @@ def on_key_press_event(self, widget, event): def reload(self, page=None, refresh=False): self.favorite_data = self.manager.load_favorites() self.status(_("Loading providers...")) + print("providers string: ", self.settings.get_strv("providers")) self.providers = [] for provider_info in self.settings.get_strv("providers"): try: provider = Provider(name=None, provider_info=provider_info) + print(f"Loading provider: {provider.name}") # Add provider to list. This must be done so that it shows up in the # list of providers for editing. @@ -1680,6 +1733,27 @@ def reinit_mpv(self): while not self.mpv_drawing_area.get_window() and not Gtk.events_pending(): time.sleep(0.1) + # Get the window handle of the drawing area + gdk_window = self.mpv_drawing_area.get_window() + if gdk_window is not None: + print("gdk_window: ", gdk_window) + if IS_WINDOWS: + # Windows-specific handling + if not gdk_window.ensure_native(): + print("Error - video playback requires a native window") + ctypes.pythonapi.PyCapsule_GetPointer.restype = ctypes.c_void_p + ctypes.pythonapi.PyCapsule_GetPointer.argtypes = [ctypes.py_object] + drawingarea_gpointer = ctypes.cast(ctypes.pythonapi.PyCapsule_GetPointer(gdk_window.__gpointer__, None), ctypes.c_void_p) + gdkdll = ctypes.CDLL("libgdk-3-0.dll") + wid = gdkdll.gdk_win32_window_get_handle(drawingarea_gpointer) + else: + # Linux-specific handling + wid = gdk_window.get_xid() + else: + raise RuntimeError("Failed to get window handle") + + options["wid"] = str(wid) # Set the window ID for MPV + osc = True if "osc" in options: # To prevent 'multiple values for keyword argument'! @@ -1691,8 +1765,7 @@ def reinit_mpv(self): input_default_bindings=True, input_vo_keyboard=True, osc=osc, - ytdl=True, - wid=str(self.mpv_drawing_area.get_window().get_xid()) + ytdl=True ) def on_mpv_drawing_area_draw(self, widget, cr): diff --git a/usr/lib/hypnotix/mpv.py b/usr/lib/hypnotix/mpv.py index 4817a13a..fe987438 100644 --- a/usr/lib/hypnotix/mpv.py +++ b/usr/lib/hypnotix/mpv.py @@ -29,12 +29,15 @@ import traceback if os.name == 'nt': - dll = ctypes.util.find_library('mpv-1.dll') + print("Windows mpv to be loaded") + dll = ctypes.util.find_library('libmpv-2.dll') # Try MSYS2 DLL name first if dll is None: - raise OSError('Cannot find mpv-1.dll in your system %PATH%. One way to deal with this is to ship mpv-1.dll ' + dll = ctypes.util.find_library('mpv-1.dll') # Fall back to original name + if dll is None: + raise OSError('Cannot find libmpv-2.dll or mpv-1.dll in your system %PATH%. One way to deal with this is to ship the DLL ' 'with your script and put the directory your script is in into %PATH% before "import mpv": ' 'os.environ["PATH"] = os.path.dirname(__file__) + os.pathsep + os.environ["PATH"] ' - 'If mpv-1.dll is located elsewhere, you can add that path to os.environ["PATH"].') + 'If the DLL is located elsewhere, you can add that path to os.environ["PATH"].') backend = CDLL(dll) fs_enc = 'utf-8' else: diff --git a/usr/share/hypnotix/hypnotix.ui b/usr/share/hypnotix/hypnotix.ui index 0308f1bb..80d789cc 100644 --- a/usr/share/hypnotix/hypnotix.ui +++ b/usr/share/hypnotix/hypnotix.ui @@ -2,7 +2,7 @@ <!-- Generated with glade 3.38.2 --> <interface> <requires lib="gtk+" version="3.20"/> - <requires lib="xapp" version="0.0"/> + <!-- <requires lib="xapp" version="0.0"/> --> <object class="GtkMenu" id="main_menu"> <property name="visible">True</property> <property name="can-focus">False</property> @@ -142,7 +142,8 @@ <object class="GtkImage"> <property name="visible">True</property> <property name="can-focus">False</property> - <property name="icon-name">xapp-prefs-behavior-symbolic</property> + <!-- <property name="icon-name">xapp-prefs-behavior-symbolic</property> --> + <property name="icon-name">preferences-system-symbolic</property> <property name="icon_size">3</property> </object> </child> @@ -985,7 +986,8 @@ <property name="can-focus">False</property> <property name="spacing">6</property> <child> - <object class="XAppStackSidebar"> + <!-- <object class="XAppStackSidebar"> Does not work on Windows --> + <object class="GtkStackSidebar"> <property name="visible">True</property> <property name="can-focus">False</property> <property name="stack">pref_stack</property> From 18687d91ce5711832d26e1f7ac329f1e3d3847b6 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Sun, 1 Dec 2024 11:13:40 +0530 Subject: [PATCH 04/11] Updated gitignore for windows --- .gitignore | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 60ad87ac..4b931341 100644 --- a/.gitignore +++ b/.gitignore @@ -6,5 +6,6 @@ usr/share/locale usr/share/hypnotix/hypnotix.ui~ .venv .vscode -usr/share/data/ -usr/lib/hypnotix/_pycache_/* \ No newline at end of file +usr/share/data +usr/lib/hypnotix/__pycache__ +usr/share/glib-2.0/schemas/*.compiled \ No newline at end of file From 3b2830148e6557e965b156eb8e28344b982ed871 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Sun, 1 Dec 2024 12:59:24 +0530 Subject: [PATCH 05/11] Removed locale debug statements from hypnotix.py. Set locale explicitly in mpv.py. Restored providers and favorites to use local os specific cache directory. --- usr/lib/hypnotix/common.py | 15 +++++++-------- usr/lib/hypnotix/hypnotix.py | 30 ------------------------------ usr/lib/hypnotix/mpv.py | 6 ++++++ 3 files changed, 13 insertions(+), 38 deletions(-) diff --git a/usr/lib/hypnotix/common.py b/usr/lib/hypnotix/common.py index e3647b28..97ac8e8a 100755 --- a/usr/lib/hypnotix/common.py +++ b/usr/lib/hypnotix/common.py @@ -11,13 +11,11 @@ EXTINF = re.compile(r'^#EXTINF:(?P<duration>-?\d+?) ?(?P<params>.*),(?P<title>.*?)$') SERIES = re.compile(r"(?P<series>.*?) S(?P<season>.\d{1,2}).*E(?P<episode>.\d{1,2}.*)$", re.IGNORECASE) -# PROVIDERS_PATH = os.path.join(GLib.get_user_cache_dir(), "hypnotix", "providers") -PROVIDERS_PATH = "usr/share/data/hypnotix/providers" +PROVIDERS_PATH = os.path.normpath(os.path.join(GLib.get_user_cache_dir(), "hypnotix", "providers")) TV_GROUP, MOVIES_GROUP, SERIES_GROUP = range(3) print("PROVIDERS_PATH:", PROVIDERS_PATH) -# FAVORITES_PATH = os.path.join(GLib.get_user_cache_dir(), "hypnotix", "favorites", "list") -FAVORITES_PATH = "usr/share/data/hypnotix/favorites/list" +FAVORITES_PATH = os.path.normpath(os.path.join(GLib.get_user_cache_dir(), "hypnotix", "favorites", "list")) print("FAVORITES_PATH:", FAVORITES_PATH) # Used as a decorator to run things in the background @@ -53,7 +51,8 @@ def __init__(self, name, provider_info): self.name, self.type_id, self.url, self.username, self.password, self.epg = provider_info.split(":::") else: self.name = name - self.path = os.path.join(PROVIDERS_PATH, slugify(self.name)) + os.makedirs(os.path.dirname(PROVIDERS_PATH), exist_ok=True) #Windows - create directory if not exists + self.path = os.path.normpath(os.path.join(PROVIDERS_PATH, slugify(self.name))) self.groups = [] self.channels = [] self.movies = [] @@ -132,14 +131,14 @@ def __init__(self, provider, info): provider_name = "favorites" else: provider_name = provider.name - self.logo_path = os.path.join(PROVIDERS_PATH, "%s-%s%s" % (slugify(provider_name), slugify(self.name), ext)) + self.logo_path = os.path.normpath(os.path.join(PROVIDERS_PATH, "%s-%s%s" % (slugify(provider_name), slugify(self.name), ext))) class Manager: def __init__(self, settings): print("directory creation") # os.system("mkdir -p '%s'" % PROVIDERS_PATH) - os.makedirs(os.path.dirname(PROVIDERS_PATH), exist_ok=True) #Windows - os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) #Windows + os.makedirs(os.path.dirname(PROVIDERS_PATH), exist_ok=True) #Windows - create providers directory if not exists + os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) #Windows - create favorites directory if not exists self.verbose = False self.settings = settings diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 0cd0d21f..f43c2a07 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -12,30 +12,6 @@ from functools import partial from pathlib import Path -# Set C locale for MPV - using environment variables and explicit locale names -os.environ['LC_ALL'] = 'C' -os.environ['LC_NUMERIC'] = 'C' -os.environ['LANG'] = 'C' - -# Try different locale names that might work on Windows -for loc in ['C', 'C.UTF-8', 'en_US.UTF-8', 'English_United States.1252']: - try: - locale.setlocale(locale.LC_ALL, loc) - locale.setlocale(locale.LC_NUMERIC, loc) - print(f"Successfully set locale to: {loc}") - break - except locale.Error: - continue - -# Debug output for locale and environment -print("Environment:") -print("LC_ALL:", os.environ.get('LC_ALL')) -print("LC_NUMERIC:", os.environ.get('LC_NUMERIC')) -print("LANG:", os.environ.get('LANG')) -print("\nLocale settings:") -print("Current LC_ALL:", locale.getlocale(locale.LC_ALL)) -print("Current LC_NUMERIC:", locale.getlocale(locale.LC_NUMERIC)) - # Force X11 on a Wayland session if "WAYLAND_DISPLAY" in os.environ: os.environ["WAYLAND_DISPLAY"] = "" @@ -78,7 +54,6 @@ # i18n APP = "hypnotix" LOCALE_DIR = "/usr/share/locale" -print("LOCALE_DIR:", LOCALE_DIR) if not IS_WINDOWS: locale.bindtextdomain(APP, LOCALE_DIR) @@ -120,8 +95,6 @@ COUNTRY_CODES = {} with open("usr/share/hypnotix/countries.list") as f: - print(f"Opening countries.list: {f.name}") - print("Loading countries.list") for line in f: line = line.strip() code, name = line.split(":") @@ -1602,13 +1575,10 @@ def on_key_press_event(self, widget, event): def reload(self, page=None, refresh=False): self.favorite_data = self.manager.load_favorites() self.status(_("Loading providers...")) - print("providers string: ", self.settings.get_strv("providers")) self.providers = [] for provider_info in self.settings.get_strv("providers"): try: provider = Provider(name=None, provider_info=provider_info) - print(f"Loading provider: {provider.name}") - # Add provider to list. This must be done so that it shows up in the # list of providers for editing. self.providers.append(provider) diff --git a/usr/lib/hypnotix/mpv.py b/usr/lib/hypnotix/mpv.py index fe987438..5f047a8e 100644 --- a/usr/lib/hypnotix/mpv.py +++ b/usr/lib/hypnotix/mpv.py @@ -30,6 +30,12 @@ if os.name == 'nt': print("Windows mpv to be loaded") + import locale + lc, enc = locale.getlocale(locale.LC_NUMERIC) + # libmpv requires LC_NUMERIC to be set to "C". Since messing with global variables everyone else relies upon is + # still better than segfaulting, we are setting LC_NUMERIC to "C". + locale.setlocale(locale.LC_NUMERIC, 'C') + dll = ctypes.util.find_library('libmpv-2.dll') # Try MSYS2 DLL name first if dll is None: dll = ctypes.util.find_library('mpv-1.dll') # Fall back to original name From 1b29e5c8f9a642cdc61139f7c8946c90474de267 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Sun, 1 Dec 2024 14:55:13 +0530 Subject: [PATCH 06/11] Cleaning up print statements and providers directory issue --- usr/lib/hypnotix/common.py | 18 +++++++----------- usr/lib/hypnotix/hypnotix.py | 8 -------- usr/lib/hypnotix/mpv.py | 1 - 3 files changed, 7 insertions(+), 20 deletions(-) diff --git a/usr/lib/hypnotix/common.py b/usr/lib/hypnotix/common.py index 97ac8e8a..51a685ab 100755 --- a/usr/lib/hypnotix/common.py +++ b/usr/lib/hypnotix/common.py @@ -12,11 +12,12 @@ SERIES = re.compile(r"(?P<series>.*?) S(?P<season>.\d{1,2}).*E(?P<episode>.\d{1,2}.*)$", re.IGNORECASE) PROVIDERS_PATH = os.path.normpath(os.path.join(GLib.get_user_cache_dir(), "hypnotix", "providers")) + TV_GROUP, MOVIES_GROUP, SERIES_GROUP = range(3) -print("PROVIDERS_PATH:", PROVIDERS_PATH) +# print("PROVIDERS_PATH:", PROVIDERS_PATH) FAVORITES_PATH = os.path.normpath(os.path.join(GLib.get_user_cache_dir(), "hypnotix", "favorites", "list")) -print("FAVORITES_PATH:", FAVORITES_PATH) +#print("FAVORITES_PATH:", FAVORITES_PATH) # Used as a decorator to run things in the background def async_function(func): @@ -51,7 +52,6 @@ def __init__(self, name, provider_info): self.name, self.type_id, self.url, self.username, self.password, self.epg = provider_info.split(":::") else: self.name = name - os.makedirs(os.path.dirname(PROVIDERS_PATH), exist_ok=True) #Windows - create directory if not exists self.path = os.path.normpath(os.path.join(PROVIDERS_PATH, slugify(self.name))) self.groups = [] self.channels = [] @@ -135,10 +135,9 @@ def __init__(self, provider, info): class Manager: def __init__(self, settings): - print("directory creation") - # os.system("mkdir -p '%s'" % PROVIDERS_PATH) - os.makedirs(os.path.dirname(PROVIDERS_PATH), exist_ok=True) #Windows - create providers directory if not exists - os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) #Windows - create favorites directory if not exists + #os.system("mkdir -p '%s'" % PROVIDERS_PATH) + os.makedirs(PROVIDERS_PATH, exist_ok=True) #Windows - create providers directory if not exists + os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) #Windows - create favorites directory if not exist self.verbose = False self.settings = settings @@ -293,15 +292,12 @@ def load_channels(self, provider): def load_favorites(self): favorites = [] - print("Loading favorites") try: with open(FAVORITES_PATH, 'r', encoding="utf-8", errors="ignore") as f: - print(f"Opening favorites list: {f.name}") - print("Loaded favorites") for line in f: favorites.append(line.strip()) except FileNotFoundError: - print(f"Creating new favorites file at: {FAVORITES_PATH}") + # Create favorites directory and new listfile if not exists os.makedirs(os.path.dirname(FAVORITES_PATH), exist_ok=True) with open(FAVORITES_PATH, 'w', encoding="utf-8") as f: pass # Create empty file diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index f43c2a07..a8ceaf38 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -22,13 +22,6 @@ import platform IS_WINDOWS = platform.system() == "Windows" -""" # Add MPV DLL path for Windows -if IS_WINDOWS: - print("Windows detected") - mpv_path = "C:/tools/msys64/ucrt64/bin" - os.environ["PATH"] = mpv_path + os.pathsep + os.environ["PATH"] - print(os.environ["PATH"]) """ - import gi gi.require_version("Gtk", "3.0") @@ -1706,7 +1699,6 @@ def reinit_mpv(self): # Get the window handle of the drawing area gdk_window = self.mpv_drawing_area.get_window() if gdk_window is not None: - print("gdk_window: ", gdk_window) if IS_WINDOWS: # Windows-specific handling if not gdk_window.ensure_native(): diff --git a/usr/lib/hypnotix/mpv.py b/usr/lib/hypnotix/mpv.py index 5f047a8e..9fa89d85 100644 --- a/usr/lib/hypnotix/mpv.py +++ b/usr/lib/hypnotix/mpv.py @@ -29,7 +29,6 @@ import traceback if os.name == 'nt': - print("Windows mpv to be loaded") import locale lc, enc = locale.getlocale(locale.LC_NUMERIC) # libmpv requires LC_NUMERIC to be set to "C". Since messing with global variables everyone else relies upon is From fc4973090ee932bdaadd9a29bf0f3191da5335ef Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Sun, 1 Dec 2024 15:27:11 +0530 Subject: [PATCH 07/11] Integrated compiling gsettings schema and setting env variable as part of the _main_ as otherwise has to be compiled manually in Windows --- usr/lib/hypnotix/hypnotix.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index a8ceaf38..03b7a2f9 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1764,7 +1764,27 @@ def on_fullscreen_button_clicked(self, widget): def on_close_info_window_button_clicked(self, widget): self.info_window.hide() +def compile_gsettings_schema(schema_dir): + # Compile the GSettings schemas + try: + subprocess.run(['glib-compile-schemas', schema_dir], check=True) + #print("GSettings schemas compiled successfully.") + except subprocess.CalledProcessError as e: + print(f"Error compiling GSettings schemas: {e}") + +def set_gsettings_schema_dir(schema_dir): + # Set the GSETTINGS_SCHEMA_DIR environment variable + os.environ['GSETTINGS_SCHEMA_DIR'] = schema_dir + #print(f"GSETTINGS_SCHEMA_DIR set to: {schema_dir}") if __name__ == "__main__": + schema_directory = "usr/share/glib-2.0/schemas/" + + # Compile the schemas. Added for Windows in specific. + compile_gsettings_schema(schema_directory) + + # Set the environment variable. Added for Windows in specific. + set_gsettings_schema_dir(schema_directory) + application = MyApplication("org.x.hypnotix", Gio.ApplicationFlags.FLAGS_NONE) application.run() From 69155589c11204d37c439584980a983ecbe17f92 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Wed, 4 Dec 2024 04:14:49 +0000 Subject: [PATCH 08/11] Updated README.md for Windows --- README.md | 118 +++++++++++++++++++++++++++++++++++++++++------------- 1 file changed, 91 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 156076d8..965b24a4 100644 --- a/README.md +++ b/README.md @@ -1,34 +1,108 @@ -# Hypnotix -![build](https://github.com/linuxmint/hypnotix/actions/workflows/build.yml/badge.svg) +# Hypnotix for Windows -Hypnotix is an IPTV streaming application with support for live TV, movies and series. +Hypnotix for Windows is an IPTV streaming application with support for live TV. -![shadow](https://user-images.githubusercontent.com/1138515/99553152-b8bac780-29b5-11eb-9d75-8756ed7581b6.png) +It is a fork of [hypnotix](https://github.com/linuxmint/hypnotix) which is a Linux only app built and maintained for Linux Mint. It is built with GTK3 and my aim is to port this over to Windows with as minimal changes as required and future compatibility in mind. -It can support multiple IPTV providers of the following types: +I have only an initial build running in the crudest way possible and I have plans to bring this up to spec and also make it package/ship ready in future. For now, you will need to build the app on your own for now with the instructions below. -- M3U URL -- Xtream API -- Local M3U playlist +**Known Issues:** +- Live TV video overlay controls are not available. This is potentially a libmpv issue. See [this issue in python-mpv](https://github.com/jaseg/python-mpv/issues/103) for details. +- Movie and Series modules are not tested and are not a priority. +- There could be other issues # License - Code: GPLv3 -- Flags: https://github.com/linuxmint/flags - Icons on the landing page: CC BY-ND 2.0 -# Requirements +# Development Requirements + +- [MSYS2](https://github.com/msys2) (tested with UCRT64 profile) +- MSYS2 packages + + > Note 1: Not all packages might be required and there might be duplicates. I need to test this in a fresh VM to find only the essential ones later. + + > Note 2: Install the packages as ```pacman -S <packagename>``` in MSYS2 + + GTK + - mingw-w64-x86_64-gtk3 + - mingw-w64-x86_64-glib2 + - mingw-w64-x86_64-glade + - mingw-w64-x86_64-gstreamer + + MPV + - mingw-w64-x86_64-mpv + - ucrt64/mingw-w64-ucrt-x86_64-mpv + + GCC and Build + - mingw-w64-x86_64-gcc + - mingw-w64-x86_64-make + - mingw-w64-x86_64-pkg-config + + Python + - mingw-w64-x86_64-python3 + - mingw-w64-x86_64-python3-gobject + - mingw-w64-x86_64-python-pip + + XML + - mingw-w64-x86_64-libxml2 + - mingw-w64-x86_64-libxslt + + Adwaita Theme for icons + - mingw64/mingw-w64-x86_64-adwaita-icon-theme + - ucrt64/mingw-w64-ucrt-x86_64-adwaita-icon-theme + + +# Run Steps: + +1) Install MSYS2 and git clone repo. + > **Note:** Use Windows Terminal (with MSYS2/UCRT64 shell profile) or MSYS2 app directly or whichever terminal app you are comfortable with for shell access. + ```git clone git@github.com:lakshminarayananb/hypnotix-windows.git``` + +2) Install the above mentioned development packages. + ``` + pacman -S mingw-w64-x86_64-gtk3 + pacman -S mingw-w64-x86_64-glade + pacman -S mingw-w64-x86_64-glib2 + pacman -S mingw-w64-x86_64-gstreamer + + pacman -S mingw-w64-x86_64-mpv + pacman -S ucrt64/mingw-w64-ucrt-x86_64-mpv + + pacman -S mingw-w64-x86_64-gcc + pacman -S mingw-w64-x86_64-make + pacman -S mingw-w64-x86_64-pkg-config + + pacman -S mingw-w64-x86_64-python3 + pacman -S mingw-w64-x86_64-python3-gobject + pacman -S mingw-w64-x86_64-python-pip + + pacman -S mingw-w64-x86_64-libxml2 + pacman -S mingw-w64-x86_64-libxslt + + pacman -S ucrt64/mingw-w64-ucrt-x86_64-adwaita-icon-theme + pacman -S mingw64/mingw-w64-x86_64-adwaita-icon-theme + ``` + +3) Install python dependencies (you may try with venv) + ``` + pip install mpv + pip install requests + pip install setproctitle + pip install unidecode + ``` + > **Note:** ```pip install IMDbPY``` is removed for now due to build issues and related features are commented out. + +4) Now running the hypnotix.py should launch the app + ```python3 usr/lib/hypnotix/hypnotix.py``` -- libxapp 2.6+ -- libmpv -- python3-imdbpy (for Older Mint and Debian releases get it from https://packages.ubuntu.com/focal/all/python3-imdbpy/download) -- circle-flags (https://github.com/linuxmint/circle-flags) # TV Channels and media content -Hypnotix does not provide content or TV channels, it is a player application which streams from IPTV providers. +Hypnotix for Windows or Hypnotix does not provide content or TV channels, it is a player application which streams from IPTV providers. -By default, Hypnotix is configured with one IPTV provider called Free-TV: https://github.com/Free-TV/IPTV. +By default, Hypnotix for Windows is configured with an IPTV provider called Free-TV: https://github.com/Free-TV/IPTV. This provider was chosen because it satisfied the following criterias: @@ -38,14 +112,4 @@ This provider was chosen because it satisfied the following criterias: Issues relating to TV channels and media content should be addressed directly to the relevant provider. -Note: Feel free to remove Free-TV from Hypnotix if you don't use it, or add any other provider you may have access to or local M3U playlists. - -# Wayland compatibility - -If you're using Wayland go the Hypnotix preferences and add the following to the list of MPV options: - -`vo=x11` - -Run Hypnotix with: - -`GDK_BACKEND=x11 hypnotix` +Note: Feel free to remove Free-TV from Hypnotix if you don't use it, or add any other provider you may have access to or local M3U playlists. \ No newline at end of file From 3a8bbcde3acdd1bdf18088f186ab1707d8057fd6 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Wed, 4 Dec 2024 04:33:35 +0000 Subject: [PATCH 09/11] Added image to README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 965b24a4..281fce4b 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ I have only an initial build running in the crudest way possible and I have plan - Movie and Series modules are not tested and are not a priority. - There could be other issues +![shadow](https://github.com/user-attachments/assets/9735d7a2-7867-48c4-aa80-8b024c8488d8) + # License - Code: GPLv3 From de012141101f1954423be9ade4274b9d11012994 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Mon, 15 Sep 2025 00:05:34 +0530 Subject: [PATCH 10/11] Fixed issues with IMDBPy and wrong on_volume_prep() merge --- usr/lib/hypnotix/hypnotix.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index 102d883a..b42535ec 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -35,7 +35,7 @@ import mpv import requests import setproctitle -from imdb import Cinemagoer +#from imdb import Cinemagoer from unidecode import unidecode from common import Manager, Provider, Channel, MOVIES_GROUP, PROVIDERS_PATH, SERIES_GROUP, TV_GROUP,\ @@ -155,7 +155,7 @@ def __init__(self, application): self.latest_search_bar_text = None self.visible_search_results = 0 self.mpv = None - self.ia = IMDb() + #self.ia = IMDb() self.page_is_loading = False # used to ignore signals while we set widget states self.video_properties = {} self.audio_properties = {} @@ -1677,8 +1677,8 @@ def reinit_mpv(self): input_default_bindings=True, input_vo_keyboard=True, osc=osc, - ytdl=True, - wid=str(self.mpv_drawing_area.get_window().get_xid()) + ytdl=True + #wid=str(wid_tmp) ) self.mpv.volume = self.volume @@ -1743,6 +1743,9 @@ def on_fullscreen_button_clicked(self, widget): def on_close_info_window_button_clicked(self, widget): self.info_window.hide() + def on_volume_prop(self, name, value ): + self.volume = value + def compile_gsettings_schema(schema_dir): # Compile the GSettings schemas try: @@ -1755,9 +1758,6 @@ def set_gsettings_schema_dir(schema_dir): # Set the GSETTINGS_SCHEMA_DIR environment variable os.environ['GSETTINGS_SCHEMA_DIR'] = schema_dir #print(f"GSETTINGS_SCHEMA_DIR set to: {schema_dir}") - -def on_volume_prop(self, name, value ): - self.volume = value if __name__ == "__main__": schema_directory = "usr/share/glib-2.0/schemas/" From ab7e477272dd073d1cdafadacf1cd14c2f8f3d79 Mon Sep 17 00:00:00 2001 From: lakshminarayananb <> Date: Mon, 15 Sep 2025 00:36:16 +0530 Subject: [PATCH 11/11] sidebar toggle feature --- usr/lib/hypnotix/hypnotix.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/usr/lib/hypnotix/hypnotix.py b/usr/lib/hypnotix/hypnotix.py index b42535ec..a10252d9 100755 --- a/usr/lib/hypnotix/hypnotix.py +++ b/usr/lib/hypnotix/hypnotix.py @@ -1499,6 +1499,11 @@ def on_key_press_event(self, widget, event): self.on_prev_channel() elif event.keyval == Gdk.KEY_Right: self.on_next_channel() + #sidebar toggle + elif event.keyval == Gdk.KEY_s: + self.toggle_sidebar_visibility() + elif event.keyval == Gdk.KEY_h: + self.toggle_header_visibility() # elif event.keyval == Gdk.KEY_Up: # # Up of in the list # pass @@ -1746,6 +1751,23 @@ def on_close_info_window_button_clicked(self, widget): def on_volume_prop(self, name, value ): self.volume = value + #sidebar toggle + def toggle_sidebar_visibility(self): + self.sidebar_visible = not self.sidebar_visible + if not self.sidebar_visible: + self.sidebar.hide() + else: + self.sidebar.show() + + def toggle_header_visibility(self): + self.header_visible = not self.header_visible + if not self.header_visible: + self.headerbar.hide() + self.mpv_top_box.hide() + else: + self.headerbar.show() + self.mpv_top_box.show() + def compile_gsettings_schema(schema_dir): # Compile the GSettings schemas try: