Najeebsk

IPTV-ALL-XTREAM-DB-RAR.pyw

Apr 8th, 2026
123
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 52.07 KB | None | 0 0
  1. # -*- coding: utf-8 -*-
  2. """
  3. NAJEEB IPTV PRO MAX - ULTRA EDITION 🔥 [FIXED v2.0]
  4. ✓ Fullscreen FIXED (Windows/Linux/macOS) - ESC to exit
  5. ✓ Mini Player Toggle - Channel list restores properly
  6. ✓ Fixed ValueError unpacking error
  7. ✓ Azure Theme Dark/Light Toggle
  8. ✓ Favorites System with persistent save
  9. ✓ M3U Export/Import
  10. ✓ Archive Support (RAR/ZIP) with cleanup
  11. ✓ Xtream Codes API Full Support
  12. ✓ SQLite Database Save/Load with indexes
  13. ✓ Image Preview Mode for local/URL images
  14. ✓ Python 3.7+ Compatible
  15. ✓ Proper VLC re-embedding on mode changes
  16. """
  17.  
  18. import tkinter as tk
  19. from tkinter import ttk, filedialog, messagebox
  20. from PIL import Image, ImageTk
  21. import vlc
  22. import requests
  23. import os
  24. import threading
  25. import re
  26. import platform
  27. import time
  28. import json
  29. import sqlite3
  30. import tempfile
  31. import shutil
  32. import rarfile
  33. import zipfile
  34. from urllib.parse import urlparse, parse_qs
  35.  
  36. # Try import ttkthemes for Azure theme support
  37. try:
  38.     from ttkthemes import ThemedStyle
  39.     HAS_TTKTHEMES = True
  40. except ImportError:
  41.     HAS_TTKTHEMES = False
  42.  
  43.  
  44. class IPTVPro:
  45.     def __init__(self, root):
  46.         self.root = root
  47.         self.root.title("NAJEEB IPTV PRO MAX - ULTRA EDITION")
  48.         self.root.geometry("1280x720")
  49.         self.root.minsize(900, 550)
  50.  
  51.         # VLC Instance
  52.         self.instance = vlc.Instance("--no-video-title-show", "--quiet",
  53.                                      "--network-caching=1500", "--avcodec-hw=any")
  54.         self.player = self.instance.media_player_new()
  55.  
  56.         # Data Structure: (name, url, category, media_type)
  57.         self.channels = []
  58.         self.filtered = []
  59.         self.current_idx = -1
  60.         self.is_list_visible = True
  61.         self.seeking = False
  62.         self.update_id = None
  63.         self.temp_extract_dir = None
  64.         self.current_image = None
  65.         self.db_path = "Video.db"
  66.         self.theme_mode = "dark"
  67.         self.is_fullscreen = False
  68.         self.is_mini_mode = False
  69.        
  70.         # State preservation for UI restoration
  71.         self._original_geometry = None
  72.         self._sidebar_state_before_fullscreen = True
  73.         self._sidebar_state_before_mini = True
  74.         self._was_list_visible = True
  75.        
  76.         # Favorites System
  77.         self.favorites = set()
  78.         self.show_favorites_only = False
  79.  
  80.         # Xtream Config
  81.         self.xtream = {"host": "", "user": "", "pass": ""}
  82.  
  83.         self._apply_theme()
  84.         self._setup_ui()
  85.         self._setup_vlc()
  86.         self._start_update_loop()
  87.         self._bind_shortcuts()
  88.         self._load_config()
  89.         self._init_database()
  90.  
  91.         self.root.protocol("WM_DELETE_WINDOW", self._cleanup)
  92.  
  93.     def _apply_theme(self):
  94.         """Apply theme with Azure support if available"""
  95.         style = ttk.Style()
  96.        
  97.         if HAS_TTKTHEMES:
  98.             try:
  99.                 style = ThemedStyle(self.root)
  100.                 style.set_theme("azure")
  101.             except:
  102.                 style.theme_use("clam")
  103.         else:
  104.             style.theme_use("clam")
  105.        
  106.         self._set_dark_style(style)
  107.  
  108.     def _set_dark_style(self, style):
  109.         """Apply dark theme colors"""
  110.         style.configure(".", background="#181818", foreground="#e0e0e0", font=("Segoe UI", 10))
  111.         style.configure("TButton", padding=6, background="#2d2d2d", foreground="#ffffff", relief="flat")
  112.         style.map("TButton", background=[("active", "#0078d7")], foreground=[("active", "#ffffff")])
  113.         style.configure("TEntry", fieldbackground="#252525", foreground="#ffffff", borderwidth=1)
  114.         style.configure("TFrame", background="#181818")
  115.         style.configure("TLabel", background="#181818", foreground="#b0b0b0")
  116.         style.configure("TScale", background="#181818", troughcolor="#3a3a3a")
  117.         style.configure("Treeview", background="#252525", foreground="#ffffff",
  118.                        fieldbackground="#252525", rowheight=28)
  119.         style.configure("Treeview.Heading", background="#2d2d2d", foreground="#ffffff", relief="flat")
  120.         style.map("Treeview.Heading", background=[("active", "#0078d7")])
  121.         style.configure("Horizontal.TScrollbar", background="#2d2d2d", troughcolor="#181818")
  122.         style.configure("Vertical.TScrollbar", background="#2d2d2d", troughcolor="#181818")
  123.        
  124.         if hasattr(self, 'listbox'):
  125.             self.listbox.config(bg="#202020", fg="#e0e0e0", selectbackground="#0078d7",
  126.                                selectforeground="#ffffff", font=("Consolas", 9), bd=0)
  127.  
  128.     def _set_light_style(self, style):
  129.         """Apply light theme colors"""
  130.         style.configure(".", background="#f0f0f0", foreground="#1a1a1a", font=("Segoe UI", 10))
  131.         style.configure("TButton", padding=6, background="#e0e0e0", foreground="#1a1a1a", relief="flat")
  132.         style.map("TButton", background=[("active", "#0078d7")], foreground=[("active", "#ffffff")])
  133.         style.configure("TEntry", fieldbackground="#ffffff", foreground="#1a1a1a", borderwidth=1)
  134.         style.configure("TFrame", background="#f0f0f0")
  135.         style.configure("TLabel", background="#f0f0f0", foreground="#333333")
  136.         style.configure("TScale", background="#f0f0f0", troughcolor="#cccccc")
  137.         style.configure("Treeview", background="#ffffff", foreground="#1a1a1a",
  138.                        fieldbackground="#ffffff", rowheight=28)
  139.         style.configure("Treeview.Heading", background="#e0e0e0", foreground="#1a1a1a", relief="flat")
  140.         style.map("Treeview.Heading", background=[("active", "#0078d7")])
  141.         style.configure("Horizontal.TScrollbar", background="#e0e0e0", troughcolor="#f0f0f0")
  142.         style.configure("Vertical.TScrollbar", background="#e0e0e0", troughcolor="#f0f0f0")
  143.        
  144.         if hasattr(self, 'listbox'):
  145.             self.listbox.config(bg="#ffffff", fg="#1a1a1a", selectbackground="#0078d7",
  146.                                selectforeground="#ffffff", font=("Consolas", 9), bd=0)
  147.  
  148.     def toggle_theme(self):
  149.         """Toggle between dark and light theme"""
  150.         style = ttk.Style()
  151.         if self.theme_mode == "dark":
  152.             self._set_light_style(style)
  153.             self.theme_mode = "light"
  154.             self.status_var.set("☀️ Light theme activated")
  155.         else:
  156.             self._set_dark_style(style)
  157.             self.theme_mode = "dark"
  158.             self.status_var.set("🌙 Dark theme activated")
  159.  
  160.     def _setup_ui(self):
  161.         main = ttk.Frame(self.root, padding=8)
  162.         main.pack(fill="both", expand=True)
  163.  
  164.         # ===== TOP BAR =====
  165.         self.top_bar = ttk.Frame(main)
  166.         self.top_bar.pack(fill="x", pady=(0, 6))
  167.  
  168.         ttk.Label(self.top_bar, text="📡 Xtream:").pack(side="left", padx=(0, 4))
  169.         self.x_host = ttk.Entry(self.top_bar, width=18)
  170.         self.x_host.pack(side="left", padx=2)
  171.         self.x_user = ttk.Entry(self.top_bar, width=10)
  172.         self.x_user.pack(side="left", padx=2)
  173.         self.x_pass = ttk.Entry(self.top_bar, width=10, show="*")
  174.         self.x_pass.pack(side="left", padx=2)
  175.         ttk.Button(self.top_bar, text="🔑 Login", command=self.xtream_login).pack(side="left", padx=4)
  176.  
  177.         ttk.Separator(self.top_bar, orient="vertical").pack(side="left", fill="y", padx=10)
  178.  
  179.         ttk.Button(self.top_bar, text="📂 M3U File", command=self.load_file).pack(side="left", padx=2)
  180.         ttk.Button(self.top_bar, text="🌐 M3U URL", command=self.load_url_dialog).pack(side="left", padx=2)
  181.         ttk.Button(self.top_bar, text="📁 Local Media", command=self.load_folder).pack(side="left", padx=2)
  182.         ttk.Button(self.top_bar, text="🗜️ Archive", command=self.load_archive).pack(side="left", padx=2)
  183.         ttk.Button(self.top_bar, text="🗄️ Load DB", command=self.load_database).pack(side="left", padx=2)
  184.         ttk.Button(self.top_bar, text="💾 Save DB", command=self.save_database).pack(side="left", padx=2)
  185.         ttk.Button(self.top_bar, text="📤 Export", command=self.export_playlist).pack(side="left", padx=2)
  186.  
  187.         # Theme toggle & sidebar toggle on right
  188.         ttk.Button(self.top_bar, text="🎨 Theme", command=self.toggle_theme).pack(side="right", padx=2)
  189.         self.toggle_btn = ttk.Button(self.top_bar, text="☰ Hide List", command=self.toggle_sidebar)
  190.         self.toggle_btn.pack(side="right")
  191.  
  192.         # ===== MAIN CONTENT =====
  193.         self.content_frame = ttk.Frame(main)
  194.         self.content_frame.pack(fill="both", expand=True, pady=5)
  195.  
  196.         # Left Panel (Channel List)
  197.         self.left_panel = ttk.Frame(self.content_frame, width=280)
  198.         self.left_panel.pack(side="left", fill="y", padx=(0, 5))
  199.         self.left_panel.pack_propagate(False)
  200.  
  201.         # Search
  202.         ttk.Label(self.left_panel, text="🔍 Search:").pack(anchor="w", padx=6, pady=(6,2))
  203.         self.search_var = tk.StringVar()
  204.         self.search_var.trace("w", lambda *_: self.filter_channels())
  205.         ttk.Entry(self.left_panel, textvariable=self.search_var).pack(fill="x", padx=6, pady=2)
  206.  
  207.         # Favorites filter
  208.         fav_frame = ttk.Frame(self.left_panel)
  209.         fav_frame.pack(fill="x", padx=6, pady=2)
  210.         ttk.Button(fav_frame, text="⭐ Favorites", command=self.toggle_favorites_filter, width=12).pack(side="left")
  211.         ttk.Button(fav_frame, text="❤️ Toggle", command=self.toggle_favorite, width=10).pack(side="right")
  212.  
  213.         # Category Tree
  214.         ttk.Label(self.left_panel, text="📁 Categories:").pack(anchor="w", padx=6, pady=(6,2))
  215.         self.cat_tree = ttk.Treeview(self.left_panel, columns=("name",), show="headings", height=6)
  216.         self.cat_tree.heading("name", text="Category")
  217.         self.cat_tree.column("name", width=240)
  218.         self.cat_tree.pack(fill="x", padx=6, pady=2)
  219.         self.cat_tree.bind("<<TreeviewSelect>>", self.on_cat_select)
  220.  
  221.         # Channel List
  222.         ttk.Label(self.left_panel, text="📺 Channels:").pack(anchor="w", padx=6, pady=(6,2))
  223.         self.listbox = tk.Listbox(self.left_panel, bg="#202020", fg="#e0e0e0", selectbackground="#0078d7",
  224.                                   selectforeground="#fff", font=("Consolas", 9), bd=0)
  225.         self.listbox.pack(fill="both", expand=True, padx=6, pady=2)
  226.         self.listbox.bind("<<ListboxSelect>>", self.play_selected)
  227.         self.listbox.bind("<Double-1>", lambda e: self.play_selected())
  228.  
  229.         # Right Panel: Video/Image Display
  230.         self.video_frame = ttk.Frame(self.content_frame, relief="solid", borderwidth=1)
  231.         self.video_frame.pack(side="right", fill="both", expand=True)
  232.        
  233.         self.image_label = ttk.Label(self.video_frame, background="#000")
  234.         self.image_label.pack(fill="both", expand=True)
  235.         self.image_label.pack_forget()
  236.  
  237.         # ===== BOTTOM CONTROLS =====
  238.         self.ctrl_frame = ttk.Frame(main)
  239.         self.ctrl_frame.pack(fill="x", pady=(5, 0))
  240.  
  241.         seek_f = ttk.Frame(self.ctrl_frame)
  242.         seek_f.pack(fill="x", pady=(0, 5))
  243.         self.time_lbl = ttk.Label(seek_f, text="00:00:00 / 00:00:00", width=20)
  244.         self.time_lbl.pack(side="left", padx=6)
  245.         self.seek_bar = ttk.Scale(seek_f, from_=0, to=100, orient="horizontal")
  246.         self.seek_bar.pack(side="left", fill="x", expand=True, padx=6)
  247.         self.seek_bar.bind("<ButtonPress-1>", lambda e: setattr(self, "seeking", True))
  248.         self.seek_bar.bind("<ButtonRelease-1>", self.end_seek)
  249.  
  250.         play_f = ttk.Frame(self.ctrl_frame)
  251.         play_f.pack(fill="x")
  252.         ttk.Button(play_f, text="⏮ Prev", command=self.prev).pack(side="left", padx=3)
  253.         self.play_btn = ttk.Button(play_f, text="▶ Play", width=6, command=self.toggle_play)
  254.         self.play_btn.pack(side="left", padx=3)
  255.         ttk.Button(play_f, text="⏹ Stop", command=self.stop).pack(side="left", padx=3)
  256.         ttk.Button(play_f, text="⏭ Next", command=self.next).pack(side="left", padx=3)
  257.         ttk.Separator(play_f, orient="vertical").pack(side="left", fill="y", padx=12)
  258.         ttk.Button(play_f, text="📸 Snapshot", command=self.snapshot).pack(side="left", padx=3)
  259.         ttk.Button(play_f, text="⛶ Fullscreen", command=self.toggle_fullscreen).pack(side="left", padx=3)
  260.         ttk.Button(play_f, text="🖼️ Image", command=self.toggle_image_mode).pack(side="left", padx=3)
  261.         ttk.Button(play_f, text="🔲 Mini", command=self.toggle_mini_player).pack(side="left", padx=3)
  262.  
  263.         vol_f = ttk.Frame(play_f)
  264.         vol_f.pack(side="right")
  265.         self.vol_var = tk.IntVar(value=75)
  266.         ttk.Label(vol_f, text="🔊").pack(side="left")
  267.         ttk.Scale(vol_f, from_=0, to=100, variable=self.vol_var, orient="horizontal",
  268.                  length=100, command=self.set_vol).pack(side="left", padx=4)
  269.  
  270.         self.status_var = tk.StringVar(value="✅ Ready | Load M3U, Archive, DB or Folder")
  271.         ttk.Label(play_f, textvariable=self.status_var, anchor="e", foreground="#4caf50").pack(
  272.             side="right", fill="x", expand=True, padx=6)
  273.  
  274.     def _setup_vlc(self):
  275.         """Setup VLC embedding for current platform - SAFE re-embed"""
  276.         try:
  277.             sys_name = platform.system()
  278.             if sys_name == "Windows":
  279.                 hwnd = self.video_frame.winfo_id()
  280.                 if hwnd:
  281.                     self.player.set_hwnd(hwnd)
  282.             elif sys_name == "Linux":
  283.                 xid = self.video_frame.winfo_id()
  284.                 if xid:
  285.                     self.player.set_xwindow(xid)
  286.             elif sys_name == "Darwin":
  287.                 nsobj = int(self.video_frame.winfo_id())
  288.                 if nsobj:
  289.                     self.player.set_nsobject(nsobj)
  290.         except Exception as e:
  291.             print(f"⚠️ VLC Embed Warning: {e}")
  292.  
  293.     def _init_database(self):
  294.         """Initialize SQLite database for storing media"""
  295.         try:
  296.             conn = sqlite3.connect(self.db_path)
  297.             cursor = conn.cursor()
  298.             cursor.execute('''
  299.                CREATE TABLE IF NOT EXISTS media (
  300.                    id INTEGER PRIMARY KEY AUTOINCREMENT,
  301.                    name TEXT NOT NULL,
  302.                    url TEXT NOT NULL,
  303.                    category TEXT DEFAULT 'Uncategorized',
  304.                    media_type TEXT DEFAULT 'VIDEO',
  305.                    is_image BOOLEAN DEFAULT 0,
  306.                    added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  307.                    UNIQUE(name, url)
  308.                )
  309.            ''')
  310.             cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON media(category)')
  311.             cursor.execute('CREATE INDEX IF NOT EXISTS idx_type ON media(media_type)')
  312.             conn.commit()
  313.             conn.close()
  314.         except Exception as e:
  315.             print(f"⚠️ Database init error: {e}")
  316.  
  317.     def _start_update_loop(self):
  318.         self.update_ui()
  319.  
  320.     def update_ui(self):
  321.         if self.player and self.player.is_playing() and not self.seeking and not self.current_image:
  322.             t = self.player.get_time()
  323.             d = self.player.get_length()
  324.             if d > 0 and d != -1:
  325.                 self.seek_bar.config(to=d)
  326.                 self.seek_bar.set(t)
  327.                 self.time_lbl.config(text=f"{self._fmt(t)} / {self._fmt(d)}")
  328.             else:
  329.                 self.time_lbl.config(text=f"{self._fmt(t)} / LIVE")
  330.  
  331.             st = self.player.get_state()
  332.             if st == vlc.State.Playing:
  333.                 name = self.channels[self.current_idx][0] if 0 <= self.current_idx < len(self.channels) else "Playing"
  334.                 self.status_var.set(f"▶ {name}")
  335.                 self.play_btn.config(text="⏸ Pause")
  336.             elif st == vlc.State.Paused:
  337.                 self.status_var.set("⏸ Paused")
  338.                 self.play_btn.config(text="▶ Play")
  339.  
  340.         self.update_id = self.root.after(500, self.update_ui)
  341.  
  342.     @staticmethod
  343.     def _fmt(ms):
  344.         if ms < 0:
  345.             return "00:00:00"
  346.         s = max(0, int(ms)//1000)
  347.         return time.strftime("%H:%M:%S", time.gmtime(s))
  348.  
  349.     # ================= FULLSCREEN (COMPLETELY FIXED) =================
  350.     def toggle_fullscreen(self):
  351.         """✅ COMPLETE FIX: Toggle fullscreen with proper VLC re-embed & UI restoration"""
  352.         try:
  353.             if self.is_fullscreen:
  354.                 # === EXIT FULLSCREEN ===
  355.                 self.root.attributes('-fullscreen', False)
  356.                 if platform.system() == "Darwin":
  357.                     self.root.attributes('-zoomed', False)
  358.                
  359.                 # Restore original geometry
  360.                 if self._original_geometry:
  361.                     self.root.geometry(self._original_geometry)
  362.                
  363.                 # Restore UI elements
  364.                 self.top_bar.pack(fill="x", pady=(0, 6))
  365.                 self.content_frame.pack(fill="both", expand=True, pady=5)
  366.                 self.ctrl_frame.pack(fill="x", pady=(5, 0))
  367.                
  368.                 # Restore sidebar based on saved state
  369.                 if self._sidebar_state_before_fullscreen and not self.is_list_visible:
  370.                     self.left_panel.pack(side="left", fill="y", padx=(0, 5))
  371.                     self.is_list_visible = True
  372.                     self.toggle_btn.config(text="☰ Hide List")
  373.                 elif not self._sidebar_state_before_fullscreen:
  374.                     self.left_panel.pack_forget()
  375.                     self.is_list_visible = False
  376.                     self.toggle_btn.config(text="☰ Show List")
  377.                
  378.                 self.is_fullscreen = False
  379.                 self.status_var.set("🖥️ Windowed mode")
  380.                
  381.                 # Re-embed VLC properly
  382.                 self.root.update_idletasks()
  383.                 self._setup_vlc()
  384.                
  385.             else:
  386.                 # === ENTER FULLSCREEN ===
  387.                 # Save current state
  388.                 self._original_geometry = self.root.geometry()
  389.                 self._sidebar_state_before_fullscreen = self.is_list_visible
  390.                
  391.                 # Hide UI elements for clean fullscreen
  392.                 self.top_bar.pack_forget()
  393.                 self.ctrl_frame.pack_forget()
  394.                
  395.                 # Handle sidebar
  396.                 if self.is_list_visible:
  397.                     self.left_panel.pack_forget()
  398.                     self.is_list_visible = False
  399.                
  400.                 # Enter fullscreen
  401.                 self.root.attributes('-fullscreen', True)
  402.                 if platform.system() == "Darwin":
  403.                     self.root.attributes('-zoomed', True)
  404.                
  405.                 self.is_fullscreen = True
  406.                 self.status_var.set("⛶ Fullscreen (Press ESC to exit)")
  407.                
  408.                 # Re-embed VLC for fullscreen
  409.                 self.root.update_idletasks()
  410.                 self._setup_vlc()
  411.                
  412.         except Exception as e:
  413.             print(f"⚠️ Fullscreen error: {e}")
  414.             self.status_var.set(f"❌ Fullscreen error: {e}")
  415.  
  416.     def _exit_fullscreen(self, event=None):
  417.         """Exit fullscreen when ESC is pressed"""
  418.         if self.is_fullscreen:
  419.             self.toggle_fullscreen()
  420.             return "break"
  421.  
  422.     # ================= MINI PLAYER (FIXED TOGGLE) =================
  423.     def toggle_mini_player(self):
  424.         """✅ FIXED: Toggle mini player with proper channel list restoration"""
  425.         try:
  426.             if self.is_mini_mode:
  427.                 # === EXIT MINI MODE ===
  428.                 # Restore original geometry
  429.                 if hasattr(self, '_original_geometry_mini') and self._original_geometry_mini:
  430.                     self.root.geometry(self._original_geometry_mini)
  431.                
  432.                 # Restore UI elements
  433.                 self.top_bar.pack(fill="x", pady=(0, 6))
  434.                 self.content_frame.pack(fill="both", expand=True, pady=5)
  435.                 self.ctrl_frame.pack(fill="x", pady=(5, 0))
  436.                
  437.                 # Restore sidebar based on saved state
  438.                 if self._sidebar_state_before_mini:
  439.                     self.left_panel.pack(side="left", fill="y", padx=(0, 5))
  440.                     self.is_list_visible = True
  441.                     self.toggle_btn.config(text="☰ Hide List")
  442.                 else:
  443.                     self.left_panel.pack_forget()
  444.                     self.is_list_visible = False
  445.                     self.toggle_btn.config(text="☰ Show List")
  446.                
  447.                 self.is_mini_mode = False
  448.                 self.status_var.set("🖥️ Full mode restored")
  449.                
  450.                 # Re-embed VLC
  451.                 self.root.update_idletasks()
  452.                 self._setup_vlc()
  453.                
  454.             else:
  455.                 # === ENTER MINI MODE ===
  456.                 # Save current state
  457.                 self._original_geometry_mini = self.root.geometry()
  458.                 self._sidebar_state_before_mini = self.is_list_visible
  459.                
  460.                 # Hide UI elements for compact view
  461.                 self.top_bar.pack_forget()
  462.                 self.ctrl_frame.pack_forget()
  463.                
  464.                 # Hide sidebar for mini mode (always)
  465.                 if self.is_list_visible:
  466.                     self.left_panel.pack_forget()
  467.                     self.is_list_visible = False
  468.                
  469.                 # Set mini window size
  470.                 self.root.geometry("500x400")
  471.                 self.root.minsize(400, 300)
  472.                
  473.                 self.is_mini_mode = True
  474.                 self.status_var.set("🔲 Mini mode - Press F11 or 🔲 to restore")
  475.                
  476.                 # Re-embed VLC for mini mode
  477.                 self.root.update_idletasks()
  478.                 self._setup_vlc()
  479.                
  480.         except Exception as e:
  481.             print(f"⚠️ Mini player error: {e}")
  482.             self.status_var.set(f"❌ Mini error: {e}")
  483.  
  484.     # ================= IMAGE PREVIEW =================
  485.     def toggle_image_mode(self):
  486.         """Toggle between video player and image preview mode"""
  487.         if self.current_image:
  488.             self.image_label.pack_forget()
  489.             self.video_frame.pack(side="right", fill="both", expand=True)
  490.             self.current_image = None
  491.             self.status_var.set("🎬 Video mode")
  492.             self._setup_vlc()  # Re-embed VLC
  493.         else:
  494.             if self.current_idx >= 0 and self.current_idx < len(self.channels):
  495.                 item = self.channels[self.current_idx]
  496.                 if item[3] == "IMAGE":
  497.                     self.video_frame.pack_forget()
  498.                     self.image_label.pack(fill="both", expand=True)
  499.                     self._show_image(item[1])
  500.  
  501.     def _show_image(self, image_path):
  502.         """Display image in preview mode"""
  503.         try:
  504.             img = Image.open(image_path)
  505.             window_w = max(400, self.root.winfo_width() - 320)
  506.             window_h = max(300, self.root.winfo_height() - 120)
  507.             img.thumbnail((window_w, window_h), Image.Resampling.LANCZOS)
  508.            
  509.             self.current_image = ImageTk.PhotoImage(img)
  510.             self.image_label.config(image=self.current_image, text="")
  511.             self.status_var.set(f"🖼️ {os.path.basename(image_path)}")
  512.         except Exception as e:
  513.             messagebox.showerror("❌ Image Error", f"Failed to load image:\n{e}")
  514.  
  515.     # ================= FAVORITES SYSTEM =================
  516.     def toggle_favorite(self):
  517.         """Toggle current channel as favorite"""
  518.         if self.current_idx < 0 or self.current_idx >= len(self.channels):
  519.             return
  520.         item = self.channels[self.current_idx]
  521.         url = item[1]
  522.         if url in self.favorites:
  523.             self.favorites.remove(url)
  524.             self.status_var.set("❤️ Removed from favorites")
  525.         else:
  526.             self.favorites.add(url)
  527.             self.status_var.set("⭐ Added to favorites")
  528.         self.refresh_list()
  529.  
  530.     def toggle_favorites_filter(self):
  531.         """Show only favorite channels"""
  532.         self.show_favorites_only = not self.show_favorites_only
  533.         if self.show_favorites_only:
  534.             self.filtered = [ch for ch in self.channels if ch[1] in self.favorites]
  535.             self.status_var.set(f"⭐ Favorites only ({len(self.filtered)} items)")
  536.         else:
  537.             self.filtered = self.channels.copy()
  538.             self.status_var.set(f"📺 All channels ({len(self.filtered)} items)")
  539.         self.refresh_list()
  540.  
  541.     # ================= DATABASE SAVE/LOAD =================
  542.     def save_database(self):
  543.         """Save current channel list to SQLite database"""
  544.         path = filedialog.asksaveasfilename(
  545.             defaultextension=".db",
  546.             filetypes=[("Database Files", "*.db *.sqlite"), ("All Files", "*.*")],
  547.             initialfile="Video.db"
  548.         )
  549.         if not path:
  550.             return
  551.        
  552.         self.status_var.set("⏳ Saving to database...")
  553.         threading.Thread(target=self._save_to_db, args=(path,), daemon=True).start()
  554.  
  555.     def _save_to_db(self, db_path):
  556.         try:
  557.             conn = sqlite3.connect(db_path)
  558.             cursor = conn.cursor()
  559.             saved = 0
  560.            
  561.             for name, url, category, media_type in self.channels:
  562.                 is_image = 1 if media_type == "IMAGE" else 0
  563.                 try:
  564.                     cursor.execute('''
  565.                        INSERT OR REPLACE INTO media (name, url, category, media_type, is_image)
  566.                        VALUES (?, ?, ?, ?, ?)
  567.                    ''', (name, url, category, media_type, is_image))
  568.                     saved += 1
  569.                 except:
  570.                     continue
  571.            
  572.             conn.commit()
  573.             conn.close()
  574.             self.db_path = db_path
  575.             self.root.after(0, lambda: messagebox.showinfo("✅ Saved", f"{saved} entries saved to:\n{db_path}"))
  576.             self.root.after(0, lambda: self.status_var.set(f"💾 Saved {saved} entries"))
  577.         except Exception as e:
  578.             self.root.after(0, lambda: messagebox.showerror("❌ Error", f"Failed to save database:\n{e}"))
  579.  
  580.     def load_database(self):
  581.         """Load media entries from SQLite database"""
  582.         path = filedialog.askopenfilename(
  583.             filetypes=[("Database Files", "*.db *.sqlite *.sqlite3"), ("All Files", "*.*")]
  584.         )
  585.         if path:
  586.             self.db_path = path
  587.             self.status_var.set(f"⏳ Loading: {os.path.basename(path)}...")
  588.             threading.Thread(target=self._load_from_db, args=(path,), daemon=True).start()
  589.  
  590.     def _load_from_db(self, db_path):
  591.         try:
  592.             conn = sqlite3.connect(db_path)
  593.             cursor = conn.cursor()
  594.             cursor.execute("SELECT name, url, category, media_type, is_image FROM media")
  595.             rows = cursor.fetchall()
  596.             conn.close()
  597.            
  598.             media_files = []
  599.             for name, url, category, media_type, is_image in rows:
  600.                 if not url.startswith(('http://', 'https://', 'rtmp://', 'rtsp://')):
  601.                     if not os.path.exists(url):
  602.                         continue
  603.                 media_files.append((name, url, category, media_type))
  604.            
  605.             if media_files:
  606.                 self.channels = media_files
  607.                 self.filtered = self.channels.copy()
  608.                 self.root.after(0, self._finish_load)
  609.                 self.root.after(0, lambda: self.status_var.set(f"✅ Loaded {len(media_files)} entries from DB"))
  610.             else:
  611.                 self.root.after(0, lambda: messagebox.showinfo("ℹ️ Info", "No valid entries found in database"))
  612.         except Exception as e:
  613.             self.root.after(0, lambda: messagebox.showerror("❌ Error", f"Failed to load database:\n{e}"))
  614.  
  615.     # ================= PLAYLIST EXPORT =================
  616.     def export_playlist(self):
  617.         """Export current channels to M3U file"""
  618.         path = filedialog.asksaveasfilename(
  619.             defaultextension=".m3u",
  620.             filetypes=[("M3U Playlist", "*.m3u")],
  621.             initialfile="my_playlist.m3u"
  622.         )
  623.         if not path:
  624.             return
  625.         try:
  626.             with open(path, "w", encoding="utf-8") as f:
  627.                 f.write("#EXTM3U\n")
  628.                 for name, url, category, media_type in self.channels:
  629.                     f.write(f'#EXTINF:-1 group-title="{category}",{name}\n{url}\n')
  630.             messagebox.showinfo("✅ Exported", f"Playlist saved to:\n{path}")
  631.             self.status_var.set(f"📤 Exported {len(self.channels)} items")
  632.         except Exception as e:
  633.             messagebox.showerror("❌ Error", f"Export failed:\n{e}")
  634.  
  635.     # ================= XTREAM & M3U URL SUPPORT =================
  636.     def load_url_dialog(self):
  637.         """Dialog for loading M3U/Xtream URLs with presets"""
  638.         dialog = tk.Toplevel(self.root)
  639.         dialog.title("🌐 Load M3U/Xtream URL")
  640.         dialog.geometry("600x300")
  641.         dialog.transient(self.root)
  642.         dialog.grab_set()
  643.        
  644.         ttk.Label(dialog, text="Enter M3U or Xtream URL:").pack(pady=10)
  645.        
  646.         url_entry = ttk.Entry(dialog, width=70)
  647.         url_entry.pack(pady=5)
  648.        
  649.         presets = [
  650.             "https://iptv-org.github.io/iptv/index.m3u",
  651.             "https://iptv-org.github.io/iptv/categories/entertainment.m3u",
  652.         ]
  653.        
  654.         ttk.Label(dialog, text="📋 Quick Load Presets:").pack(pady=(15,5))
  655.         preset_frame = ttk.Frame(dialog)
  656.         preset_frame.pack(pady=5)
  657.        
  658.         for i, preset in enumerate(presets):
  659.             display = preset[:50] + "..." if len(preset) > 50 else preset
  660.             ttk.Button(preset_frame, text=f"{i+1}. {display}",
  661.                       command=lambda u=preset: self._load_url_from_dialog(u, dialog)).pack(fill="x", pady=2)
  662.        
  663.         ttk.Button(dialog, text="✅ Load URL",
  664.                   command=lambda: self._load_url_from_dialog(url_entry.get(), dialog)).pack(pady=15)
  665.        
  666.         url_entry.focus_set()
  667.         dialog.bind('<Return>', lambda e: self._load_url_from_dialog(url_entry.get(), dialog))
  668.  
  669.     def _load_url_from_dialog(self, url, dialog=None):
  670.         """Process URL from dialog"""
  671.         if dialog:
  672.             dialog.destroy()
  673.        
  674.         url = url.strip()
  675.         if not url:
  676.             messagebox.showwarning("⚠️ Warning", "Please enter a URL")
  677.             return
  678.        
  679.         # Auto-detect Xtream format
  680.         if 'get.php' in url and 'username=' in url and 'password=' in url:
  681.             parsed = urlparse(url)
  682.             params = parse_qs(parsed.query)
  683.             if 'username' in params and 'password' in params:
  684.                 self.xtream = {
  685.                     "host": f"{parsed.scheme}://{parsed.netloc}",
  686.                     "user": params['username'][0],
  687.                     "pass": params['password'][0]
  688.                 }
  689.                 self.x_host.delete(0, tk.END)
  690.                 self.x_host.insert(0, self.xtream["host"])
  691.                 self.x_user.delete(0, tk.END)
  692.                 self.x_user.insert(0, self.xtream["user"])
  693.                 self.x_pass.delete(0, tk.END)
  694.                 self.x_pass.insert(0, self.xtream["pass"])
  695.                 self.status_var.set("🔑 Xtream credentials detected")
  696.        
  697.         self.status_var.set("⏳ Downloading playlist...")
  698.         threading.Thread(target=self._parse_url, args=(url,), daemon=True).start()
  699.  
  700.     # ================= ARCHIVE (RAR/ZIP) =================
  701.     def load_archive(self):
  702.         path = filedialog.askopenfilename(filetypes=[
  703.             ("Archive Files", "*.rar *.zip *.RAR *.ZIP"),
  704.             ("All Files", "*.*")
  705.         ])
  706.         if path:
  707.             self.status_var.set(f"⏳ Extracting: {os.path.basename(path)}...")
  708.             threading.Thread(target=self._extract_archive, args=(path,), daemon=True).start()
  709.  
  710.     def _extract_archive(self, archive_path):
  711.         try:
  712.             if self.temp_extract_dir and os.path.exists(self.temp_extract_dir):
  713.                 shutil.rmtree(self.temp_extract_dir)
  714.             self.temp_extract_dir = tempfile.mkdtemp(prefix="iptv_extract_")
  715.            
  716.             media_files = []
  717.             video_exts = {'.mp4', '.mkv', '.avi', '.webm', '.m4v', '.mov', '.flv', '.wmv'}
  718.             audio_exts = {'.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma'}
  719.             image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'}
  720.             all_exts = video_exts | audio_exts | image_exts
  721.            
  722.             archive_lower = archive_path.lower()
  723.            
  724.             if archive_lower.endswith('.rar'):
  725.                 with rarfile.RarFile(archive_path, 'r') as rf:
  726.                     for member in rf.namelist():
  727.                         if any(member.lower().endswith(ext) for ext in all_exts):
  728.                             extracted = rf.extract(member, path=self.temp_extract_dir)
  729.                             filename = os.path.basename(member)
  730.                             ext = os.path.splitext(filename)[1].lower()
  731.                             mtype = "IMAGE" if ext in image_exts else "AUDIO" if ext in audio_exts else "VIDEO"
  732.                             media_files.append((filename, extracted, "Archive", mtype))
  733.             elif archive_lower.endswith('.zip'):
  734.                 with zipfile.ZipFile(archive_path, 'r') as zf:
  735.                     for member in zf.namelist():
  736.                         if any(member.lower().endswith(ext) for ext in all_exts):
  737.                             extracted = zf.extract(member, path=self.temp_extract_dir)
  738.                             filename = os.path.basename(member)
  739.                             ext = os.path.splitext(filename)[1].lower()
  740.                             mtype = "IMAGE" if ext in image_exts else "AUDIO" if ext in audio_exts else "VIDEO"
  741.                             media_files.append((filename, extracted, "Archive", mtype))
  742.            
  743.             if media_files:
  744.                 self.channels.extend(media_files)
  745.                 self.filtered = self.channels.copy()
  746.                 self.root.after(0, self._finish_load)
  747.                 self.root.after(0, lambda: self.status_var.set(f"✅ Extracted {len(media_files)} files"))
  748.             else:
  749.                 self.root.after(0, lambda: messagebox.showinfo("ℹ️ Info", "No media files found in archive"))
  750.         except Exception as e:
  751.             self.root.after(0, lambda: messagebox.showerror("❌ Error", f"Extraction failed:\n{e}"))
  752.  
  753.     # ================= FOLDER SCANNING =================
  754.     def load_folder(self):
  755.         folder = filedialog.askdirectory()
  756.         if not folder:
  757.             return
  758.         self.status_var.set("⏳ Scanning folder...")
  759.         threading.Thread(target=self._scan_folder, args=(folder,), daemon=True).start()
  760.  
  761.     def _scan_folder(self, folder):
  762.         self.channels = []
  763.         video_exts = {'.mp4', '.mkv', '.avi', '.webm', '.m4v', '.mov', '.flv', '.wmv'}
  764.         audio_exts = {'.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma'}
  765.         image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff'}
  766.        
  767.         for root, dirs, files in os.walk(folder):
  768.             for f in files:
  769.                 ext = os.path.splitext(f)[1].lower()
  770.                 if ext in video_exts | audio_exts | image_exts:
  771.                     full_path = os.path.join(root, f)
  772.                     category = os.path.basename(root) if root != folder else "Media"
  773.                     if ext in image_exts:
  774.                         mtype = "IMAGE"
  775.                     elif ext in audio_exts:
  776.                         mtype = "AUDIO"
  777.                     else:
  778.                         mtype = "VIDEO"
  779.                     self.channels.append((f, full_path, category, mtype))
  780.        
  781.         self.filtered = self.channels.copy()
  782.         self.root.after(0, self._finish_load)
  783.  
  784.     def _parse_m3u(self, data):
  785.         self.channels = []
  786.         lines = data.strip().splitlines()
  787.         name, cat = "Unknown", "IPTV"
  788.        
  789.         for line in lines:
  790.             line = line.strip()
  791.             if line.startswith("#EXTINF"):
  792.                 n = re.search(r',(.*)$', line)
  793.                 g = re.search(r'group-title="([^"]*)"', line)
  794.                 name = n.group(1).strip() if n else "Unknown"
  795.                 cat = g.group(1).strip() if g else "IPTV"
  796.             elif line.startswith(("http://", "https://", "rtmp://", "rtsp://", "file:")):
  797.                 if any(line.lower().endswith(ext) for ext in ['.jpg','.jpeg','.png','.gif','.webp']):
  798.                     self.channels.append((name, line, cat, "IMAGE"))
  799.                 elif any(line.lower().endswith(ext) for ext in ['.mp3','.wav','.flac']):
  800.                     self.channels.append((name, line, cat, "AUDIO"))
  801.                 else:
  802.                     self.channels.append((name, line, cat, "VIDEO"))
  803.        
  804.         self.filtered = self.channels.copy()
  805.         self.root.after(0, self._finish_load)
  806.  
  807.     def _finish_load(self):
  808.         self.cat_tree.delete(*self.cat_tree.get_children())
  809.         cats = sorted(set(ch[2] for ch in self.channels))
  810.         for c in cats:
  811.             self.cat_tree.insert("", "end", values=(c,), iid=c)
  812.         self.refresh_list()
  813.         self.status_var.set(f"📺 {len(self.channels)} items loaded")
  814.  
  815.     # ================= LIST & PLAYBACK =================
  816.     def refresh_list(self):
  817.         self.listbox.delete(0, tk.END)
  818.         for ch in self.filtered:
  819.             star = "⭐" if ch[1] in self.favorites else ""
  820.             icon = "🖼️" if ch[3] == "IMAGE" else "🎵" if ch[3] == "AUDIO" else "🎬"
  821.             self.listbox.insert(tk.END, f"{star}{icon} {ch[0]}")
  822.  
  823.     def filter_channels(self):
  824.         q = self.search_var.get().lower()
  825.         if self.show_favorites_only:
  826.             self.filtered = [ch for ch in self.channels
  827.                            if (q in ch[0].lower() or q in ch[2].lower()) and ch[1] in self.favorites]
  828.         else:
  829.             self.filtered = [ch for ch in self.channels
  830.                            if q in ch[0].lower() or q in ch[2].lower()]
  831.         self.refresh_list()
  832.  
  833.     def play_selected(self, event=None):
  834.         sel = self.listbox.curselection()
  835.         if not sel:
  836.             return
  837.         idx = sel[0]
  838.         if idx < len(self.filtered):
  839.             item = self.filtered[idx]
  840.             try:
  841.                 self.current_idx = self.channels.index(item)
  842.             except:
  843.                 pass
  844.             self._play_item(item)
  845.  
  846.     def _play_item(self, item):
  847.         """Play or preview based on media type"""
  848.         name, url, category, media_type = item
  849.        
  850.         if media_type == "IMAGE":
  851.             self.player.stop()
  852.             self.play_btn.config(text="▶ Play")
  853.             if url.startswith(('http://', 'https://')):
  854.                 try:
  855.                     resp = requests.get(url, timeout=10)
  856.                     with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(url)[1]) as tmp:
  857.                         tmp.write(resp.content)
  858.                         tmp_path = tmp.name
  859.                     self._show_image(tmp_path)
  860.                 except:
  861.                     messagebox.showerror("❌ Error", "Failed to download image")
  862.             else:
  863.                 self._show_image(url)
  864.             self.video_frame.pack_forget()
  865.             self.image_label.pack(fill="both", expand=True)
  866.         else:
  867.             self.image_label.pack_forget()
  868.             self.video_frame.pack(side="right", fill="both", expand=True)
  869.             self.current_image = None
  870.            
  871.             media = self.instance.media_new(url)
  872.             self.player.set_media(media)
  873.             self.player.play()
  874.             self.play_btn.config(text="⏸ Pause")
  875.             self.status_var.set(f"{'🎵' if media_type=='AUDIO' else '🎬'} {name}")
  876.             # Re-embed VLC after playing video
  877.             self.root.update_idletasks()
  878.             self._setup_vlc()
  879.  
  880.     def toggle_play(self):
  881.         if self.current_image:
  882.             return
  883.         if self.player.is_playing():
  884.             self.player.pause()
  885.         else:
  886.             self.player.play()
  887.  
  888.     def stop(self):
  889.         if self.current_image:
  890.             self.current_image = None
  891.             self.image_label.config(image="", text="")
  892.             return
  893.         self.player.stop()
  894.         self.play_btn.config(text="▶ Play")
  895.         self.status_var.set("⏹ Stopped")
  896.  
  897.     def prev(self):
  898.         if self.channels:
  899.             self.current_idx = (self.current_idx - 1) % len(self.channels)
  900.             self._play_item(self.channels[self.current_idx])
  901.  
  902.     def next(self):
  903.         if self.channels:
  904.             self.current_idx = (self.current_idx + 1) % len(self.channels)
  905.             self._play_item(self.channels[self.current_idx])
  906.  
  907.     def set_vol(self, val):
  908.         try:
  909.             vol = int(float(val))
  910.             vol = max(0, min(100, vol))
  911.             self.player.audio_set_volume(vol)
  912.         except:
  913.             pass
  914.  
  915.     def end_seek(self, e):
  916.         self.seeking = False
  917.         try:
  918.             self.player.set_time(int(self.seek_bar.get()))
  919.         except:
  920.             pass
  921.  
  922.     def snapshot(self):
  923.         if self.current_image:
  924.             path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
  925.             if path and self.current_image:
  926.                 messagebox.showinfo("ℹ️ Info", "Use file manager to save displayed image")
  927.         else:
  928.             p = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
  929.             if p:
  930.                 try:
  931.                     self.player.video_take_snapshot(0, p, 0, 0)
  932.                     messagebox.showinfo("📸 Saved", "Screenshot saved!")
  933.                 except Exception as e:
  934.                     messagebox.showerror("❌ Error", f"Failed: {e}")
  935.  
  936.     def toggle_sidebar(self):
  937.         """✅ FIXED: Toggle sidebar with proper state management across all modes"""
  938.         # Don't allow sidebar toggle in fullscreen/mini - exit mode first
  939.         if self.is_fullscreen:
  940.             self.status_var.set("⚠️ Press ESC to exit fullscreen first")
  941.             return
  942.         if self.is_mini_mode:
  943.             self.status_var.set("⚠️ Press F11 to exit mini mode first")
  944.             return
  945.            
  946.         if self.is_list_visible:
  947.             self.left_panel.pack_forget()
  948.             self.is_list_visible = False
  949.             self.toggle_btn.config(text="☰ Show List")
  950.             self.status_var.set("📋 Channel list hidden")
  951.         else:
  952.             self.left_panel.pack(side="left", fill="y", padx=(0, 5))
  953.             self.is_list_visible = True
  954.             self.toggle_btn.config(text="☰ Hide List")
  955.             self.status_var.set("📋 Channel list visible")
  956.  
  957.     # ================= XTREAM API =================
  958.     def xtream_login(self):
  959.         h, u, p = self.x_host.get().strip(), self.x_user.get().strip(), self.x_pass.get().strip()
  960.         if not (h and u and p):
  961.             messagebox.showwarning("⚠️ Warning", "Enter Host, Username & Password")
  962.             return
  963.         if not h.startswith("http"):
  964.             h = "http://" + h
  965.         self.xtream = {"host": h.rstrip("/"), "user": u, "pass": p}
  966.         self.status_var.set("⏳ Authenticating...")
  967.         threading.Thread(target=self._xtream_auth, daemon=True).start()
  968.  
  969.     def _xtream_auth(self):
  970.         try:
  971.             url = f"{self.xtream['host']}/player_api.php?username={self.xtream['user']}&password={self.xtream['pass']}"
  972.             r = requests.get(url, timeout=10)
  973.             r.raise_for_status()
  974.             auth = r.json()
  975.             if auth.get("user_info", {}).get("auth") == 1:
  976.                 self.root.after(0, lambda: self.status_var.set("✅ Authenticated"))
  977.                 self._fetch_xtream_cats()
  978.             else:
  979.                 self.root.after(0, lambda: messagebox.showerror("❌ Error", "Invalid credentials"))
  980.         except Exception as e:
  981.             self.root.after(0, lambda: messagebox.showerror("❌ Error", str(e)))
  982.  
  983.     def _fetch_xtream_cats(self):
  984.         try:
  985.             base = f"{self.xtream['host']}/player_api.php?username={self.xtream['user']}&password={self.xtream['pass']}"
  986.             live = requests.get(f"{base}&action=get_live_categories", timeout=10).json()
  987.             vod = requests.get(f"{base}&action=get_vod_categories", timeout=10).json()
  988.            
  989.             self.cat_tree.delete(*self.cat_tree.get_children())
  990.             self.channels = []
  991.            
  992.             for cat in live:
  993.                 cat_id = cat.get('category_id', '')
  994.                 cat_name = cat.get('category_name', 'Unknown')
  995.                 self.cat_tree.insert("", "end", values=(f"📺 {cat_name}",), iid=f"L_{cat_id}")
  996.             for cat in vod:
  997.                 cat_id = cat.get('category_id', '')
  998.                 cat_name = cat.get('category_name', 'Unknown')
  999.                 self.cat_tree.insert("", "end", values=(f"🎬 {cat_name}",), iid=f"V_{cat_id}")
  1000.                
  1001.             self.root.after(0, lambda: self.status_var.set("📁 Categories loaded"))
  1002.         except Exception as e:
  1003.             self.root.after(0, lambda: messagebox.showerror("❌ Error", f"{e}"))
  1004.  
  1005.     def on_cat_select(self, event):
  1006.         """✅ FIXED: Handle category selection - supports both Xtream and M3U categories"""
  1007.         sel = self.cat_tree.selection()
  1008.         if not sel:
  1009.             return
  1010.         iid = sel[0]
  1011.        
  1012.         # Check if this is an Xtream category (format: L_123 or V_456)
  1013.         if "_" in iid:
  1014.             parts = iid.split("_", 1)  # Split only on FIRST underscore
  1015.             if len(parts) == 2:
  1016.                 prefix, cid = parts
  1017.                 if prefix in ("L", "V"):
  1018.                     stype = "live" if prefix == "L" else "movie"
  1019.                     action = f"get_{stype}_streams"
  1020.                     url = f"{self.xtream['host']}/player_api.php?username={self.xtream['user']}&password={self.xtream['pass']}&action={action}&category_id={cid}"
  1021.                     self.status_var.set(f"⏳ Loading {stype}...")
  1022.                     threading.Thread(target=self._fetch_streams, args=(url, stype), daemon=True).start()
  1023.                     return
  1024.        
  1025.         # Fallback: treat as plain category name (M3U/local files)
  1026.         category = iid
  1027.         if self.show_favorites_only:
  1028.             self.filtered = [ch for ch in self.channels if ch[2] == category and ch[1] in self.favorites]
  1029.         else:
  1030.             self.filtered = [ch for ch in self.channels if ch[2] == category]
  1031.         self.refresh_list()
  1032.         self.status_var.set(f"📁 Showing: {category} ({len(self.filtered)} items)")
  1033.  
  1034.     def _fetch_streams(self, url, stype):
  1035.         try:
  1036.             res = requests.get(url, timeout=15).json()
  1037.             new_ch = []
  1038.             for s in res:
  1039.                 name = s.get("name", "Unknown")
  1040.                 sid = s.get("stream_id", "")
  1041.                 cat = s.get("category_name", "Uncategorized")
  1042.                 ext = "m3u8" if stype == "live" else s.get("container_extension", "mp4")
  1043.                 stream_url = f"{self.xtream['host']}/{stype}/{self.xtream['user']}/{self.xtream['pass']}/{sid}.{ext}"
  1044.                 new_ch.append((name, stream_url, cat, "VIDEO"))
  1045.            
  1046.             self.channels.extend(new_ch)
  1047.             self.filtered = self.channels.copy()
  1048.             self.root.after(0, self.refresh_list)
  1049.             self.root.after(0, lambda: self.status_var.set(f"✅ Loaded {len(new_ch)} streams"))
  1050.         except Exception as e:
  1051.             self.root.after(0, lambda: messagebox.showerror("❌ Error", f"{e}"))
  1052.  
  1053.     # ================= M3U FILE/URL =================
  1054.     def load_file(self):
  1055.         path = filedialog.askopenfilename(filetypes=[("M3U Files", "*.m3u *.m3u8")])
  1056.         if path:
  1057.             threading.Thread(target=self._parse_file, args=(path,), daemon=True).start()
  1058.  
  1059.     def _parse_file(self, path):
  1060.         try:
  1061.             with open(path, "r", encoding="utf-8", errors="ignore") as f:
  1062.                 self._parse_m3u(f.read())
  1063.             self.root.after(0, lambda: self.status_var.set(f"✅ Loaded: {os.path.basename(path)}"))
  1064.         except Exception as e:
  1065.             self.root.after(0, lambda: messagebox.showerror("❌ Error", str(e)))
  1066.  
  1067.     def _parse_url(self, url):
  1068.         try:
  1069.             headers = {'User-Agent': 'Mozilla/5.0'}
  1070.             res = requests.get(url, headers=headers, timeout=20)
  1071.             res.raise_for_status()
  1072.             self._parse_m3u(res.text)
  1073.             self.root.after(0, lambda: self.status_var.set("✅ Playlist loaded"))
  1074.         except Exception as e:
  1075.             self.root.after(0, lambda: messagebox.showerror("❌ Error", f"{e}"))
  1076.  
  1077.     # ================= SHORTCUTS & CLEANUP =================
  1078.     def _bind_shortcuts(self):
  1079.         self.root.bind("<space>", lambda e: self.toggle_play())
  1080.         self.root.bind("<f>", lambda e: self.toggle_fullscreen())
  1081.         self.root.bind("<F11>", lambda e: self.toggle_fullscreen())  # F11 also toggles fullscreen
  1082.         self.root.bind("<Escape>", self._exit_fullscreen)  # ✅ ESC to exit fullscreen
  1083.         self.root.bind("<m>", lambda e: self._toggle_mute())
  1084.         self.root.bind("<Right>", lambda e: self.player.set_time(self.player.get_time() + 10000))
  1085.         self.root.bind("<Left>", lambda e: self.player.set_time(self.player.get_time() - 10000))
  1086.         self.root.bind("<Up>", lambda e: self.set_vol(min(100, self.vol_var.get() + 5)) or self.vol_var.set(min(100, self.vol_var.get() + 5)))
  1087.         self.root.bind("<Down>", lambda e: self.set_vol(max(0, self.vol_var.get() - 5)) or self.vol_var.set(max(0, self.vol_var.get() - 5)))
  1088.         self.root.bind("<i>", lambda e: self.toggle_image_mode())
  1089.         self.root.bind("<Control-f>", lambda e: self.toggle_favorites_filter())
  1090.         self.root.bind("<Control-s>", lambda e: self.toggle_favorite())
  1091.         self.root.bind("<Control-e>", lambda e: self.export_playlist())
  1092.         self.root.bind("<F5>", lambda e: self.refresh_list())
  1093.         self.root.bind("<Control-m>", lambda e: self.toggle_mini_player())  # Ctrl+M for mini
  1094.         self.root.bind("<Control-t>", lambda e: self.toggle_theme())
  1095.  
  1096.     def _toggle_mute(self):
  1097.         c = self.player.audio_get_volume()
  1098.         self.vol_var.set(0 if c > 0 else 75)
  1099.         self.set_vol(0 if c > 0 else 75)
  1100.  
  1101.     def _load_config(self):
  1102.         try:
  1103.             if os.path.exists("iptv_config.json"):
  1104.                 with open("iptv_config.json", "r") as f:
  1105.                     cfg = json.load(f)
  1106.                 self.xtream = cfg.get("xtream", self.xtream)
  1107.                 self.x_host.insert(0, self.xtream["host"])
  1108.                 self.x_user.insert(0, self.xtream["user"])
  1109.                 self.x_pass.insert(0, self.xtream["pass"])
  1110.                 self.vol_var.set(cfg.get("volume", 75))
  1111.                 self.db_path = cfg.get("db_path", "Video.db")
  1112.                 self.favorites = set(cfg.get("favorites", []))
  1113.                 self.theme_mode = cfg.get("theme", "dark")
  1114.                 if self.theme_mode == "light":
  1115.                     self.toggle_theme()
  1116.         except:
  1117.             pass
  1118.  
  1119.     def _save_config(self):
  1120.         try:
  1121.             cfg = {
  1122.                 "xtream": {"host": self.xtream["host"], "user": self.xtream["user"], "pass": self.xtream["pass"]},
  1123.                 "volume": self.vol_var.get(),
  1124.                 "db_path": self.db_path,
  1125.                 "favorites": list(self.favorites),
  1126.                 "theme": self.theme_mode
  1127.             }
  1128.             with open("iptv_config.json", "w") as f:
  1129.                 json.dump(cfg, f)
  1130.         except:
  1131.             pass
  1132.  
  1133.     def _cleanup(self):
  1134.         if self.update_id:
  1135.             self.root.after_cancel(self.update_id)
  1136.         # Exit fullscreen/mini before closing
  1137.         if self.is_fullscreen:
  1138.             self.root.attributes('-fullscreen', False)
  1139.         if self.is_mini_mode:
  1140.             if hasattr(self, '_original_geometry_mini'):
  1141.                 self.root.geometry(self._original_geometry_mini)
  1142.         self.player.stop()
  1143.         if self.player:
  1144.             self.player.release()
  1145.         if self.instance:
  1146.             self.instance.release()
  1147.         if self.temp_extract_dir and os.path.exists(self.temp_extract_dir):
  1148.             try:
  1149.                 shutil.rmtree(self.temp_extract_dir)
  1150.             except:
  1151.                 pass
  1152.         self._save_config()
  1153.         self.root.destroy()
  1154.  
  1155.  
  1156. if __name__ == "__main__":
  1157.     root = tk.Tk()
  1158.     app = IPTVPro(root)
  1159.     root.mainloop()
  1160.  
Add Comment
Please, Sign In to add comment