Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- # -*- coding: utf-8 -*-
- """
- NAJEEB IPTV PRO MAX - ULTRA EDITION 🔥 [FIXED v2.0]
- ✓ Fullscreen FIXED (Windows/Linux/macOS) - ESC to exit
- ✓ Mini Player Toggle - Channel list restores properly
- ✓ Fixed ValueError unpacking error
- ✓ Azure Theme Dark/Light Toggle
- ✓ Favorites System with persistent save
- ✓ M3U Export/Import
- ✓ Archive Support (RAR/ZIP) with cleanup
- ✓ Xtream Codes API Full Support
- ✓ SQLite Database Save/Load with indexes
- ✓ Image Preview Mode for local/URL images
- ✓ Python 3.7+ Compatible
- ✓ Proper VLC re-embedding on mode changes
- """
- import tkinter as tk
- from tkinter import ttk, filedialog, messagebox
- from PIL import Image, ImageTk
- import vlc
- import requests
- import os
- import threading
- import re
- import platform
- import time
- import json
- import sqlite3
- import tempfile
- import shutil
- import rarfile
- import zipfile
- from urllib.parse import urlparse, parse_qs
- # Try import ttkthemes for Azure theme support
- try:
- from ttkthemes import ThemedStyle
- HAS_TTKTHEMES = True
- except ImportError:
- HAS_TTKTHEMES = False
- class IPTVPro:
- def __init__(self, root):
- self.root = root
- self.root.title("NAJEEB IPTV PRO MAX - ULTRA EDITION")
- self.root.geometry("1280x720")
- self.root.minsize(900, 550)
- # VLC Instance
- self.instance = vlc.Instance("--no-video-title-show", "--quiet",
- "--network-caching=1500", "--avcodec-hw=any")
- self.player = self.instance.media_player_new()
- # Data Structure: (name, url, category, media_type)
- self.channels = []
- self.filtered = []
- self.current_idx = -1
- self.is_list_visible = True
- self.seeking = False
- self.update_id = None
- self.temp_extract_dir = None
- self.current_image = None
- self.db_path = "Video.db"
- self.theme_mode = "dark"
- self.is_fullscreen = False
- self.is_mini_mode = False
- # State preservation for UI restoration
- self._original_geometry = None
- self._sidebar_state_before_fullscreen = True
- self._sidebar_state_before_mini = True
- self._was_list_visible = True
- # Favorites System
- self.favorites = set()
- self.show_favorites_only = False
- # Xtream Config
- self.xtream = {"host": "", "user": "", "pass": ""}
- self._apply_theme()
- self._setup_ui()
- self._setup_vlc()
- self._start_update_loop()
- self._bind_shortcuts()
- self._load_config()
- self._init_database()
- self.root.protocol("WM_DELETE_WINDOW", self._cleanup)
- def _apply_theme(self):
- """Apply theme with Azure support if available"""
- style = ttk.Style()
- if HAS_TTKTHEMES:
- try:
- style = ThemedStyle(self.root)
- style.set_theme("azure")
- except:
- style.theme_use("clam")
- else:
- style.theme_use("clam")
- self._set_dark_style(style)
- def _set_dark_style(self, style):
- """Apply dark theme colors"""
- style.configure(".", background="#181818", foreground="#e0e0e0", font=("Segoe UI", 10))
- style.configure("TButton", padding=6, background="#2d2d2d", foreground="#ffffff", relief="flat")
- style.map("TButton", background=[("active", "#0078d7")], foreground=[("active", "#ffffff")])
- style.configure("TEntry", fieldbackground="#252525", foreground="#ffffff", borderwidth=1)
- style.configure("TFrame", background="#181818")
- style.configure("TLabel", background="#181818", foreground="#b0b0b0")
- style.configure("TScale", background="#181818", troughcolor="#3a3a3a")
- style.configure("Treeview", background="#252525", foreground="#ffffff",
- fieldbackground="#252525", rowheight=28)
- style.configure("Treeview.Heading", background="#2d2d2d", foreground="#ffffff", relief="flat")
- style.map("Treeview.Heading", background=[("active", "#0078d7")])
- style.configure("Horizontal.TScrollbar", background="#2d2d2d", troughcolor="#181818")
- style.configure("Vertical.TScrollbar", background="#2d2d2d", troughcolor="#181818")
- if hasattr(self, 'listbox'):
- self.listbox.config(bg="#202020", fg="#e0e0e0", selectbackground="#0078d7",
- selectforeground="#ffffff", font=("Consolas", 9), bd=0)
- def _set_light_style(self, style):
- """Apply light theme colors"""
- style.configure(".", background="#f0f0f0", foreground="#1a1a1a", font=("Segoe UI", 10))
- style.configure("TButton", padding=6, background="#e0e0e0", foreground="#1a1a1a", relief="flat")
- style.map("TButton", background=[("active", "#0078d7")], foreground=[("active", "#ffffff")])
- style.configure("TEntry", fieldbackground="#ffffff", foreground="#1a1a1a", borderwidth=1)
- style.configure("TFrame", background="#f0f0f0")
- style.configure("TLabel", background="#f0f0f0", foreground="#333333")
- style.configure("TScale", background="#f0f0f0", troughcolor="#cccccc")
- style.configure("Treeview", background="#ffffff", foreground="#1a1a1a",
- fieldbackground="#ffffff", rowheight=28)
- style.configure("Treeview.Heading", background="#e0e0e0", foreground="#1a1a1a", relief="flat")
- style.map("Treeview.Heading", background=[("active", "#0078d7")])
- style.configure("Horizontal.TScrollbar", background="#e0e0e0", troughcolor="#f0f0f0")
- style.configure("Vertical.TScrollbar", background="#e0e0e0", troughcolor="#f0f0f0")
- if hasattr(self, 'listbox'):
- self.listbox.config(bg="#ffffff", fg="#1a1a1a", selectbackground="#0078d7",
- selectforeground="#ffffff", font=("Consolas", 9), bd=0)
- def toggle_theme(self):
- """Toggle between dark and light theme"""
- style = ttk.Style()
- if self.theme_mode == "dark":
- self._set_light_style(style)
- self.theme_mode = "light"
- self.status_var.set("☀️ Light theme activated")
- else:
- self._set_dark_style(style)
- self.theme_mode = "dark"
- self.status_var.set("🌙 Dark theme activated")
- def _setup_ui(self):
- main = ttk.Frame(self.root, padding=8)
- main.pack(fill="both", expand=True)
- # ===== TOP BAR =====
- self.top_bar = ttk.Frame(main)
- self.top_bar.pack(fill="x", pady=(0, 6))
- ttk.Label(self.top_bar, text="📡 Xtream:").pack(side="left", padx=(0, 4))
- self.x_host = ttk.Entry(self.top_bar, width=18)
- self.x_host.pack(side="left", padx=2)
- self.x_user = ttk.Entry(self.top_bar, width=10)
- self.x_user.pack(side="left", padx=2)
- self.x_pass = ttk.Entry(self.top_bar, width=10, show="*")
- self.x_pass.pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="🔑 Login", command=self.xtream_login).pack(side="left", padx=4)
- ttk.Separator(self.top_bar, orient="vertical").pack(side="left", fill="y", padx=10)
- ttk.Button(self.top_bar, text="📂 M3U File", command=self.load_file).pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="🌐 M3U URL", command=self.load_url_dialog).pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="📁 Local Media", command=self.load_folder).pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="🗜️ Archive", command=self.load_archive).pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="🗄️ Load DB", command=self.load_database).pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="💾 Save DB", command=self.save_database).pack(side="left", padx=2)
- ttk.Button(self.top_bar, text="📤 Export", command=self.export_playlist).pack(side="left", padx=2)
- # Theme toggle & sidebar toggle on right
- ttk.Button(self.top_bar, text="🎨 Theme", command=self.toggle_theme).pack(side="right", padx=2)
- self.toggle_btn = ttk.Button(self.top_bar, text="☰ Hide List", command=self.toggle_sidebar)
- self.toggle_btn.pack(side="right")
- # ===== MAIN CONTENT =====
- self.content_frame = ttk.Frame(main)
- self.content_frame.pack(fill="both", expand=True, pady=5)
- # Left Panel (Channel List)
- self.left_panel = ttk.Frame(self.content_frame, width=280)
- self.left_panel.pack(side="left", fill="y", padx=(0, 5))
- self.left_panel.pack_propagate(False)
- # Search
- ttk.Label(self.left_panel, text="🔍 Search:").pack(anchor="w", padx=6, pady=(6,2))
- self.search_var = tk.StringVar()
- self.search_var.trace("w", lambda *_: self.filter_channels())
- ttk.Entry(self.left_panel, textvariable=self.search_var).pack(fill="x", padx=6, pady=2)
- # Favorites filter
- fav_frame = ttk.Frame(self.left_panel)
- fav_frame.pack(fill="x", padx=6, pady=2)
- ttk.Button(fav_frame, text="⭐ Favorites", command=self.toggle_favorites_filter, width=12).pack(side="left")
- ttk.Button(fav_frame, text="❤️ Toggle", command=self.toggle_favorite, width=10).pack(side="right")
- # Category Tree
- ttk.Label(self.left_panel, text="📁 Categories:").pack(anchor="w", padx=6, pady=(6,2))
- self.cat_tree = ttk.Treeview(self.left_panel, columns=("name",), show="headings", height=6)
- self.cat_tree.heading("name", text="Category")
- self.cat_tree.column("name", width=240)
- self.cat_tree.pack(fill="x", padx=6, pady=2)
- self.cat_tree.bind("<<TreeviewSelect>>", self.on_cat_select)
- # Channel List
- ttk.Label(self.left_panel, text="📺 Channels:").pack(anchor="w", padx=6, pady=(6,2))
- self.listbox = tk.Listbox(self.left_panel, bg="#202020", fg="#e0e0e0", selectbackground="#0078d7",
- selectforeground="#fff", font=("Consolas", 9), bd=0)
- self.listbox.pack(fill="both", expand=True, padx=6, pady=2)
- self.listbox.bind("<<ListboxSelect>>", self.play_selected)
- self.listbox.bind("<Double-1>", lambda e: self.play_selected())
- # Right Panel: Video/Image Display
- self.video_frame = ttk.Frame(self.content_frame, relief="solid", borderwidth=1)
- self.video_frame.pack(side="right", fill="both", expand=True)
- self.image_label = ttk.Label(self.video_frame, background="#000")
- self.image_label.pack(fill="both", expand=True)
- self.image_label.pack_forget()
- # ===== BOTTOM CONTROLS =====
- self.ctrl_frame = ttk.Frame(main)
- self.ctrl_frame.pack(fill="x", pady=(5, 0))
- seek_f = ttk.Frame(self.ctrl_frame)
- seek_f.pack(fill="x", pady=(0, 5))
- self.time_lbl = ttk.Label(seek_f, text="00:00:00 / 00:00:00", width=20)
- self.time_lbl.pack(side="left", padx=6)
- self.seek_bar = ttk.Scale(seek_f, from_=0, to=100, orient="horizontal")
- self.seek_bar.pack(side="left", fill="x", expand=True, padx=6)
- self.seek_bar.bind("<ButtonPress-1>", lambda e: setattr(self, "seeking", True))
- self.seek_bar.bind("<ButtonRelease-1>", self.end_seek)
- play_f = ttk.Frame(self.ctrl_frame)
- play_f.pack(fill="x")
- ttk.Button(play_f, text="⏮ Prev", command=self.prev).pack(side="left", padx=3)
- self.play_btn = ttk.Button(play_f, text="▶ Play", width=6, command=self.toggle_play)
- self.play_btn.pack(side="left", padx=3)
- ttk.Button(play_f, text="⏹ Stop", command=self.stop).pack(side="left", padx=3)
- ttk.Button(play_f, text="⏭ Next", command=self.next).pack(side="left", padx=3)
- ttk.Separator(play_f, orient="vertical").pack(side="left", fill="y", padx=12)
- ttk.Button(play_f, text="📸 Snapshot", command=self.snapshot).pack(side="left", padx=3)
- ttk.Button(play_f, text="⛶ Fullscreen", command=self.toggle_fullscreen).pack(side="left", padx=3)
- ttk.Button(play_f, text="🖼️ Image", command=self.toggle_image_mode).pack(side="left", padx=3)
- ttk.Button(play_f, text="🔲 Mini", command=self.toggle_mini_player).pack(side="left", padx=3)
- vol_f = ttk.Frame(play_f)
- vol_f.pack(side="right")
- self.vol_var = tk.IntVar(value=75)
- ttk.Label(vol_f, text="🔊").pack(side="left")
- ttk.Scale(vol_f, from_=0, to=100, variable=self.vol_var, orient="horizontal",
- length=100, command=self.set_vol).pack(side="left", padx=4)
- self.status_var = tk.StringVar(value="✅ Ready | Load M3U, Archive, DB or Folder")
- ttk.Label(play_f, textvariable=self.status_var, anchor="e", foreground="#4caf50").pack(
- side="right", fill="x", expand=True, padx=6)
- def _setup_vlc(self):
- """Setup VLC embedding for current platform - SAFE re-embed"""
- try:
- sys_name = platform.system()
- if sys_name == "Windows":
- hwnd = self.video_frame.winfo_id()
- if hwnd:
- self.player.set_hwnd(hwnd)
- elif sys_name == "Linux":
- xid = self.video_frame.winfo_id()
- if xid:
- self.player.set_xwindow(xid)
- elif sys_name == "Darwin":
- nsobj = int(self.video_frame.winfo_id())
- if nsobj:
- self.player.set_nsobject(nsobj)
- except Exception as e:
- print(f"⚠️ VLC Embed Warning: {e}")
- def _init_database(self):
- """Initialize SQLite database for storing media"""
- try:
- conn = sqlite3.connect(self.db_path)
- cursor = conn.cursor()
- cursor.execute('''
- CREATE TABLE IF NOT EXISTS media (
- id INTEGER PRIMARY KEY AUTOINCREMENT,
- name TEXT NOT NULL,
- url TEXT NOT NULL,
- category TEXT DEFAULT 'Uncategorized',
- media_type TEXT DEFAULT 'VIDEO',
- is_image BOOLEAN DEFAULT 0,
- added_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- UNIQUE(name, url)
- )
- ''')
- cursor.execute('CREATE INDEX IF NOT EXISTS idx_category ON media(category)')
- cursor.execute('CREATE INDEX IF NOT EXISTS idx_type ON media(media_type)')
- conn.commit()
- conn.close()
- except Exception as e:
- print(f"⚠️ Database init error: {e}")
- def _start_update_loop(self):
- self.update_ui()
- def update_ui(self):
- if self.player and self.player.is_playing() and not self.seeking and not self.current_image:
- t = self.player.get_time()
- d = self.player.get_length()
- if d > 0 and d != -1:
- self.seek_bar.config(to=d)
- self.seek_bar.set(t)
- self.time_lbl.config(text=f"{self._fmt(t)} / {self._fmt(d)}")
- else:
- self.time_lbl.config(text=f"{self._fmt(t)} / LIVE")
- st = self.player.get_state()
- if st == vlc.State.Playing:
- name = self.channels[self.current_idx][0] if 0 <= self.current_idx < len(self.channels) else "Playing"
- self.status_var.set(f"▶ {name}")
- self.play_btn.config(text="⏸ Pause")
- elif st == vlc.State.Paused:
- self.status_var.set("⏸ Paused")
- self.play_btn.config(text="▶ Play")
- self.update_id = self.root.after(500, self.update_ui)
- @staticmethod
- def _fmt(ms):
- if ms < 0:
- return "00:00:00"
- s = max(0, int(ms)//1000)
- return time.strftime("%H:%M:%S", time.gmtime(s))
- # ================= FULLSCREEN (COMPLETELY FIXED) =================
- def toggle_fullscreen(self):
- """✅ COMPLETE FIX: Toggle fullscreen with proper VLC re-embed & UI restoration"""
- try:
- if self.is_fullscreen:
- # === EXIT FULLSCREEN ===
- self.root.attributes('-fullscreen', False)
- if platform.system() == "Darwin":
- self.root.attributes('-zoomed', False)
- # Restore original geometry
- if self._original_geometry:
- self.root.geometry(self._original_geometry)
- # Restore UI elements
- self.top_bar.pack(fill="x", pady=(0, 6))
- self.content_frame.pack(fill="both", expand=True, pady=5)
- self.ctrl_frame.pack(fill="x", pady=(5, 0))
- # Restore sidebar based on saved state
- if self._sidebar_state_before_fullscreen and not self.is_list_visible:
- self.left_panel.pack(side="left", fill="y", padx=(0, 5))
- self.is_list_visible = True
- self.toggle_btn.config(text="☰ Hide List")
- elif not self._sidebar_state_before_fullscreen:
- self.left_panel.pack_forget()
- self.is_list_visible = False
- self.toggle_btn.config(text="☰ Show List")
- self.is_fullscreen = False
- self.status_var.set("🖥️ Windowed mode")
- # Re-embed VLC properly
- self.root.update_idletasks()
- self._setup_vlc()
- else:
- # === ENTER FULLSCREEN ===
- # Save current state
- self._original_geometry = self.root.geometry()
- self._sidebar_state_before_fullscreen = self.is_list_visible
- # Hide UI elements for clean fullscreen
- self.top_bar.pack_forget()
- self.ctrl_frame.pack_forget()
- # Handle sidebar
- if self.is_list_visible:
- self.left_panel.pack_forget()
- self.is_list_visible = False
- # Enter fullscreen
- self.root.attributes('-fullscreen', True)
- if platform.system() == "Darwin":
- self.root.attributes('-zoomed', True)
- self.is_fullscreen = True
- self.status_var.set("⛶ Fullscreen (Press ESC to exit)")
- # Re-embed VLC for fullscreen
- self.root.update_idletasks()
- self._setup_vlc()
- except Exception as e:
- print(f"⚠️ Fullscreen error: {e}")
- self.status_var.set(f"❌ Fullscreen error: {e}")
- def _exit_fullscreen(self, event=None):
- """Exit fullscreen when ESC is pressed"""
- if self.is_fullscreen:
- self.toggle_fullscreen()
- return "break"
- # ================= MINI PLAYER (FIXED TOGGLE) =================
- def toggle_mini_player(self):
- """✅ FIXED: Toggle mini player with proper channel list restoration"""
- try:
- if self.is_mini_mode:
- # === EXIT MINI MODE ===
- # Restore original geometry
- if hasattr(self, '_original_geometry_mini') and self._original_geometry_mini:
- self.root.geometry(self._original_geometry_mini)
- # Restore UI elements
- self.top_bar.pack(fill="x", pady=(0, 6))
- self.content_frame.pack(fill="both", expand=True, pady=5)
- self.ctrl_frame.pack(fill="x", pady=(5, 0))
- # Restore sidebar based on saved state
- if self._sidebar_state_before_mini:
- self.left_panel.pack(side="left", fill="y", padx=(0, 5))
- self.is_list_visible = True
- self.toggle_btn.config(text="☰ Hide List")
- else:
- self.left_panel.pack_forget()
- self.is_list_visible = False
- self.toggle_btn.config(text="☰ Show List")
- self.is_mini_mode = False
- self.status_var.set("🖥️ Full mode restored")
- # Re-embed VLC
- self.root.update_idletasks()
- self._setup_vlc()
- else:
- # === ENTER MINI MODE ===
- # Save current state
- self._original_geometry_mini = self.root.geometry()
- self._sidebar_state_before_mini = self.is_list_visible
- # Hide UI elements for compact view
- self.top_bar.pack_forget()
- self.ctrl_frame.pack_forget()
- # Hide sidebar for mini mode (always)
- if self.is_list_visible:
- self.left_panel.pack_forget()
- self.is_list_visible = False
- # Set mini window size
- self.root.geometry("500x400")
- self.root.minsize(400, 300)
- self.is_mini_mode = True
- self.status_var.set("🔲 Mini mode - Press F11 or 🔲 to restore")
- # Re-embed VLC for mini mode
- self.root.update_idletasks()
- self._setup_vlc()
- except Exception as e:
- print(f"⚠️ Mini player error: {e}")
- self.status_var.set(f"❌ Mini error: {e}")
- # ================= IMAGE PREVIEW =================
- def toggle_image_mode(self):
- """Toggle between video player and image preview mode"""
- if self.current_image:
- self.image_label.pack_forget()
- self.video_frame.pack(side="right", fill="both", expand=True)
- self.current_image = None
- self.status_var.set("🎬 Video mode")
- self._setup_vlc() # Re-embed VLC
- else:
- if self.current_idx >= 0 and self.current_idx < len(self.channels):
- item = self.channels[self.current_idx]
- if item[3] == "IMAGE":
- self.video_frame.pack_forget()
- self.image_label.pack(fill="both", expand=True)
- self._show_image(item[1])
- def _show_image(self, image_path):
- """Display image in preview mode"""
- try:
- img = Image.open(image_path)
- window_w = max(400, self.root.winfo_width() - 320)
- window_h = max(300, self.root.winfo_height() - 120)
- img.thumbnail((window_w, window_h), Image.Resampling.LANCZOS)
- self.current_image = ImageTk.PhotoImage(img)
- self.image_label.config(image=self.current_image, text="")
- self.status_var.set(f"🖼️ {os.path.basename(image_path)}")
- except Exception as e:
- messagebox.showerror("❌ Image Error", f"Failed to load image:\n{e}")
- # ================= FAVORITES SYSTEM =================
- def toggle_favorite(self):
- """Toggle current channel as favorite"""
- if self.current_idx < 0 or self.current_idx >= len(self.channels):
- return
- item = self.channels[self.current_idx]
- url = item[1]
- if url in self.favorites:
- self.favorites.remove(url)
- self.status_var.set("❤️ Removed from favorites")
- else:
- self.favorites.add(url)
- self.status_var.set("⭐ Added to favorites")
- self.refresh_list()
- def toggle_favorites_filter(self):
- """Show only favorite channels"""
- self.show_favorites_only = not self.show_favorites_only
- if self.show_favorites_only:
- self.filtered = [ch for ch in self.channels if ch[1] in self.favorites]
- self.status_var.set(f"⭐ Favorites only ({len(self.filtered)} items)")
- else:
- self.filtered = self.channels.copy()
- self.status_var.set(f"📺 All channels ({len(self.filtered)} items)")
- self.refresh_list()
- # ================= DATABASE SAVE/LOAD =================
- def save_database(self):
- """Save current channel list to SQLite database"""
- path = filedialog.asksaveasfilename(
- defaultextension=".db",
- filetypes=[("Database Files", "*.db *.sqlite"), ("All Files", "*.*")],
- initialfile="Video.db"
- )
- if not path:
- return
- self.status_var.set("⏳ Saving to database...")
- threading.Thread(target=self._save_to_db, args=(path,), daemon=True).start()
- def _save_to_db(self, db_path):
- try:
- conn = sqlite3.connect(db_path)
- cursor = conn.cursor()
- saved = 0
- for name, url, category, media_type in self.channels:
- is_image = 1 if media_type == "IMAGE" else 0
- try:
- cursor.execute('''
- INSERT OR REPLACE INTO media (name, url, category, media_type, is_image)
- VALUES (?, ?, ?, ?, ?)
- ''', (name, url, category, media_type, is_image))
- saved += 1
- except:
- continue
- conn.commit()
- conn.close()
- self.db_path = db_path
- self.root.after(0, lambda: messagebox.showinfo("✅ Saved", f"{saved} entries saved to:\n{db_path}"))
- self.root.after(0, lambda: self.status_var.set(f"💾 Saved {saved} entries"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", f"Failed to save database:\n{e}"))
- def load_database(self):
- """Load media entries from SQLite database"""
- path = filedialog.askopenfilename(
- filetypes=[("Database Files", "*.db *.sqlite *.sqlite3"), ("All Files", "*.*")]
- )
- if path:
- self.db_path = path
- self.status_var.set(f"⏳ Loading: {os.path.basename(path)}...")
- threading.Thread(target=self._load_from_db, args=(path,), daemon=True).start()
- def _load_from_db(self, db_path):
- try:
- conn = sqlite3.connect(db_path)
- cursor = conn.cursor()
- cursor.execute("SELECT name, url, category, media_type, is_image FROM media")
- rows = cursor.fetchall()
- conn.close()
- media_files = []
- for name, url, category, media_type, is_image in rows:
- if not url.startswith(('http://', 'https://', 'rtmp://', 'rtsp://')):
- if not os.path.exists(url):
- continue
- media_files.append((name, url, category, media_type))
- if media_files:
- self.channels = media_files
- self.filtered = self.channels.copy()
- self.root.after(0, self._finish_load)
- self.root.after(0, lambda: self.status_var.set(f"✅ Loaded {len(media_files)} entries from DB"))
- else:
- self.root.after(0, lambda: messagebox.showinfo("ℹ️ Info", "No valid entries found in database"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", f"Failed to load database:\n{e}"))
- # ================= PLAYLIST EXPORT =================
- def export_playlist(self):
- """Export current channels to M3U file"""
- path = filedialog.asksaveasfilename(
- defaultextension=".m3u",
- filetypes=[("M3U Playlist", "*.m3u")],
- initialfile="my_playlist.m3u"
- )
- if not path:
- return
- try:
- with open(path, "w", encoding="utf-8") as f:
- f.write("#EXTM3U\n")
- for name, url, category, media_type in self.channels:
- f.write(f'#EXTINF:-1 group-title="{category}",{name}\n{url}\n')
- messagebox.showinfo("✅ Exported", f"Playlist saved to:\n{path}")
- self.status_var.set(f"📤 Exported {len(self.channels)} items")
- except Exception as e:
- messagebox.showerror("❌ Error", f"Export failed:\n{e}")
- # ================= XTREAM & M3U URL SUPPORT =================
- def load_url_dialog(self):
- """Dialog for loading M3U/Xtream URLs with presets"""
- dialog = tk.Toplevel(self.root)
- dialog.title("🌐 Load M3U/Xtream URL")
- dialog.geometry("600x300")
- dialog.transient(self.root)
- dialog.grab_set()
- ttk.Label(dialog, text="Enter M3U or Xtream URL:").pack(pady=10)
- url_entry = ttk.Entry(dialog, width=70)
- url_entry.pack(pady=5)
- presets = [
- "https://iptv-org.github.io/iptv/index.m3u",
- "https://iptv-org.github.io/iptv/categories/entertainment.m3u",
- ]
- ttk.Label(dialog, text="📋 Quick Load Presets:").pack(pady=(15,5))
- preset_frame = ttk.Frame(dialog)
- preset_frame.pack(pady=5)
- for i, preset in enumerate(presets):
- display = preset[:50] + "..." if len(preset) > 50 else preset
- ttk.Button(preset_frame, text=f"{i+1}. {display}",
- command=lambda u=preset: self._load_url_from_dialog(u, dialog)).pack(fill="x", pady=2)
- ttk.Button(dialog, text="✅ Load URL",
- command=lambda: self._load_url_from_dialog(url_entry.get(), dialog)).pack(pady=15)
- url_entry.focus_set()
- dialog.bind('<Return>', lambda e: self._load_url_from_dialog(url_entry.get(), dialog))
- def _load_url_from_dialog(self, url, dialog=None):
- """Process URL from dialog"""
- if dialog:
- dialog.destroy()
- url = url.strip()
- if not url:
- messagebox.showwarning("⚠️ Warning", "Please enter a URL")
- return
- # Auto-detect Xtream format
- if 'get.php' in url and 'username=' in url and 'password=' in url:
- parsed = urlparse(url)
- params = parse_qs(parsed.query)
- if 'username' in params and 'password' in params:
- self.xtream = {
- "host": f"{parsed.scheme}://{parsed.netloc}",
- "user": params['username'][0],
- "pass": params['password'][0]
- }
- self.x_host.delete(0, tk.END)
- self.x_host.insert(0, self.xtream["host"])
- self.x_user.delete(0, tk.END)
- self.x_user.insert(0, self.xtream["user"])
- self.x_pass.delete(0, tk.END)
- self.x_pass.insert(0, self.xtream["pass"])
- self.status_var.set("🔑 Xtream credentials detected")
- self.status_var.set("⏳ Downloading playlist...")
- threading.Thread(target=self._parse_url, args=(url,), daemon=True).start()
- # ================= ARCHIVE (RAR/ZIP) =================
- def load_archive(self):
- path = filedialog.askopenfilename(filetypes=[
- ("Archive Files", "*.rar *.zip *.RAR *.ZIP"),
- ("All Files", "*.*")
- ])
- if path:
- self.status_var.set(f"⏳ Extracting: {os.path.basename(path)}...")
- threading.Thread(target=self._extract_archive, args=(path,), daemon=True).start()
- def _extract_archive(self, archive_path):
- try:
- if self.temp_extract_dir and os.path.exists(self.temp_extract_dir):
- shutil.rmtree(self.temp_extract_dir)
- self.temp_extract_dir = tempfile.mkdtemp(prefix="iptv_extract_")
- media_files = []
- video_exts = {'.mp4', '.mkv', '.avi', '.webm', '.m4v', '.mov', '.flv', '.wmv'}
- audio_exts = {'.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma'}
- image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg'}
- all_exts = video_exts | audio_exts | image_exts
- archive_lower = archive_path.lower()
- if archive_lower.endswith('.rar'):
- with rarfile.RarFile(archive_path, 'r') as rf:
- for member in rf.namelist():
- if any(member.lower().endswith(ext) for ext in all_exts):
- extracted = rf.extract(member, path=self.temp_extract_dir)
- filename = os.path.basename(member)
- ext = os.path.splitext(filename)[1].lower()
- mtype = "IMAGE" if ext in image_exts else "AUDIO" if ext in audio_exts else "VIDEO"
- media_files.append((filename, extracted, "Archive", mtype))
- elif archive_lower.endswith('.zip'):
- with zipfile.ZipFile(archive_path, 'r') as zf:
- for member in zf.namelist():
- if any(member.lower().endswith(ext) for ext in all_exts):
- extracted = zf.extract(member, path=self.temp_extract_dir)
- filename = os.path.basename(member)
- ext = os.path.splitext(filename)[1].lower()
- mtype = "IMAGE" if ext in image_exts else "AUDIO" if ext in audio_exts else "VIDEO"
- media_files.append((filename, extracted, "Archive", mtype))
- if media_files:
- self.channels.extend(media_files)
- self.filtered = self.channels.copy()
- self.root.after(0, self._finish_load)
- self.root.after(0, lambda: self.status_var.set(f"✅ Extracted {len(media_files)} files"))
- else:
- self.root.after(0, lambda: messagebox.showinfo("ℹ️ Info", "No media files found in archive"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", f"Extraction failed:\n{e}"))
- # ================= FOLDER SCANNING =================
- def load_folder(self):
- folder = filedialog.askdirectory()
- if not folder:
- return
- self.status_var.set("⏳ Scanning folder...")
- threading.Thread(target=self._scan_folder, args=(folder,), daemon=True).start()
- def _scan_folder(self, folder):
- self.channels = []
- video_exts = {'.mp4', '.mkv', '.avi', '.webm', '.m4v', '.mov', '.flv', '.wmv'}
- audio_exts = {'.mp3', '.wav', '.flac', '.aac', '.ogg', '.m4a', '.wma'}
- image_exts = {'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.tiff'}
- for root, dirs, files in os.walk(folder):
- for f in files:
- ext = os.path.splitext(f)[1].lower()
- if ext in video_exts | audio_exts | image_exts:
- full_path = os.path.join(root, f)
- category = os.path.basename(root) if root != folder else "Media"
- if ext in image_exts:
- mtype = "IMAGE"
- elif ext in audio_exts:
- mtype = "AUDIO"
- else:
- mtype = "VIDEO"
- self.channels.append((f, full_path, category, mtype))
- self.filtered = self.channels.copy()
- self.root.after(0, self._finish_load)
- def _parse_m3u(self, data):
- self.channels = []
- lines = data.strip().splitlines()
- name, cat = "Unknown", "IPTV"
- for line in lines:
- line = line.strip()
- if line.startswith("#EXTINF"):
- n = re.search(r',(.*)$', line)
- g = re.search(r'group-title="([^"]*)"', line)
- name = n.group(1).strip() if n else "Unknown"
- cat = g.group(1).strip() if g else "IPTV"
- elif line.startswith(("http://", "https://", "rtmp://", "rtsp://", "file:")):
- if any(line.lower().endswith(ext) for ext in ['.jpg','.jpeg','.png','.gif','.webp']):
- self.channels.append((name, line, cat, "IMAGE"))
- elif any(line.lower().endswith(ext) for ext in ['.mp3','.wav','.flac']):
- self.channels.append((name, line, cat, "AUDIO"))
- else:
- self.channels.append((name, line, cat, "VIDEO"))
- self.filtered = self.channels.copy()
- self.root.after(0, self._finish_load)
- def _finish_load(self):
- self.cat_tree.delete(*self.cat_tree.get_children())
- cats = sorted(set(ch[2] for ch in self.channels))
- for c in cats:
- self.cat_tree.insert("", "end", values=(c,), iid=c)
- self.refresh_list()
- self.status_var.set(f"📺 {len(self.channels)} items loaded")
- # ================= LIST & PLAYBACK =================
- def refresh_list(self):
- self.listbox.delete(0, tk.END)
- for ch in self.filtered:
- star = "⭐" if ch[1] in self.favorites else ""
- icon = "🖼️" if ch[3] == "IMAGE" else "🎵" if ch[3] == "AUDIO" else "🎬"
- self.listbox.insert(tk.END, f"{star}{icon} {ch[0]}")
- def filter_channels(self):
- q = self.search_var.get().lower()
- if self.show_favorites_only:
- self.filtered = [ch for ch in self.channels
- if (q in ch[0].lower() or q in ch[2].lower()) and ch[1] in self.favorites]
- else:
- self.filtered = [ch for ch in self.channels
- if q in ch[0].lower() or q in ch[2].lower()]
- self.refresh_list()
- def play_selected(self, event=None):
- sel = self.listbox.curselection()
- if not sel:
- return
- idx = sel[0]
- if idx < len(self.filtered):
- item = self.filtered[idx]
- try:
- self.current_idx = self.channels.index(item)
- except:
- pass
- self._play_item(item)
- def _play_item(self, item):
- """Play or preview based on media type"""
- name, url, category, media_type = item
- if media_type == "IMAGE":
- self.player.stop()
- self.play_btn.config(text="▶ Play")
- if url.startswith(('http://', 'https://')):
- try:
- resp = requests.get(url, timeout=10)
- with tempfile.NamedTemporaryFile(delete=False, suffix=os.path.splitext(url)[1]) as tmp:
- tmp.write(resp.content)
- tmp_path = tmp.name
- self._show_image(tmp_path)
- except:
- messagebox.showerror("❌ Error", "Failed to download image")
- else:
- self._show_image(url)
- self.video_frame.pack_forget()
- self.image_label.pack(fill="both", expand=True)
- else:
- self.image_label.pack_forget()
- self.video_frame.pack(side="right", fill="both", expand=True)
- self.current_image = None
- media = self.instance.media_new(url)
- self.player.set_media(media)
- self.player.play()
- self.play_btn.config(text="⏸ Pause")
- self.status_var.set(f"{'🎵' if media_type=='AUDIO' else '🎬'} {name}")
- # Re-embed VLC after playing video
- self.root.update_idletasks()
- self._setup_vlc()
- def toggle_play(self):
- if self.current_image:
- return
- if self.player.is_playing():
- self.player.pause()
- else:
- self.player.play()
- def stop(self):
- if self.current_image:
- self.current_image = None
- self.image_label.config(image="", text="")
- return
- self.player.stop()
- self.play_btn.config(text="▶ Play")
- self.status_var.set("⏹ Stopped")
- def prev(self):
- if self.channels:
- self.current_idx = (self.current_idx - 1) % len(self.channels)
- self._play_item(self.channels[self.current_idx])
- def next(self):
- if self.channels:
- self.current_idx = (self.current_idx + 1) % len(self.channels)
- self._play_item(self.channels[self.current_idx])
- def set_vol(self, val):
- try:
- vol = int(float(val))
- vol = max(0, min(100, vol))
- self.player.audio_set_volume(vol)
- except:
- pass
- def end_seek(self, e):
- self.seeking = False
- try:
- self.player.set_time(int(self.seek_bar.get()))
- except:
- pass
- def snapshot(self):
- if self.current_image:
- path = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
- if path and self.current_image:
- messagebox.showinfo("ℹ️ Info", "Use file manager to save displayed image")
- else:
- p = filedialog.asksaveasfilename(defaultextension=".png", filetypes=[("PNG", "*.png")])
- if p:
- try:
- self.player.video_take_snapshot(0, p, 0, 0)
- messagebox.showinfo("📸 Saved", "Screenshot saved!")
- except Exception as e:
- messagebox.showerror("❌ Error", f"Failed: {e}")
- def toggle_sidebar(self):
- """✅ FIXED: Toggle sidebar with proper state management across all modes"""
- # Don't allow sidebar toggle in fullscreen/mini - exit mode first
- if self.is_fullscreen:
- self.status_var.set("⚠️ Press ESC to exit fullscreen first")
- return
- if self.is_mini_mode:
- self.status_var.set("⚠️ Press F11 to exit mini mode first")
- return
- if self.is_list_visible:
- self.left_panel.pack_forget()
- self.is_list_visible = False
- self.toggle_btn.config(text="☰ Show List")
- self.status_var.set("📋 Channel list hidden")
- else:
- self.left_panel.pack(side="left", fill="y", padx=(0, 5))
- self.is_list_visible = True
- self.toggle_btn.config(text="☰ Hide List")
- self.status_var.set("📋 Channel list visible")
- # ================= XTREAM API =================
- def xtream_login(self):
- h, u, p = self.x_host.get().strip(), self.x_user.get().strip(), self.x_pass.get().strip()
- if not (h and u and p):
- messagebox.showwarning("⚠️ Warning", "Enter Host, Username & Password")
- return
- if not h.startswith("http"):
- h = "http://" + h
- self.xtream = {"host": h.rstrip("/"), "user": u, "pass": p}
- self.status_var.set("⏳ Authenticating...")
- threading.Thread(target=self._xtream_auth, daemon=True).start()
- def _xtream_auth(self):
- try:
- url = f"{self.xtream['host']}/player_api.php?username={self.xtream['user']}&password={self.xtream['pass']}"
- r = requests.get(url, timeout=10)
- r.raise_for_status()
- auth = r.json()
- if auth.get("user_info", {}).get("auth") == 1:
- self.root.after(0, lambda: self.status_var.set("✅ Authenticated"))
- self._fetch_xtream_cats()
- else:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", "Invalid credentials"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", str(e)))
- def _fetch_xtream_cats(self):
- try:
- base = f"{self.xtream['host']}/player_api.php?username={self.xtream['user']}&password={self.xtream['pass']}"
- live = requests.get(f"{base}&action=get_live_categories", timeout=10).json()
- vod = requests.get(f"{base}&action=get_vod_categories", timeout=10).json()
- self.cat_tree.delete(*self.cat_tree.get_children())
- self.channels = []
- for cat in live:
- cat_id = cat.get('category_id', '')
- cat_name = cat.get('category_name', 'Unknown')
- self.cat_tree.insert("", "end", values=(f"📺 {cat_name}",), iid=f"L_{cat_id}")
- for cat in vod:
- cat_id = cat.get('category_id', '')
- cat_name = cat.get('category_name', 'Unknown')
- self.cat_tree.insert("", "end", values=(f"🎬 {cat_name}",), iid=f"V_{cat_id}")
- self.root.after(0, lambda: self.status_var.set("📁 Categories loaded"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", f"{e}"))
- def on_cat_select(self, event):
- """✅ FIXED: Handle category selection - supports both Xtream and M3U categories"""
- sel = self.cat_tree.selection()
- if not sel:
- return
- iid = sel[0]
- # Check if this is an Xtream category (format: L_123 or V_456)
- if "_" in iid:
- parts = iid.split("_", 1) # Split only on FIRST underscore
- if len(parts) == 2:
- prefix, cid = parts
- if prefix in ("L", "V"):
- stype = "live" if prefix == "L" else "movie"
- action = f"get_{stype}_streams"
- url = f"{self.xtream['host']}/player_api.php?username={self.xtream['user']}&password={self.xtream['pass']}&action={action}&category_id={cid}"
- self.status_var.set(f"⏳ Loading {stype}...")
- threading.Thread(target=self._fetch_streams, args=(url, stype), daemon=True).start()
- return
- # Fallback: treat as plain category name (M3U/local files)
- category = iid
- if self.show_favorites_only:
- self.filtered = [ch for ch in self.channels if ch[2] == category and ch[1] in self.favorites]
- else:
- self.filtered = [ch for ch in self.channels if ch[2] == category]
- self.refresh_list()
- self.status_var.set(f"📁 Showing: {category} ({len(self.filtered)} items)")
- def _fetch_streams(self, url, stype):
- try:
- res = requests.get(url, timeout=15).json()
- new_ch = []
- for s in res:
- name = s.get("name", "Unknown")
- sid = s.get("stream_id", "")
- cat = s.get("category_name", "Uncategorized")
- ext = "m3u8" if stype == "live" else s.get("container_extension", "mp4")
- stream_url = f"{self.xtream['host']}/{stype}/{self.xtream['user']}/{self.xtream['pass']}/{sid}.{ext}"
- new_ch.append((name, stream_url, cat, "VIDEO"))
- self.channels.extend(new_ch)
- self.filtered = self.channels.copy()
- self.root.after(0, self.refresh_list)
- self.root.after(0, lambda: self.status_var.set(f"✅ Loaded {len(new_ch)} streams"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", f"{e}"))
- # ================= M3U FILE/URL =================
- def load_file(self):
- path = filedialog.askopenfilename(filetypes=[("M3U Files", "*.m3u *.m3u8")])
- if path:
- threading.Thread(target=self._parse_file, args=(path,), daemon=True).start()
- def _parse_file(self, path):
- try:
- with open(path, "r", encoding="utf-8", errors="ignore") as f:
- self._parse_m3u(f.read())
- self.root.after(0, lambda: self.status_var.set(f"✅ Loaded: {os.path.basename(path)}"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", str(e)))
- def _parse_url(self, url):
- try:
- headers = {'User-Agent': 'Mozilla/5.0'}
- res = requests.get(url, headers=headers, timeout=20)
- res.raise_for_status()
- self._parse_m3u(res.text)
- self.root.after(0, lambda: self.status_var.set("✅ Playlist loaded"))
- except Exception as e:
- self.root.after(0, lambda: messagebox.showerror("❌ Error", f"{e}"))
- # ================= SHORTCUTS & CLEANUP =================
- def _bind_shortcuts(self):
- self.root.bind("<space>", lambda e: self.toggle_play())
- self.root.bind("<f>", lambda e: self.toggle_fullscreen())
- self.root.bind("<F11>", lambda e: self.toggle_fullscreen()) # F11 also toggles fullscreen
- self.root.bind("<Escape>", self._exit_fullscreen) # ✅ ESC to exit fullscreen
- self.root.bind("<m>", lambda e: self._toggle_mute())
- self.root.bind("<Right>", lambda e: self.player.set_time(self.player.get_time() + 10000))
- self.root.bind("<Left>", lambda e: self.player.set_time(self.player.get_time() - 10000))
- 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)))
- 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)))
- self.root.bind("<i>", lambda e: self.toggle_image_mode())
- self.root.bind("<Control-f>", lambda e: self.toggle_favorites_filter())
- self.root.bind("<Control-s>", lambda e: self.toggle_favorite())
- self.root.bind("<Control-e>", lambda e: self.export_playlist())
- self.root.bind("<F5>", lambda e: self.refresh_list())
- self.root.bind("<Control-m>", lambda e: self.toggle_mini_player()) # Ctrl+M for mini
- self.root.bind("<Control-t>", lambda e: self.toggle_theme())
- def _toggle_mute(self):
- c = self.player.audio_get_volume()
- self.vol_var.set(0 if c > 0 else 75)
- self.set_vol(0 if c > 0 else 75)
- def _load_config(self):
- try:
- if os.path.exists("iptv_config.json"):
- with open("iptv_config.json", "r") as f:
- cfg = json.load(f)
- self.xtream = cfg.get("xtream", self.xtream)
- self.x_host.insert(0, self.xtream["host"])
- self.x_user.insert(0, self.xtream["user"])
- self.x_pass.insert(0, self.xtream["pass"])
- self.vol_var.set(cfg.get("volume", 75))
- self.db_path = cfg.get("db_path", "Video.db")
- self.favorites = set(cfg.get("favorites", []))
- self.theme_mode = cfg.get("theme", "dark")
- if self.theme_mode == "light":
- self.toggle_theme()
- except:
- pass
- def _save_config(self):
- try:
- cfg = {
- "xtream": {"host": self.xtream["host"], "user": self.xtream["user"], "pass": self.xtream["pass"]},
- "volume": self.vol_var.get(),
- "db_path": self.db_path,
- "favorites": list(self.favorites),
- "theme": self.theme_mode
- }
- with open("iptv_config.json", "w") as f:
- json.dump(cfg, f)
- except:
- pass
- def _cleanup(self):
- if self.update_id:
- self.root.after_cancel(self.update_id)
- # Exit fullscreen/mini before closing
- if self.is_fullscreen:
- self.root.attributes('-fullscreen', False)
- if self.is_mini_mode:
- if hasattr(self, '_original_geometry_mini'):
- self.root.geometry(self._original_geometry_mini)
- self.player.stop()
- if self.player:
- self.player.release()
- if self.instance:
- self.instance.release()
- if self.temp_extract_dir and os.path.exists(self.temp_extract_dir):
- try:
- shutil.rmtree(self.temp_extract_dir)
- except:
- pass
- self._save_config()
- self.root.destroy()
- if __name__ == "__main__":
- root = tk.Tk()
- app = IPTVPro(root)
- root.mainloop()
Add Comment
Please, Sign In to add comment