Najeebsk

Ultimate-IPTV-Country-Playlist.py

Dec 7th, 2025
18
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 27.41 KB | None | 0 0
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. """
  4. Najeeb Ultimate IPTV Country Playlist Generator
  5. Features:
  6. - Load local M3U / M3U8 files
  7. - Fetch remote playlists from custom URLs
  8. - Filter by country (Albania, Italy, USA, etc.)
  9. - Search, Check URLs, Remove dead channels
  10. - Save as M3U/M3U8 or Export JSON
  11. - Play in embedded mini VLC or external VLC
  12. - Dark/Light mode with ttkbootstrap
  13. """
  14.  
  15. from __future__ import annotations
  16. import os
  17. import sys
  18. import json
  19. import re
  20. import threading
  21. import subprocess
  22. import webbrowser
  23. import shutil
  24. from concurrent.futures import ThreadPoolExecutor, as_completed
  25. from typing import List, Dict, Optional
  26. import base64
  27. import io
  28.  
  29. # Required: pip install requests ttkbootstrap pillow python-vlc
  30. try:
  31.     import requests
  32. except Exception:
  33.     raise SystemExit("Install: pip install requests")
  34.  
  35. try:
  36.     import vlc
  37.     VLC_AVAILABLE = True
  38. except Exception:
  39.     VLC_AVAILABLE = False
  40.  
  41. try:
  42.     import ttkbootstrap as tb
  43.     from ttkbootstrap.constants import *
  44.     TTB_AVAILABLE = True
  45. except Exception:
  46.     tb = None
  47.     TTB_AVAILABLE = False
  48.  
  49. import tkinter as tk
  50. from tkinter import ttk, filedialog, messagebox
  51.  
  52. # ---------- Config & Constants ----------
  53. CONFIG_FILE = "iptv_ultimate_config.json"
  54. DEFAULT_REMOTE_URL2 = "https://iptv-org.github.io/iptv/index.m3u"
  55. DEFAULT_REMOTE_URL = "https://raw.githubusercontent.com/Free-TV/IPTV/master/playlist.m3u8"
  56. EPG_URL = "https://tvit.leicaflorianrobert.dev/epg/list.xml"
  57. REQUEST_TIMEOUT = 10
  58. MAX_THREADS = 25
  59.  
  60. GROUPS_MAP = {
  61.     'al': ['Albania'], 'ad': ['Andorra'], 'ar': ['Argentina'], 'am': ['Armenia'],
  62.     'au': ['Australia'], 'at': ['Austria'], 'az': ['Azerbaijan'], 'by': ['Belarus'],
  63.     'be': ['Belgium'], 'ba': ['Bosnia and Herzegovina'], 'br': ['Brazil'],
  64.     'bg': ['Bulgaria'], 'ca': ['Canada'], 'cl': ['Chile'], 'cn': ['China'],
  65.     'hr': ['Croatia'], 'cy': ['Cyprus'], 'cz': ['Czech Republic'], 'dk': ['Denmark'],
  66.     'eg': ['Egypt'], 'fi': ['Finland'], 'fr': ['France'], 'de': ['Germany'],
  67.     'gr': ['Greece'], 'hk': ['Hong Kong'], 'hu': ['Hungary'], 'in': ['India'],
  68.     'it': ['Italy', 'VOD Italy'], 'jp': ['Japan'], 'kr': ['Korea'], 'es': ['Spain'],
  69.     'gb': ['UK'], 'us': ['USA'], 'all': []
  70. }
  71.  
  72. COUNTRY_DISPLAY = {k: (v[0] if v else "All Countries") for k, v in GROUPS_MAP.items()}
  73. SORTED_COUNTRY_ITEMS = sorted(
  74.     [(k, f"{k.upper()} — {COUNTRY_DISPLAY.get(k, k)}") for k in GROUPS_MAP.keys()],
  75.     key=lambda x: x[1]
  76. )
  77.  
  78. CATEGORY_COLORS = {
  79.     'News': '#ffdddd',
  80.     'Sports': '#ddeeff',
  81.     'Movies': '#fff4dd',
  82.     'Documentary': '#ddffdd',
  83.     'Music': '#f0e6ff',
  84.     'Other': '#f7f7f7'
  85. }
  86.  
  87. # 1x1 transparent PNG
  88. PLACEHOLDER_PNG = base64.b64encode(
  89.     b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01'
  90.     b'\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc`\x00'
  91.     b'\x00\x00\x02\x00\x01\xe2!\xbc\x33\x00\x00\x00\x00IEND\xaeB`\x82'
  92. ).decode()
  93.  
  94. # ---------- Utility Functions ----------
  95. def load_config() -> Dict:
  96.     if os.path.exists(CONFIG_FILE):
  97.         try:
  98.             with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
  99.                 return json.load(f)
  100.         except Exception:
  101.             pass
  102.     return {}
  103.  
  104. def save_config(cfg: Dict):
  105.     try:
  106.         with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
  107.             json.dump(cfg, f, indent=2, ensure_ascii=False)
  108.     except Exception as e:
  109.         print("Config save failed:", e)
  110.  
  111. def fetch_m3u_text(url: str) -> str:
  112.     r = requests.get(url.strip(), timeout=REQUEST_TIMEOUT)
  113.     r.raise_for_status()
  114.     return r.text
  115.  
  116. EXTINF_RE = re.compile(r'#EXTINF:[^\n]*,(?P<title>.*)', re.IGNORECASE)
  117. TVG_RE = re.compile(r'tvg-logo="(?P<logo>[^"]+)"', re.IGNORECASE)
  118. GROUP_RE = re.compile(r'group-title="(?P<group>[^"]+)"', re.IGNORECASE)
  119.  
  120. def parse_m3u_to_channels(m3u_text: str) -> List[Dict]:
  121.     lines = [ln.rstrip() for ln in m3u_text.splitlines()]
  122.     channels = []
  123.     i = 0
  124.     while i < len(lines):
  125.         line = lines[i]
  126.         if line.upper().startswith('#EXTINF'):
  127.             meta = line
  128.             title_m = EXTINF_RE.search(meta)
  129.             title = title_m.group('title').strip() if title_m else "Unknown"
  130.             group_m = GROUP_RE.search(meta)
  131.             group = group_m.group('group') if group_m else ""
  132.             tvg_m = TVG_RE.search(meta)
  133.             logo = tvg_m.group('logo') if tvg_m else ""
  134.             url = ""
  135.             j = i + 1
  136.             while j < len(lines):
  137.                 candidate = lines[j].strip()
  138.                 if candidate and not candidate.startswith('#'):
  139.                     url = candidate
  140.                     break
  141.                 j += 1
  142.             channels.append({
  143.                 'title': title,
  144.                 'meta': meta,
  145.                 'url': url,
  146.                 'group': group,
  147.                 'logo': logo,
  148.                 'status': ''
  149.             })
  150.             i = j + 1
  151.         else:
  152.             i += 1
  153.     return channels
  154.  
  155. def channels_to_m3u_text(channels: List[Dict], include_epg: bool = False) -> str:
  156.     header = f'#EXTM3U url-tvg="{EPG_URL}" refresh="24"\n' if include_epg else "#EXTM3U\n"
  157.     body = []
  158.     for c in channels:
  159.         meta = c.get('meta') or f'#EXTINF:-1 tvg-name="{c["title"]}" group-title="{c.get("group","")}",{c["title"]}'
  160.         body.append(meta)
  161.         body.append(c['url'])
  162.     return header + "\n".join(body) + "\n"
  163.  
  164. # ---------- Main App ----------
  165. class UltimateIPTVApp:
  166.     def __init__(self, root: tk.Tk):
  167.         self.root = root
  168.         self.root.title("Najeeb Ultimate IPTV Country Playlist Generator")
  169.         self.root.geometry("1280x800")
  170.         self.config = load_config()
  171.         self.config.setdefault('vlc_path', '')
  172.         self.config.setdefault('darkmode', False)
  173.         self.config.setdefault('last_source', '')
  174.  
  175.         self.vlc_path = self.config['vlc_path']
  176.         self.darkmode = self.config['darkmode']
  177.         self.channels: List[Dict] = []
  178.         self.filtered: List[Dict] = []
  179.  
  180.         # Setup Style
  181.         if TTB_AVAILABLE:
  182.             self.style = tb.Style(theme='darkly' if self.darkmode else 'flatly')
  183.             self.win = root
  184.         else:
  185.             self.win = root
  186.             self.style = ttk.Style()
  187.             if self.darkmode:
  188.                 try:
  189.                     self.style.theme_use('clam')
  190.                 except Exception:
  191.                     pass
  192.  
  193.         self._build_ui()
  194.         self._load_last_source()
  195.         if VLC_AVAILABLE:
  196.             self._init_vlc()
  197.         self.win.protocol("WM_DELETE_WINDOW", self.on_close)
  198.  
  199.     def _load_last_source(self):
  200.         last = self.config.get('last_source', '')
  201.         if not last:
  202.             return
  203.         try:
  204.             if last.startswith(('http://', 'https://')):
  205.                 txt = fetch_m3u_text(last)
  206.                 self.channels = parse_m3u_to_channels(txt)
  207.             elif os.path.exists(last):
  208.                 with open(last, 'r', encoding='utf-8', errors='ignore') as f:
  209.                     txt = f.read()
  210.                 self.channels = parse_m3u_to_channels(txt)
  211.             self.filtered = list(self.channels)
  212.             self._populate_table(self.filtered)
  213.         except Exception as e:
  214.             messagebox.showwarning("Auto-load Failed", f"Could not reload last source:\n{str(e)}")
  215.  
  216.     def _build_ui(self):
  217.         main_pane = ttk.PanedWindow(self.win, orient='horizontal')
  218.         main_pane.pack(fill='both', expand=True, padx=6, pady=6)
  219.  
  220.         # === Left Panel: Countries ===
  221.         left_frame = ttk.Frame(main_pane, width=250)
  222.         main_pane.add(left_frame, weight=1)
  223.  
  224.         ttk.Label(left_frame, text="Countries (Ctrl+Click)").pack(anchor='w', pady=(0,2))
  225.         self.country_filter_var = tk.StringVar()
  226.         self.country_filter_var.trace_add('write', self._filter_country_list)
  227.         ttk.Entry(left_frame, textvariable=self.country_filter_var).pack(fill='x', padx=4, pady=2)
  228.  
  229.         self.country_listbox = tk.Listbox(left_frame, selectmode='extended', height=20)
  230.         self.country_listbox.pack(fill='both', expand=True, padx=4, pady=2)
  231.         for _, disp in SORTED_COUNTRY_ITEMS:
  232.             self.country_listbox.insert('end', disp)
  233.  
  234.         btns = ttk.Frame(left_frame)
  235.         btns.pack(fill='x', pady=4)
  236.         ttk.Button(btns, text="Select All", command=lambda: self.country_listbox.select_set(0,'end')).pack(side='left', padx=2)
  237.         ttk.Button(btns, text="Clear", command=lambda: self.country_listbox.selection_clear(0,'end')).pack(side='left', padx=2)
  238.         ttk.Button(btns, text="Local", command=self.on_load_local_m3u).pack(side='left', padx=4)
  239.         ttk.Button(btns, text="Remove Dead", command=self.on_auto_remove_dead).pack(side='left', padx=4)
  240.  
  241.         # === Right Panel: Controls, Player, Table ===
  242.         right_frame = ttk.Frame(main_pane)
  243.         main_pane.add(right_frame, weight=4)
  244.  
  245.         # --- Top Control Bar ---
  246.         top = ttk.Frame(right_frame)
  247.         top.pack(fill='x', padx=6, pady=4)
  248.  
  249.         ttk.Button(top, text="Fetch def", command=self.on_fetch_remote).pack(side='left', padx=2)
  250.        
  251.         ttk.Label(top, text="URL:").pack(side='left', padx=(10,2))
  252.         self.custom_url_var = tk.StringVar(value=DEFAULT_REMOTE_URL2)
  253.         self.url_entry = ttk.Entry(top, textvariable=self.custom_url_var, width=35)
  254.         self.url_entry.pack(side='left', padx=2)
  255.         ttk.Button(top, text="Load URL", command=self.on_load_from_url).pack(side='left', padx=4)
  256.  
  257.         ttk.Button(top, text="Generate", command=self.on_generate).pack(side='left', padx=4)
  258.         ttk.Button(top, text="Merge", command=self.on_generate_merge).pack(side='left', padx=2)
  259.         ttk.Button(top, text="Check", command=self.on_check_urls).pack(side='left', padx=4)
  260.         ttk.Button(top, text="Save", command=self.on_save_m3u_dialog).pack(side='left', padx=4)
  261.         ttk.Button(top, text="Export", command=self.on_export_json).pack(side='left', padx=4)
  262.         # --- Top Control Bar ---
  263.         top2 = ttk.Frame(right_frame)
  264.         top2.pack(fill='x', padx=6, pady=4)
  265.         # VLC Controls
  266.         ttk.Button(top2, text="Browse VLC", command=self.browse_vlc_path).pack(side='left', padx=4)
  267.         ttk.Button(top2, text="Test VLC", command=self.test_vlc).pack(side='left', padx=2)
  268.         ttk.Button(top2, text="Open in VLC", command=self.open_in_vlc).pack(side='left', padx=4)
  269.  
  270.         # --- Mini Player Frame ---
  271.         player_frame = ttk.LabelFrame(right_frame, text="Mini VLC Preview")
  272.         player_frame.pack(fill='x', padx=6, pady=4)
  273.         self.player_widget = tk.Frame(player_frame, width=480, height=270, bg='black')
  274.         self.player_widget.pack(padx=4, pady=4)
  275.         self.player_widget.pack_propagate(False)
  276.  
  277.         pf = ttk.Frame(player_frame)
  278.         pf.pack(fill='x')
  279.         ttk.Button(pf, text="▶ Play Selected", command=self.on_play_selected).pack(side='left', padx=4)
  280.         ttk.Button(pf, text="■ Stop", command=self.vlc_stop).pack(side='left', padx=4)
  281.         ttk.Label(pf, text="VLC Path:").pack(side='left', padx=6)
  282.         self.vlc_entry = ttk.Entry(pf, width=40)
  283.         self.vlc_entry.pack(side='left', padx=4)
  284.         if self.vlc_path:
  285.             self.vlc_entry.insert(0, self.vlc_path)
  286.         ttk.Button(pf, text="Save", command=self.on_save_settings).pack(side='left', padx=4)
  287.         self.dark_var = tk.BooleanVar(value=self.darkmode)
  288.         ttk.Checkbutton(pf, text="Dark Mode", variable=self.dark_var, command=self._toggle_dark).pack(side='right', padx=4)
  289.  
  290.         # --- Search & Status ---
  291.         mid = ttk.Frame(right_frame)
  292.         mid.pack(fill='x', padx=6, pady=2)
  293.         ttk.Label(mid, text="Search:").pack(side='left')
  294.         self.search_var = tk.StringVar()
  295.         self.search_var.trace_add('write', lambda *a: self.apply_search_filter())
  296.         ttk.Entry(mid, textvariable=self.search_var, width=40).pack(side='left', padx=4)
  297.         self.status_label = ttk.Label(mid, text="Channels: 0 | Visible: 0")
  298.         self.status_label.pack(side='right')
  299.  
  300.         # --- Channel Table ---
  301.         table_frame = ttk.Frame(right_frame)
  302.         table_frame.pack(fill='both', expand=True, padx=6, pady=6)
  303.         cols = ('title', 'group', 'url', 'status')
  304.         self.tree = ttk.Treeview(table_frame, columns=cols, show='headings', height=18)
  305.         for col in cols:
  306.             self.tree.heading(col, text=col.title())
  307.         self.tree.column('title', width=300)
  308.         self.tree.column('group', width=150)
  309.         self.tree.column('url', width=450)
  310.         self.tree.column('status', width=100)
  311.         vs = ttk.Scrollbar(table_frame, orient='vertical', command=self.tree.yview)
  312.         hs = ttk.Scrollbar(table_frame, orient='horizontal', command=self.tree.xview)
  313.         self.tree.configure(yscrollcommand=vs.set, xscrollcommand=hs.set)
  314.         self.tree.grid(row=0, column=0, sticky='nsew')
  315.         vs.grid(row=0, column=1, sticky='ns')
  316.         hs.grid(row=1, column=0, sticky='ew')
  317.         table_frame.rowconfigure(0, weight=1)
  318.         table_frame.columnconfigure(0, weight=1)
  319.  
  320.         # Tag styles
  321.         for tag, color in CATEGORY_COLORS.items():
  322.             self.tree.tag_configure(tag, background=color)
  323.  
  324.         # Context menu & bindings
  325.         self._build_context_menu()
  326.         self.tree.bind("<Double-1>", lambda e: self.on_play_selected())
  327.  
  328.         # --- Progress Bar ---
  329.         bottom = ttk.Frame(right_frame)
  330.         bottom.pack(fill='x', padx=6, pady=4)
  331.         self.progress_var = tk.IntVar(value=0)
  332.         self.progress_bar = ttk.Progressbar(bottom, variable=self.progress_var, maximum=100)
  333.         self.progress_bar.pack(side='left', fill='x', expand=True, padx=(0,8))
  334.         self.progress_label = ttk.Label(bottom, text="Ready")
  335.         self.progress_label.pack(side='left')
  336.  
  337.     def _build_context_menu(self):
  338.         self.context = tk.Menu(self.win, tearoff=0)
  339.         self.context.add_command(label="Play in VLC", command=self.open_in_vlc)
  340.         self.context.add_command(label="Copy URL", command=self.copy_url)
  341.         self.context.add_command(label="Remove", command=self.remove_selected_row)
  342.         self.tree.bind("<Button-3>", self.show_context_menu)
  343.  
  344.     def show_context_menu(self, event):
  345.         row = self.tree.identify_row(event.y)
  346.         if row:
  347.             self.tree.selection_set(row)
  348.             self.context.tk_popup(event.x_root, event.y_root)
  349.         self.context.grab_release()
  350.  
  351.     # ========== VLC Integration ==========
  352.     def _init_vlc(self):
  353.         try:
  354.             self.vlc_instance = vlc.Instance("--no-xlib")
  355.             self.vlc_player = self.vlc_instance.media_player_new()
  356.             self._attach_vlc()
  357.         except Exception as e:
  358.             print("VLC init failed:", e)
  359.             self.vlc_player = None
  360.  
  361.     def _attach_vlc(self):
  362.         if not self.vlc_player:
  363.             return
  364.         try:
  365.             win_id = self.player_widget.winfo_id()
  366.             if sys.platform == 'win32':
  367.                 self.vlc_player.set_hwnd(win_id)
  368.             elif sys.platform.startswith('linux'):
  369.                 self.vlc_player.set_xwindow(win_id)
  370.             elif sys.platform == 'darwin':
  371.                 self.vlc_player.set_nsobject(win_id)
  372.         except Exception:
  373.             pass
  374.  
  375.     def vlc_play(self, url):
  376.         if VLC_AVAILABLE and self.vlc_player:
  377.             try:
  378.                 media = self.vlc_instance.media_new(url)
  379.                 self.vlc_player.set_media(media)
  380.                 self._attach_vlc()
  381.                 self.vlc_player.play()
  382.             except Exception as e:
  383.                 messagebox.showerror("Playback Error", str(e))
  384.         else:
  385.             path = self.get_vlc_path()
  386.             if path:
  387.                 try:
  388.                     subprocess.Popen([path, url])
  389.                 except Exception as e:
  390.                     messagebox.showerror("VLC Launch Failed", str(e))
  391.             else:
  392.                 webbrowser.open(url)
  393.  
  394.     def on_play_selected(self):
  395.         sel = self.tree.selection()
  396.         if not sel:
  397.             messagebox.showinfo("No Selection", "Please select a channel to play.")
  398.             return
  399.         url = self.tree.item(sel[0])['values'][2]
  400.         self.vlc_play(url)
  401.  
  402.     def vlc_stop(self):
  403.         if hasattr(self, 'vlc_player') and self.vlc_player:
  404.             self.vlc_player.stop()
  405.  
  406.     # ========== Loading Sources ==========
  407.     def on_load_local_m3u(self):
  408.         path = filedialog.askopenfilename(filetypes=[("M3U Files", "*.m3u*"), ("All", "*.*")])
  409.         if not path:
  410.             return
  411.         try:
  412.             with open(path, 'r', encoding='utf-8', errors='ignore') as f:
  413.                 txt = f.read()
  414.             self.channels = parse_m3u_to_channels(txt)
  415.             self.filtered = list(self.channels)
  416.             self._populate_table(self.filtered)
  417.             self.config['last_source'] = path
  418.             save_config(self.config)
  419.             self.progress_label.config(text=f"Loaded {len(self.channels)} channels from file")
  420.         except Exception as e:
  421.             messagebox.showerror("Load Error", str(e))
  422.  
  423.     def on_fetch_remote(self):
  424.         self._load_from_url(DEFAULT_REMOTE_URL)
  425.  
  426.     def on_load_from_url(self):
  427.         url = self.custom_url_var.get().strip()
  428.         if not url:
  429.             messagebox.showwarning("Empty", "Please enter a valid URL.")
  430.             return
  431.         if not url.lower().startswith(('http://', 'https://')):
  432.             messagebox.showwarning("Invalid", "URL must start with http:// or https://")
  433.             return
  434.         self._load_from_url(url)
  435.  
  436.     def _load_from_url(self, url):
  437.         self.progress_label.config(text="Fetching playlist...")
  438.         self.root.update_idletasks()
  439.         try:
  440.             txt = fetch_m3u_text(url)
  441.             self.channels = parse_m3u_to_channels(txt)
  442.             self.filtered = list(self.channels)
  443.             self._populate_table(self.filtered)
  444.             self.config['last_source'] = url
  445.             save_config(self.config)
  446.             self.progress_label.config(text=f"Fetched {len(self.channels)} channels")
  447.         except Exception as e:
  448.             messagebox.showerror("Fetch Failed", f"Error: {e}")
  449.  
  450.     # ========== Filtering & Generation ==========
  451.     def _filter_country_list(self, *args):
  452.         query = self.country_filter_var.get().lower()
  453.         self.country_listbox.delete(0, 'end')
  454.         for key, disp in SORTED_COUNTRY_ITEMS:
  455.             if not query or query in key.lower() or query in disp.lower():
  456.                 self.country_listbox.insert('end', disp)
  457.  
  458.     def on_generate(self):
  459.         sels = self.country_listbox.curselection()
  460.         keys = [SORTED_COUNTRY_ITEMS[i][0] for i in sels] if sels else ['it']
  461.         if 'all' in keys:
  462.             filtered = self.channels
  463.         else:
  464.             allowed = {g for k in keys for g in GROUPS_MAP[k]}
  465.             filtered = [c for c in self.channels if c['group'] in allowed]
  466.         self.filtered = filtered
  467.         self._populate_table(filtered)
  468.  
  469.     def on_generate_merge(self):
  470.         self.on_generate()
  471.  
  472.     def apply_search_filter(self):
  473.         q = self.search_var.get().lower()
  474.         if not q:
  475.             self.filtered = list(self.channels)
  476.         else:
  477.             self.filtered = [
  478.                 c for c in self.channels
  479.                 if q in c['title'].lower() or q in c['group'].lower() or q in c['url'].lower()
  480.             ]
  481.         self._populate_table(self.filtered)
  482.  
  483.     def _populate_table(self, chans):
  484.         for item in self.tree.get_children():
  485.             self.tree.delete(item)
  486.         for c in chans:
  487.             tag = self._get_category_tag(c['group'])
  488.             self.tree.insert('', 'end', values=(c['title'], c['group'], c['url'], c.get('status','')), tags=(tag,))
  489.         self.update_status_counts()
  490.  
  491.     def _get_category_tag(self, group):
  492.         g = group.lower()
  493.         if 'news' in g: return 'News'
  494.         if 'sport' in g: return 'Sports'
  495.         if 'movie' in g or 'vod' in g: return 'Movies'
  496.         if 'doc' in g: return 'Documentary'
  497.         if 'music' in g: return 'Music'
  498.         return 'Other'
  499.  
  500.     def update_status_counts(self):
  501.         self.status_label.config(text=f"Channels: {len(self.channels)} | Visible: {len(self.filtered)}")
  502.  
  503.     # ========== URL Checking ==========
  504.     def on_check_urls(self):
  505.         urls = [self.tree.item(i)['values'][2] for i in self.tree.get_children()]
  506.         if not urls:
  507.             messagebox.showinfo("Empty", "No streams to check.")
  508.             return
  509.         self.progress_bar.config(maximum=len(urls))
  510.         self.progress_var.set(0)
  511.         self.progress_label.config(text="Checking...")
  512.  
  513.         def check(u):
  514.             try:
  515.                 r = requests.head(u, timeout=8, allow_redirects=True)
  516.                 if r.status_code in (405, 403):
  517.                     r = requests.get(u, timeout=8, stream=True)
  518.                 return u, r.status_code if 200 <= r.status_code < 400 else False
  519.             except Exception:
  520.                 return u, False
  521.  
  522.         def worker():
  523.             with ThreadPoolExecutor(max_workers=MAX_THREADS) as ex:
  524.                 futures = [ex.submit(check, u) for u in urls]
  525.                 for future in as_completed(futures):
  526.                     url, ok = future.result()
  527.                     status = "OK" if ok else "DEAD"
  528.                     for item in self.tree.get_children():
  529.                         if self.tree.item(item)['values'][2] == url:
  530.                             self.tree.set(item, 'status', status)
  531.                             break
  532.                     self.progress_var.set(self.progress_var.get() + 1)
  533.             self.progress_label.config(text="Check complete.")
  534.  
  535.         threading.Thread(target=worker, daemon=True).start()
  536.  
  537.     def on_auto_remove_dead(self):
  538.         self.on_check_urls()
  539.         if messagebox.askyesno("Remove Dead?", "Now remove all DEAD channels?"):
  540.             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]}
  541.             self.channels = [c for c in self.channels if c['url'] in good_urls]
  542.             self.filtered = [c for c in self.filtered if c['url'] in good_urls]
  543.             self._populate_table(self.filtered)
  544.             messagebox.showinfo("Cleanup", "Dead channels removed.")
  545.  
  546.     # ========== Save & Export ==========
  547.     def on_save_m3u_dialog(self):
  548.         if not self.filtered:
  549.             messagebox.showinfo("Empty", "No channels to save.")
  550.             return
  551.         path = filedialog.asksaveasfilename(defaultextension=".m3u8", filetypes=[("M3U8", "*.m3u8"), ("M3U", "*.m3u")])
  552.         if not path: return
  553.         try:
  554.             content = channels_to_m3u_text(self.filtered, include_epg=True)
  555.             with open(path, 'w', encoding='utf-8') as f:
  556.                 f.write(content)
  557.             messagebox.showinfo("Saved", f"Playlist saved:\n{path}")
  558.         except Exception as e:
  559.             messagebox.showerror("Save Failed", str(e))
  560.  
  561.     def on_export_json(self):
  562.         if not self.filtered:
  563.             messagebox.showinfo("Empty", "Nothing to export.")
  564.             return
  565.         path = filedialog.asksaveasfilename(defaultextension=".json", filetypes=[("JSON", "*.json")])
  566.         if not path: return
  567.         try:
  568.             with open(path, 'w', encoding='utf-8') as f:
  569.                 json.dump(self.filtered, f, indent=2, ensure_ascii=False)
  570.             messagebox.showinfo("Exported", f"Saved as JSON:\n{path}")
  571.         except Exception as e:
  572.             messagebox.showerror("Export Failed", str(e))
  573.  
  574.     # ========== VLC Path Management ==========
  575.     def browse_vlc_path(self):
  576.         path = filedialog.askopenfilename(title="Select VLC.exe", filetypes=[("Executable", "*.exe"), ("All", "*.*")])
  577.         if path:
  578.             self.vlc_entry.delete(0, 'end')
  579.             self.vlc_entry.insert(0, path)
  580.             self.config['vlc_path'] = path
  581.             save_config(self.config)
  582.  
  583.     def get_vlc_path(self) -> Optional[str]:
  584.         path = self.vlc_entry.get().strip()
  585.         if os.path.isfile(path):
  586.             return path
  587.         auto = self.auto_detect_vlc()
  588.         if auto:
  589.             self.vlc_entry.delete(0, 'end')
  590.             self.vlc_entry.insert(0, auto)
  591.             self.config['vlc_path'] = auto
  592.             save_config(self.config)
  593.             return auto
  594.         if messagebox.askyesno("Not Found", "VLC not found. Open download page?\nVersion: 3.0.21 • Windows • 38 MB"):
  595.             webbrowser.open("https://www.videolan.org/vlc/")
  596.         return None
  597.  
  598.     def auto_detect_vlc(self) -> Optional[str]:
  599.         paths = [
  600.             r"C:\Program Files\VideoLAN\VLC\vlc.exe",
  601.             r"C:\Program Files (x86)\VideoLAN\VLC\vlc.exe",
  602.             shutil.which("vlc"),
  603.             "/Applications/VLC.app/Contents/MacOS/VLC"
  604.         ]
  605.         for p in paths:
  606.             if p and os.path.isfile(p):
  607.                 return os.path.normpath(p)
  608.         return None
  609.  
  610.     def test_vlc(self):
  611.         path = self.get_vlc_path()
  612.         if not path:
  613.             return
  614.         try:
  615.             subprocess.run([path, '--version'], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  616.             messagebox.showinfo("Success", "✅ VLC is working!")
  617.         except Exception:
  618.             messagebox.showerror("Failed", "❌ Could not launch VLC.")
  619.  
  620.     def open_in_vlc(self):
  621.         sel = self.tree.selection()
  622.         if not sel:
  623.             messagebox.showinfo("Select", "Choose a channel first.")
  624.             return
  625.         url = self.tree.item(sel[0])['values'][2]
  626.         path = self.get_vlc_path()
  627.         if path:
  628.             try:
  629.                 subprocess.Popen([path, url])
  630.             except Exception as e:
  631.                 messagebox.showerror("Error", str(e))
  632.  
  633.     # ========== Utilities ==========
  634.     def copy_url(self):
  635.         sel = self.tree.selection()
  636.         if sel:
  637.             url = self.tree.item(sel[0])['values'][2]
  638.             self.root.clipboard_clear()
  639.             self.root.clipboard_append(url)
  640.             self.progress_label.config(text="URL copied!")
  641.  
  642.     def remove_selected_row(self):
  643.         sel = self.tree.selection()
  644.         if not sel: return
  645.         url = self.tree.item(sel[0])['values'][2]
  646.         self.channels = [c for c in self.channels if c['url'] != url]
  647.         self.filtered = [c for c in self.filtered if c['url'] != url]
  648.         self._populate_table(self.filtered)
  649.  
  650.     def on_save_settings(self):
  651.         self.config['vlc_path'] = self.vlc_entry.get().strip()
  652.         self.config['darkmode'] = self.dark_var.get()
  653.         save_config(self.config)
  654.         messagebox.showinfo("Settings", "Saved successfully.")
  655.  
  656.     def _toggle_dark(self):
  657.         theme = 'darkly' if self.dark_var.get() else 'flatly'
  658.         if TTB_AVAILABLE:
  659.             try:
  660.                 self.style.theme_use(theme)
  661.             except Exception:
  662.                 pass
  663.         else:
  664.             messagebox.showinfo("Theme", "Install 'ttkbootstrap' for better dark mode.")
  665.  
  666.     def on_close(self):
  667.         self.config['vlc_path'] = self.vlc_entry.get().strip()
  668.         self.config['darkmode'] = self.dark_var.get()
  669.         save_config(self.config)
  670.         if hasattr(self, 'vlc_player'):
  671.             self.vlc_player.stop()
  672.         self.win.destroy()
  673.  
  674. # ========== Run ==========
  675. def main():
  676.     print("📌 Najeeb Ultimate IPTV Tool - Ready")
  677.     print("💡 Tip: Install 'python-vlc' and VLC for embedded preview.")
  678.     print("🌐 VLC Download: https://www.videolan.org/vlc/  (v3.0.21 • 38 MB)")
  679.     root = tk.Tk()
  680.     app = UltimateIPTVApp(root)
  681.     root.mainloop()
  682.  
  683. if __name__ == "__main__":
  684.     main()
  685.  
Add Comment
Please, Sign In to add comment