Not a member of Pastebin yet?
Sign Up,
it unlocks many cool features!
- #!/usr/bin/env python3
- # -*- coding: utf-8 -*-
- """
- Najeeb Ultimate IPTV Country Playlist Generator
- Features:
- - Load local M3U / M3U8 files
- - Fetch remote playlists from custom URLs
- - Filter by country (Albania, Italy, USA, etc.)
- - Search, Check URLs, Remove dead channels
- - Save as M3U/M3U8 or Export JSON
- - Play in embedded mini VLC or external VLC
- - Dark/Light mode with ttkbootstrap
- """
- from __future__ import annotations
- import os
- import sys
- import json
- import re
- import threading
- import subprocess
- import webbrowser
- import shutil
- from concurrent.futures import ThreadPoolExecutor, as_completed
- from typing import List, Dict, Optional
- import base64
- import io
- # Required: pip install requests ttkbootstrap pillow python-vlc
- try:
- import requests
- except Exception:
- raise SystemExit("Install: pip install requests")
- try:
- import vlc
- VLC_AVAILABLE = True
- except Exception:
- VLC_AVAILABLE = False
- try:
- import ttkbootstrap as tb
- from ttkbootstrap.constants import *
- TTB_AVAILABLE = True
- except Exception:
- tb = None
- TTB_AVAILABLE = False
- import tkinter as tk
- from tkinter import ttk, filedialog, messagebox
- # ---------- Config & Constants ----------
- CONFIG_FILE = "iptv_ultimate_config.json"
- DEFAULT_REMOTE_URL2 = "https://iptv-org.github.io/iptv/index.m3u"
- DEFAULT_REMOTE_URL = "https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8"
- EPG_URL = "https://tvit.leicaflorianrobert.dev/epg/list.xml"
- REQUEST_TIMEOUT = 10
- MAX_THREADS = 25
- GROUPS_MAP = {
- 'al': ['Albania'], 'ad': ['Andorra'], 'ar': ['Argentina'], 'am': ['Armenia'],
- 'au': ['Australia'], 'at': ['Austria'], 'az': ['Azerbaijan'], 'by': ['Belarus'],
- 'be': ['Belgium'], 'ba': ['Bosnia and Herzegovina'], 'br': ['Brazil'],
- 'bg': ['Bulgaria'], 'ca': ['Canada'], 'cl': ['Chile'], 'cn': ['China'],
- 'hr': ['Croatia'], 'cy': ['Cyprus'], 'cz': ['Czech Republic'], 'dk': ['Denmark'],
- 'eg': ['Egypt'], 'fi': ['Finland'], 'fr': ['France'], 'de': ['Germany'],
- 'gr': ['Greece'], 'hk': ['Hong Kong'], 'hu': ['Hungary'], 'in': ['India'],
- 'it': ['Italy', 'VOD Italy'], 'jp': ['Japan'], 'kr': ['Korea'], 'es': ['Spain'],
- 'gb': ['UK'], 'us': ['USA'], 'all': []
- }
- COUNTRY_DISPLAY = {k: (v[0] if v else "All Countries") for k, v in GROUPS_MAP.items()}
- SORTED_COUNTRY_ITEMS = sorted(
- [(k, f"{k.upper()} — {COUNTRY_DISPLAY.get(k, k)}") for k in GROUPS_MAP.keys()],
- key=lambda x: x[1]
- )
- CATEGORY_COLORS = {
- 'News': '#ffdddd',
- 'Sports': '#ddeeff',
- 'Movies': '#fff4dd',
- 'Documentary': '#ddffdd',
- 'Music': '#f0e6ff',
- 'Other': '#f7f7f7'
- }
- # 1x1 transparent PNG
- PLACEHOLDER_PNG = base64.b64encode(
- b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01'
- b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc`\x00'
- b'\x00\x00\x02\x00\x01\xe2!\xbc\x33\x00\x00\x00\x00IEND\xaeB`\x82'
- ).decode()
- # ---------- Utility Functions ----------
- def load_config() -> Dict:
- if os.path.exists(CONFIG_FILE):
- try:
- with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
- return json.load(f)
- except Exception:
- pass
- return {}
- def save_config(cfg: Dict):
- try:
- with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
- json.dump(cfg, f, indent=2, ensure_ascii=False)
- except Exception as e:
- print("Config save failed:", e)
- def fetch_m3u_text(url: str) -> str:
- r = requests.get(url.strip(), timeout=REQUEST_TIMEOUT)
- r.raise_for_status()
- return r.text
- EXTINF_RE = re.compile(r'#EXTINF:[^\n]*,(?P<title>.*)', re.IGNORECASE)
- TVG_RE = re.compile(r'tvg-logo="(?P<logo>[^"]+)"', re.IGNORECASE)
- GROUP_RE = re.compile(r'group-title="(?P<group>[^"]+)"', re.IGNORECASE)
- def parse_m3u_to_channels(m3u_text: str) -> List[Dict]:
- lines = [ln.rstrip() for ln in m3u_text.splitlines()]
- channels = []
- i = 0
- while i < len(lines):
- line = lines[i]
- if line.upper().startswith('#EXTINF'):
- meta = line
- title_m = EXTINF_RE.search(meta)
- title = title_m.group('title').strip() if title_m else "Unknown"
- group_m = GROUP_RE.search(meta)
- group = group_m.group('group') if group_m else ""
- tvg_m = TVG_RE.search(meta)
- logo = tvg_m.group('logo') if tvg_m else ""
- url = ""
- j = i + 1
- while j < len(lines):
- candidate = lines[j].strip()
- if candidate and not candidate.startswith('#'):
- url = candidate
- break
- j += 1
- channels.append({
- 'title': title,
- 'meta': meta,
- 'url': url,
- 'group': group,
- 'logo': logo,
- 'status': ''
- })
- i = j + 1
- else:
- i += 1
- return channels
- def channels_to_m3u_text(channels: List[Dict], include_epg: bool = False) -> str:
- header = f'#EXTM3U url-tvg="{EPG_URL}" refresh="24"\n' if include_epg else "#EXTM3U\n"
- body = []
- for c in channels:
- meta = c.get('meta') or f'#EXTINF:-1 tvg-name="{c["title"]}" group-title="{c.get("group","")}",{c["title"]}'
- body.append(meta)
- body.append(c['url'])
- return header + "\n".join(body) + "\n"
- # ---------- Main App ----------
- class UltimateIPTVApp:
- def __init__(self, root: tk.Tk):
- self.root = root
- self.root.title("Najeeb Ultimate IPTV Country Playlist Generator")
- self.root.geometry("1280x800")
- self.config = load_config()
- self.config.setdefault('vlc_path', '')
- self.config.setdefault('darkmode', False)
- self.config.setdefault('last_source', '')
- self.vlc_path = self.config['vlc_path']
- self.darkmode = self.config['darkmode']
- self.channels: List[Dict] = []
- self.filtered: List[Dict] = []
- # Setup Style
- if TTB_AVAILABLE:
- self.style = tb.Style(theme='darkly' if self.darkmode else 'flatly')
- self.win = root
- else:
- self.win = root
- self.style = ttk.Style()
- if self.darkmode:
- try:
- self.style.theme_use('clam')
- except Exception:
- pass
- self._build_ui()
- self._load_last_source()
- if VLC_AVAILABLE:
- self._init_vlc()
- self.win.protocol("WM_DELETE_WINDOW", self.on_close)
- def _load_last_source(self):
- last = self.config.get('last_source', '')
- if not last:
- return
- try:
- if last.startswith(('http://', 'https://')):
- txt = fetch_m3u_text(last)
- self.channels = parse_m3u_to_channels(txt)
- elif os.path.exists(last):
- with open(last, 'r', encoding='utf-8', errors='ignore') as f:
- txt = f.read()
- self.channels = parse_m3u_to_channels(txt)
- self.filtered = list(self.channels)
- self._populate_table(self.filtered)
- except Exception as e:
- messagebox.showwarning("Auto-load Failed", f"Could not reload last source:\n{str(e)}")
- def _build_ui(self):
- main_pane = ttk.PanedWindow(self.win, orient='horizontal')
- main_pane.pack(fill='both', expand=True, padx=6, pady=6)
- # === Left Panel: Countries ===
- left_frame = ttk.Frame(main_pane, width=250)
- main_pane.add(left_frame, weight=1)
- ttk.Label(left_frame, text="Countries (Ctrl+Click)").pack(anchor='w', pady=(0,2))
- self.country_filter_var = tk.StringVar()
- self.country_filter_var.trace_add('write', self._filter_country_list)
- ttk.Entry(left_frame, textvariable=self.country_filter_var).pack(fill='x', padx=4, pady=2)
- self.country_listbox = tk.Listbox(left_frame, selectmode='extended', height=20)
- self.country_listbox.pack(fill='both', expand=True, padx=4, pady=2)
- for _, disp in SORTED_COUNTRY_ITEMS:
- self.country_listbox.insert('end', disp)
- btns = ttk.Frame(left_frame)
- btns.pack(fill='x', pady=4)
- ttk.Button(btns, text="Select All", command=lambda: self.country_listbox.select_set(0,'end')).pack(side='left', padx=2)
- ttk.Button(btns, text="Clear", command=lambda: self.country_listbox.selection_clear(0,'end')).pack(side='left', padx=2)
- ttk.Button(btns, text="Local", command=self.on_load_local_m3u).pack(side='left', padx=4)
- ttk.Button(btns, text="Remove Dead", command=self.on_auto_remove_dead).pack(side='left', padx=4)
- # === Right Panel: Controls, Player, Table ===
- right_frame = ttk.Frame(main_pane)
- main_pane.add(right_frame, weight=4)
- # --- Top Control Bar ---
- top = ttk.Frame(right_frame)
- top.pack(fill='x', padx=6, pady=4)
- ttk.Button(top, text="Fetch def", command=self.on_fetch_remote).pack(side='left', padx=2)
- ttk.Label(top, text="URL:").pack(side='left', padx=(10,2))
- self.custom_url_var = tk.StringVar(value=DEFAULT_REMOTE_URL2)
- self.url_entry = ttk.Entry(top, textvariable=self.custom_url_var, width=35)
- self.url_entry.pack(side='left', padx=2)
- ttk.Button(top, text="Load URL", command=self.on_load_from_url).pack(side='left', padx=4)
- ttk.Button(top, text="Generate", command=self.on_generate).pack(side='left', padx=4)
- ttk.Button(top, text="Merge", command=self.on_generate_merge).pack(side='left', padx=2)
- ttk.Button(top, text="Check", command=self.on_check_urls).pack(side='left', padx=4)
- ttk.Button(top, text="Save", command=self.on_save_m3u_dialog).pack(side='left', padx=4)
- ttk.Button(top, text="Export", command=self.on_export_json).pack(side='left', padx=4)
- # --- Top Control Bar ---
- top2 = ttk.Frame(right_frame)
- top2.pack(fill='x', padx=6, pady=4)
- # VLC Controls
- ttk.Button(top2, text="Browse VLC", command=self.browse_vlc_path).pack(side='left', padx=4)
- ttk.Button(top2, text="Test VLC", command=self.test_vlc).pack(side='left', padx=2)
- ttk.Button(top2, text="Open in VLC", command=self.open_in_vlc).pack(side='left', padx=4)
- # --- Mini Player Frame ---
- player_frame = ttk.LabelFrame(right_frame, text="Mini VLC Preview")
- player_frame.pack(fill='x', padx=6, pady=4)
- self.player_widget = tk.Frame(player_frame, width=480, height=270, bg='black')
- self.player_widget.pack(padx=4, pady=4)
- self.player_widget.pack_propagate(False)
- pf = ttk.Frame(player_frame)
- pf.pack(fill='x')
- ttk.Button(pf, text="▶ Play Selected", command=self.on_play_selected).pack(side='left', padx=4)
- ttk.Button(pf, text="■ Stop", command=self.vlc_stop).pack(side='left', padx=4)
- ttk.Label(pf, text="VLC Path:").pack(side='left', padx=6)
- self.vlc_entry = ttk.Entry(pf, width=40)
- self.vlc_entry.pack(side='left', padx=4)
- if self.vlc_path:
- self.vlc_entry.insert(0, self.vlc_path)
- ttk.Button(pf, text="Save", command=self.on_save_settings).pack(side='left', padx=4)
- self.dark_var = tk.BooleanVar(value=self.darkmode)
- ttk.Checkbutton(pf, text="Dark Mode", variable=self.dark_var, command=self._toggle_dark).pack(side='right', padx=4)
- # --- Search & Status ---
- mid = ttk.Frame(right_frame)
- mid.pack(fill='x', padx=6, pady=2)
- ttk.Label(mid, text="Search:").pack(side='left')
- self.search_var = tk.StringVar()
- self.search_var.trace_add('write', lambda *a: self.apply_search_filter())
- ttk.Entry(mid, textvariable=self.search_var, width=40).pack(side='left', padx=4)
- self.status_label = ttk.Label(mid, text="Channels: 0 | Visible: 0")
- self.status_label.pack(side='right')
- # --- Channel Table ---
- table_frame = ttk.Frame(right_frame)
- table_frame.pack(fill='both', expand=True, padx=6, pady=6)
- cols = ('title', 'group', 'url', 'status')
- self.tree = ttk.Treeview(table_frame, columns=cols, show='headings', height=18)
- for col in cols:
- self.tree.heading(col, text=col.title())
- self.tree.column('title', width=300)
- self.tree.column('group', width=150)
- self.tree.column('url', width=450)
- self.tree.column('status', width=100)
- vs = ttk.Scrollbar(table_frame, orient='vertical', command=self.tree.yview)
- hs = ttk.Scrollbar(table_frame, orient='horizontal', command=self.tree.xview)
- self.tree.configure(yscrollcommand=vs.set, xscrollcommand=hs.set)
- self.tree.grid(row=0, column=0, sticky='nsew')
- vs.grid(row=0, column=1, sticky='ns')
- hs.grid(row=1, column=0, sticky='ew')
- table_frame.rowconfigure(0, weight=1)
- table_frame.columnconfigure(0, weight=1)
- # Tag styles
- for tag, color in CATEGORY_COLORS.items():
- self.tree.tag_configure(tag, background=color)
- # Context menu & bindings
- self._build_context_menu()
- self.tree.bind("<Double-1>", lambda e: self.on_play_selected())
- # --- Progress Bar ---
- bottom = ttk.Frame(right_frame)
- bottom.pack(fill='x', padx=6, pady=4)
- self.progress_var = tk.IntVar(value=0)
- self.progress_bar = ttk.Progressbar(bottom, variable=self.progress_var, maximum=100)
- self.progress_bar.pack(side='left', fill='x', expand=True, padx=(0,8))
- self.progress_label = ttk.Label(bottom, text="Ready")
- self.progress_label.pack(side='left')
- def _build_context_menu(self):
- self.context = tk.Menu(self.win, tearoff=0)
- self.context.add_command(label="Play in VLC", command=self.open_in_vlc)
- self.context.add_command(label="Copy URL", command=self.copy_url)
- self.context.add_command(label="Remove", command=self.remove_selected_row)
- self.tree.bind("<Button-3>", self.show_context_menu)
- def show_context_menu(self, event):
- row = self.tree.identify_row(event.y)
- if row:
- self.tree.selection_set(row)
- self.context.tk_popup(event.x_root, event.y_root)
- self.context.grab_release()
- # ========== VLC Integration ==========
- def _init_vlc(self):
- try:
- self.vlc_instance = vlc.Instance("--no-xlib")
- self.vlc_player = self.vlc_instance.media_player_new()
- self._attach_vlc()
- except Exception as e:
- print("VLC init failed:", e)
- self.vlc_player = None
- def _attach_vlc(self):
- if not self.vlc_player:
- return
- try:
- win_id = self.player_widget.winfo_id()
- if sys.platform == 'win32':
- self.vlc_player.set_hwnd(win_id)
- elif sys.platform.startswith('linux'):
- self.vlc_player.set_xwindow(win_id)
- elif sys.platform == 'darwin':
- self.vlc_player.set_nsobject(win_id)
- except Exception:
- pass
- def vlc_play(self, url):
- if VLC_AVAILABLE and self.vlc_player:
- try:
- media = self.vlc_instance.media_new(url)
- self.vlc_player.set_media(media)
- self._attach_vlc()
- self.vlc_player.play()
- except Exception as e:
- messagebox.showerror("Playback Error", str(e))
- else:
- path = self.get_vlc_path()
- if path:
- try:
- subprocess.Popen([path, url])
- except Exception as e:
- messagebox.showerror("VLC Launch Failed", str(e))
- else:
- webbrowser.open(url)
- def on_play_selected(self):
- sel = self.tree.selection()
- if not sel:
- messagebox.showinfo("No Selection", "Please select a channel to play.")
- return
- url = self.tree.item(sel[0])['values'][2]
- self.vlc_play(url)
- def vlc_stop(self):
- if hasattr(self, 'vlc_player') and self.vlc_player:
- self.vlc_player.stop()
- # ========== Loading Sources ==========
- def on_load_local_m3u(self):
- path = filedialog.askopenfilename(filetypes=[("M3U Files", "*.m3u*"), ("All", "*.*")])
- if not path:
- return
- try:
- with open(path, 'r', encoding='utf-8', errors='ignore') as f:
- txt = f.read()
- self.channels = parse_m3u_to_channels(txt)
- self.filtered = list(self.channels)
- self._populate_table(self.filtered)
- self.config['last_source'] = path
- save_config(self.config)
- self.progress_label.config(text=f"Loaded {len(self.channels)} channels from file")
- except Exception as e:
- messagebox.showerror("Load Error", str(e))
- def on_fetch_remote(self):
- self._load_from_url(DEFAULT_REMOTE_URL)
- def on_load_from_url(self):
- url = self.custom_url_var.get().strip()
- if not url:
- messagebox.showwarning("Empty", "Please enter a valid URL.")
- return
- if not url.lower().startswith(('http://', 'https://')):
- messagebox.showwarning("Invalid", "URL must start with http:// or https://")
- return
- self._load_from_url(url)
- def _load_from_url(self, url):
- self.progress_label.config(text="Fetching playlist...")
- self.root.update_idletasks()
- try:
- txt = fetch_m3u_text(url)
- self.channels = parse_m3u_to_channels(txt)
- self.filtered = list(self.channels)
- self._populate_table(self.filtered)
- self.config['last_source'] = url
- save_config(self.config)
- self.progress_label.config(text=f"Fetched {len(self.channels)} channels")
- except Exception as e:
- messagebox.showerror("Fetch Failed", f"Error: {e}")
- # ========== Filtering & Generation ==========
- def _filter_country_list(self, *args):
- query = self.country_filter_var.get().lower()
- self.country_listbox.delete(0, 'end')
- for key, disp in SORTED_COUNTRY_ITEMS:
- if not query or query in key.lower() or query in disp.lower():
- self.country_listbox.insert('end', disp)
- def on_generate(self):
- sels = self.country_listbox.curselection()
- keys = [SORTED_COUNTRY_ITEMS[i][0] for i in sels] if sels else ['it']
- if 'all' in keys:
- filtered = self.channels
- else:
- allowed = {g for k in keys for g in GROUPS_MAP[k]}
- filtered = [c for c in self.channels if c['group'] in allowed]
- self.filtered = filtered
- self._populate_table(filtered)
- def on_generate_merge(self):
- self.on_generate()
- def apply_search_filter(self):
- q = self.search_var.get().lower()
- if not q:
- self.filtered = list(self.channels)
- else:
- self.filtered = [
- c for c in self.channels
- if q in c['title'].lower() or q in c['group'].lower() or q in c['url'].lower()
- ]
- self._populate_table(self.filtered)
- def _populate_table(self, chans):
- for item in self.tree.get_children():
- self.tree.delete(item)
- for c in chans:
- tag = self._get_category_tag(c['group'])
- self.tree.insert('', 'end', values=(c['title'], c['group'], c['url'], c.get('status','')), tags=(tag,))
- self.update_status_counts()
- def _get_category_tag(self, group):
- g = group.lower()
- if 'news' in g: return 'News'
- if 'sport' in g: return 'Sports'
- if 'movie' in g or 'vod' in g: return 'Movies'
- if 'doc' in g: return 'Documentary'
- if 'music' in g: return 'Music'
- return 'Other'
- def update_status_counts(self):
- self.status_label.config(text=f"Channels: {len(self.channels)} | Visible: {len(self.filtered)}")
- # ========== URL Checking ==========
- def on_check_urls(self):
- urls = [self.tree.item(i)['values'][2] for i in self.tree.get_children()]
- if not urls:
- messagebox.showinfo("Empty", "No streams to check.")
- return
- self.progress_bar.config(maximum=len(urls))
- self.progress_var.set(0)
- self.progress_label.config(text="Checking...")
- def check(u):
- try:
- r = requests.head(u, timeout=8, allow_redirects=True)
- if r.status_code in (405, 403):
- r = requests.get(u, timeout=8, stream=True)
- return u, r.status_code if 200 <= r.status_code < 400 else False
- except Exception:
- return u, False
- def worker():
- with ThreadPoolExecutor(max_workers=MAX_THREADS) as ex:
- futures = [ex.submit(check, u) for u in urls]
- for future in as_completed(futures):
- url, ok = future.result()
- status = "OK" if ok else "DEAD"
- for item in self.tree.get_children():
- if self.tree.item(item)['values'][2] == url:
- self.tree.set(item, 'status', status)
- break
- self.progress_var.set(self.progress_var.get() + 1)
- self.progress_label.config(text="Check complete.")
- threading.Thread(target=worker, daemon=True).start()
- def on_auto_remove_dead(self):
- self.on_check_urls()
- if messagebox.askyesno("Remove Dead?", "Now remove all DEAD channels?"):
- good_urls = {self.tree.item(i)['values'][2] for i in self.tree.get_children() if "DEAD" not in self.tree.item(i)['values'][3]}
- self.channels = [c for c in self.channels if c['url'] in good_urls]
- self.filtered = [c for c in self.filtered if c['url'] in good_urls]
- self._populate_table(self.filtered)
- messagebox.showinfo("Cleanup", "Dead channels removed.")
- # ========== Save & Export ==========
- def on_save_m3u_dialog(self):
- if not self.filtered:
- messagebox.showinfo("Empty", "No channels to save.")
- return
- path = filedialog.asksaveasfilename(defaultextension=".m3u8", filetypes=[("M3U8", "*.m3u8"), ("M3U", "*.m3u")])
- if not path: return
- try:
- content = channels_to_m3u_text(self.filtered, include_epg=True)
- with open(path, 'w', encoding='utf-8') as f:
- f.write(content)
- messagebox.showinfo("Saved", f"Playlist saved:\n{path}")
- except Exception as e:
- messagebox.showerror("Save Failed", str(e))
- def on_export_json(self):
- if not self.filtered:
- messagebox.showinfo("Empty", "Nothing to export.")
- return
- path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON", "*.json")])
- if not path: return
- try:
- with open(path, 'w', encoding='utf-8') as f:
- json.dump(self.filtered, f, indent=2, ensure_ascii=False)
- messagebox.showinfo("Exported", f"Saved as JSON:\n{path}")
- except Exception as e:
- messagebox.showerror("Export Failed", str(e))
- # ========== VLC Path Management ==========
- def browse_vlc_path(self):
- path = filedialog.askopenfilename(title="Select VLC.exe", filetypes=[("Executable", "*.exe"), ("All", "*.*")])
- if path:
- self.vlc_entry.delete(0, 'end')
- self.vlc_entry.insert(0, path)
- self.config['vlc_path'] = path
- save_config(self.config)
- def get_vlc_path(self) -> Optional[str]:
- path = self.vlc_entry.get().strip()
- if os.path.isfile(path):
- return path
- auto = self.auto_detect_vlc()
- if auto:
- self.vlc_entry.delete(0, 'end')
- self.vlc_entry.insert(0, auto)
- self.config['vlc_path'] = auto
- save_config(self.config)
- return auto
- if messagebox.askyesno("Not Found", "VLC not found. Open download page?\nVersion: 3.0.21 • Windows • 38 MB"):
- webbrowser.open("https://www.videolan.org/vlc/")
- return None
- def auto_detect_vlc(self) -> Optional[str]:
- paths = [
- r"C:\Program Files\VideoLAN\VLC\vlc.exe",
- r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
- shutil.which("vlc"),
- "/Applications/VLC.app/Contents/MacOS/VLC"
- ]
- for p in paths:
- if p and os.path.isfile(p):
- return os.path.normpath(p)
- return None
- def test_vlc(self):
- path = self.get_vlc_path()
- if not path:
- return
- try:
- subprocess.run([path, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- messagebox.showinfo("Success", "✅ VLC is working!")
- except Exception:
- messagebox.showerror("Failed", "❌ Could not launch VLC.")
- def open_in_vlc(self):
- sel = self.tree.selection()
- if not sel:
- messagebox.showinfo("Select", "Choose a channel first.")
- return
- url = self.tree.item(sel[0])['values'][2]
- path = self.get_vlc_path()
- if path:
- try:
- subprocess.Popen([path, url])
- except Exception as e:
- messagebox.showerror("Error", str(e))
- # ========== Utilities ==========
- def copy_url(self):
- sel = self.tree.selection()
- if sel:
- url = self.tree.item(sel[0])['values'][2]
- self.root.clipboard_clear()
- self.root.clipboard_append(url)
- self.progress_label.config(text="URL copied!")
- def remove_selected_row(self):
- sel = self.tree.selection()
- if not sel: return
- url = self.tree.item(sel[0])['values'][2]
- self.channels = [c for c in self.channels if c['url'] != url]
- self.filtered = [c for c in self.filtered if c['url'] != url]
- self._populate_table(self.filtered)
- def on_save_settings(self):
- self.config['vlc_path'] = self.vlc_entry.get().strip()
- self.config['darkmode'] = self.dark_var.get()
- save_config(self.config)
- messagebox.showinfo("Settings", "Saved successfully.")
- def _toggle_dark(self):
- theme = 'darkly' if self.dark_var.get() else 'flatly'
- if TTB_AVAILABLE:
- try:
- self.style.theme_use(theme)
- except Exception:
- pass
- else:
- messagebox.showinfo("Theme", "Install 'ttkbootstrap' for better dark mode.")
- def on_close(self):
- self.config['vlc_path'] = self.vlc_entry.get().strip()
- self.config['darkmode'] = self.dark_var.get()
- save_config(self.config)
- if hasattr(self, 'vlc_player'):
- self.vlc_player.stop()
- self.win.destroy()
- # ========== Run ==========
- def main():
- print("📌 Najeeb Ultimate IPTV Tool - Ready")
- print("💡 Tip: Install 'python-vlc' and VLC for embedded preview.")
- print("🌐 VLC Download: https://www.videolan.org/vlc/ (v3.0.21 • 38 MB)")
- root = tk.Tk()
- app = UltimateIPTVApp(root)
- root.mainloop()
- if __name__ == "__main__":
- main()
Add Comment
Please, Sign In to add comment