Najeebsk

NSK-IPTV-PLAYER.pyw

Apr 21st, 2025 (edited)
21
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 48.11 KB | None | 0 0
  1. import sys
  2. import os
  3. import requests
  4. import time
  5. import hashlib
  6. import json
  7. import vlc
  8. from PyQt5.QtWidgets import QMenu, QAction  # Add these imports at the top
  9. from PyQt5.QtWidgets import QFileDialog  # Add this import at the top
  10. from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
  11.                              QHBoxLayout, QPushButton, QLabel, QLineEdit,
  12.                              QTabWidget, QMessageBox, QProgressBar, QDialog,
  13.                              QListWidget, QListWidgetItem, QTreeWidget, QTreeWidgetItem,
  14.                              QScrollArea, QFrame, QSlider, QInputDialog)
  15. from PyQt5.QtGui import QFont
  16. from PyQt5.QtCore import Qt, QThread, pyqtSignal
  17. from PyQt5.QtCore import QTimer
  18. from PyQt5.QtMultimedia import QMediaPlayer, QMediaContent
  19. from PyQt5.QtMultimediaWidgets import QVideoWidget
  20. from PyQt5.QtCore import QUrl
  21. from PyQt5.QtWidgets import QSizePolicy
  22. import re
  23.  
  24. class DownloadWorker(QThread):
  25.     progress = pyqtSignal(int, str, str)  # progress, speed, time remaining
  26.     finished = pyqtSignal(str)
  27.     error = pyqtSignal(str)
  28.  
  29.     def __init__(self, url, save_path):
  30.         super().__init__()
  31.         self.url = url
  32.         self.save_path = save_path
  33.  
  34.     def run(self):
  35.         try:
  36.             session = requests.Session()
  37.             session.verify = False
  38.             session.trust_env = False
  39.             response = session.get(self.url, stream=True)
  40.             response.raise_for_status()
  41.             total_size = int(response.headers.get('content-length', 0))
  42.             os.makedirs(os.path.dirname(self.save_path), exist_ok=True)
  43.             block_size = 1024
  44.             downloaded = 0
  45.             start_time = time.time()
  46.             with open(self.save_path, 'wb') as f:
  47.                 for data in response.iter_content(block_size):
  48.                     if not data:
  49.                         continue
  50.                     downloaded += len(data)
  51.                     f.write(data)
  52.                     # Calculate progress
  53.                     if total_size > 0:
  54.                         progress = int((downloaded / total_size) * 100)
  55.                     else:
  56.                         progress = int((downloaded / (1024 * 1024)) % 100)
  57.                     # Calculate speed
  58.                     elapsed_time = time.time() - start_time
  59.                     if elapsed_time > 0:
  60.                         speed = downloaded / (1024 * elapsed_time)  # KB/s
  61.                         if speed > 1024:
  62.                             speed_text = f"{speed/1024:.1f} MB/s"
  63.                         else:
  64.                             speed_text = f"{speed:.1f} KB/s"
  65.                         # Calculate time remaining
  66.                         if total_size > 0:
  67.                             bytes_remaining = total_size - downloaded
  68.                             time_remaining = bytes_remaining / (downloaded / elapsed_time)
  69.                             if time_remaining > 60:
  70.                                 time_text = f"{time_remaining/60:.1f} minutes remaining"
  71.                             else:
  72.                                 time_text = f"{time_remaining:.1f} seconds remaining"
  73.                         else:
  74.                             time_text = "Calculating..."
  75.                     else:
  76.                         speed_text = "Calculating..."
  77.                         time_text = "Calculating..."
  78.                     self.progress.emit(progress, speed_text, time_text)
  79.             self.finished.emit(self.save_path)
  80.         except Exception as e:
  81.             self.error.emit(str(e))
  82.  
  83.  
  84. class PlaylistSelector(QDialog):
  85.     def __init__(self, playlists_info, parent=None):
  86.         super().__init__(parent)
  87.         self.setWindowTitle("Select Playlist")
  88.         self.setModal(True)
  89.         self.setMinimumWidth(500)
  90.         self.parent = parent
  91.         self.playlists_info = playlists_info
  92.         layout = QVBoxLayout()
  93.         # Create list widget
  94.         self.list_widget = QListWidget()
  95.         # Add only existing playlists to the list
  96.         self.existing_playlists = {}
  97.         for filename, info in playlists_info.items():
  98.             file_path = info.get('path', '')
  99.             if os.path.exists(file_path):
  100.                 self.existing_playlists[filename] = info
  101.                 item = QListWidgetItem()
  102.                 url = info.get('url', 'Unknown URL')
  103.                 timestamp = time.strftime('%Y-%m-%d %H:%M:%S',
  104.                                       time.localtime(info.get('timestamp', 0)))
  105.                 item.setText(f"{filename}\nURL: {url}\nDownloaded: {timestamp}")
  106.                 item.setData(Qt.UserRole, file_path)
  107.                 self.list_widget.addItem(item)
  108.         # Update parent's playlist_info if any playlists were removed
  109.         if len(self.existing_playlists) < len(playlists_info):
  110.             self.parent.playlist_info = self.existing_playlists
  111.             self.parent.save_playlist_info()
  112.             removed_count = len(playlists_info) - len(self.existing_playlists)
  113.             QMessageBox.information(self, "Playlist Cleanup",
  114.                                 f"Removed {removed_count} missing playlist(s) from the list.")
  115.         if self.list_widget.count() == 0:
  116.             QMessageBox.warning(self, "No Playlists",
  117.                             "No downloaded playlists available.")
  118.             self.reject()
  119.             return
  120.         layout.addWidget(self.list_widget)
  121.         # Management buttons layout
  122.         management_layout = QHBoxLayout()
  123.         # Add rename button
  124.         rename_button = QPushButton("Rename Selected")
  125.         rename_button.clicked.connect(self.rename_playlist)
  126.         management_layout.addWidget(rename_button)
  127.         # Add delete button
  128.         delete_button = QPushButton("Delete Selected")
  129.         delete_button.clicked.connect(self.delete_playlist)
  130.         delete_button.setStyleSheet("background-color: #ff4444; color: white;")
  131.         management_layout.addWidget(delete_button)
  132.         layout.addLayout(management_layout)
  133.         # Buttons
  134.         button_layout = QHBoxLayout()
  135.         load_button = QPushButton("Load")
  136.         load_button.clicked.connect(self.accept)
  137.         cancel_button = QPushButton("Cancel")
  138.         cancel_button.clicked.connect(self.reject)
  139.         button_layout.addWidget(load_button)
  140.         button_layout.addWidget(cancel_button)
  141.         layout.addLayout(button_layout)
  142.         self.setLayout(layout)
  143.  
  144.     def delete_playlist(self):
  145.         current_item = self.list_widget.currentItem()
  146.         if not current_item:
  147.             QMessageBox.warning(self, "No Selection", "Please select a playlist to delete.")
  148.             return
  149.         # Find the current filename
  150.         current_filename = None
  151.         for filename, info in self.existing_playlists.items():
  152.             if info['path'] == current_item.data(Qt.UserRole):
  153.                 current_filename = filename
  154.                 break
  155.         if not current_filename:
  156.             QMessageBox.warning(self, "Error", "Could not find playlist information.")
  157.             return
  158.         # Confirm deletion
  159.         reply = QMessageBox.question(self, "Confirm Delete",
  160.                                    f"Are you sure you want to delete the playlist '{current_filename}'?\n"
  161.                                    "This will permanently delete the playlist file.",
  162.                                    QMessageBox.Yes | QMessageBox.No,
  163.                                    QMessageBox.No)
  164.         if reply == QMessageBox.Yes:
  165.             try:
  166.                 # Get the file path
  167.                 file_path = self.existing_playlists[current_filename]['path']
  168.                 # Delete the file
  169.                 os.remove(file_path)
  170.                 # Remove from playlists info
  171.                 del self.existing_playlists[current_filename]
  172.                 # Update parent's playlist info
  173.                 self.parent.playlist_info = self.existing_playlists
  174.                 self.parent.save_playlist_info()
  175.                 # Remove from list widget
  176.                 row = self.list_widget.row(current_item)
  177.                 self.list_widget.takeItem(row)
  178.                 # Check if we have any playlists left
  179.                 if self.list_widget.count() == 0:
  180.                     QMessageBox.information(self, "No Playlists",
  181.                                         "All playlists have been deleted.")
  182.                     self.reject()
  183.                 else:
  184.                     QMessageBox.information(self, "Success",
  185.                                         f"Playlist '{current_filename}' has been deleted.")
  186.             except Exception as e:
  187.                 QMessageBox.critical(self, "Error",
  188.                                    f"Failed to delete playlist: {str(e)}")
  189.  
  190.     def rename_playlist(self):
  191.         current_item = self.list_widget.currentItem()
  192.         if not current_item:
  193.             QMessageBox.warning(self, "No Selection", "Please select a playlist to rename.")
  194.             return
  195.         # Find the current filename
  196.         current_filename = None
  197.         for filename, info in self.existing_playlists.items():
  198.             if info['path'] == current_item.data(Qt.UserRole):
  199.                 current_filename = filename
  200.                 break
  201.         if not current_filename:
  202.             QMessageBox.warning(self, "Error", "Could not find playlist information.")
  203.             return
  204.         # Show rename dialog
  205.         new_name, ok = QInputDialog.getText(self, "Rename Playlist",
  206.                                           "Enter new name:",
  207.                                           QLineEdit.Normal,
  208.                                           current_filename)
  209.         if ok and new_name:
  210.             if new_name == current_filename:
  211.                 return  # No change needed
  212.             if new_name in self.existing_playlists:
  213.                 QMessageBox.warning(self, "Name Exists",
  214.                                   "A playlist with this name already exists.")
  215.                 return
  216.             try:
  217.                 # Get the old file info
  218.                 old_info = self.existing_playlists[current_filename]
  219.                 old_path = old_info['path']
  220.                 # Create new file path
  221.                 new_path = os.path.join(os.path.dirname(old_path), f"{new_name}.m3u")
  222.                 # Rename the file
  223.                 os.rename(old_path, new_path)
  224.                 # Update playlist info
  225.                 self.existing_playlists[new_name] = old_info.copy()
  226.                 self.existing_playlists[new_name]['path'] = new_path
  227.                 del self.existing_playlists[current_filename]
  228.                 # Update parent's playlist info
  229.                 self.parent.playlist_info = self.existing_playlists
  230.                 self.parent.save_playlist_info()
  231.                 # Update list item
  232.                 url = old_info.get('url', 'Unknown URL')
  233.                 timestamp = time.strftime('%Y-%m-%d %H:%M:%S',
  234.                                       time.localtime(old_info.get('timestamp', 0)))
  235.                 current_item.setText(f"{new_name}\nURL: {url}\nDownloaded: {timestamp}")
  236.                 current_item.setData(Qt.UserRole, new_path)
  237.                 QMessageBox.information(self, "Success",
  238.                                       f"Playlist renamed to '{new_name}'")
  239.             except Exception as e:
  240.                 QMessageBox.critical(self, "Error",
  241.                                    f"Failed to rename playlist: {str(e)}")
  242.  
  243.     def get_selected_playlist(self):
  244.         current_item = self.list_widget.currentItem()
  245.         if current_item:
  246.             return current_item.data(Qt.UserRole)
  247.         return None
  248.  
  249.  
  250. class MediaItem:
  251.     def __init__(self, name, logo_url, group, stream_url):
  252.         self.name = name
  253.         self.logo_url = logo_url
  254.         self.group = group
  255.         self.stream_url = stream_url
  256.  
  257.  
  258. class PlaylistParserWorker(QThread):
  259.     progress = pyqtSignal(int, int)  # current, total
  260.     finished = pyqtSignal(dict, dict, dict)  # channels, movies, series
  261.     error = pyqtSignal(str)
  262.  
  263.     def __init__(self, playlist_path):
  264.         super().__init__()
  265.         self.playlist_path = playlist_path
  266.  
  267.     def run(self):
  268.         try:
  269.             channels = {}
  270.             movies = {}
  271.             series = {}
  272.             with open(self.playlist_path, 'r', encoding='utf-8') as f:
  273.                 lines = f.readlines()
  274.             total_lines = len(lines)
  275.             i = 0
  276.             while i < total_lines:
  277.                 line = lines[i].strip()
  278.                 if line.startswith('#EXTINF:'):
  279.                     info_line = line
  280.                     url_line = lines[i + 1].strip() if i + 1 < total_lines else None
  281.                     if url_line:
  282.                         # Extract information using regex
  283.                         name_match = re.search(r'tvg-name="([^"]*)"', info_line)
  284.                         logo_match = re.search(r'tvg-logo="([^"]*)"', info_line)
  285.                         group_match = re.search(r'group-title="([^"]*)"', info_line)
  286.                         name = name_match.group(1) if name_match else ""
  287.                         logo_url = logo_match.group(1) if logo_match else ""
  288.                         group = group_match.group(1) if group_match else "Ungrouped"
  289.                         # Create MediaItem
  290.                         media_item = MediaItem(name, logo_url, group, url_line)
  291.                         # Add to appropriate dictionary
  292.                         if "/movie/" in url_line:
  293.                             if group not in movies:
  294.                                 movies[group] = []
  295.                             movies[group].append(media_item)
  296.                         elif "/series/" in url_line:
  297.                             if group not in series:
  298.                                 series[group] = []
  299.                             series[group].append(media_item)
  300.                         else:
  301.                             if group not in channels:
  302.                                 channels[group] = []
  303.                             channels[group].append(media_item)
  304.                     i += 2
  305.                 else:
  306.                     i += 1
  307.                 # Emit progress every 100 items
  308.                 if i % 100 == 0:
  309.                     self.progress.emit(i, total_lines)
  310.             self.progress.emit(total_lines, total_lines)
  311.             self.finished.emit(channels, movies, series)
  312.         except Exception as e:
  313.             self.error.emit(str(e))
  314.  
  315.  
  316. class MediaTreeWidget(QTreeWidget):
  317.     loading_progress = pyqtSignal(int, int)  # current, total
  318.     loading_finished = pyqtSignal()
  319.  
  320.     def __init__(self, parent=None):
  321.         super().__init__(parent)
  322.         self.setHeaderHidden(True)
  323.         self.setColumnCount(1)
  324.         self.setAnimated(True)
  325.         self.batch_size = 50  # Number of items to load per batch
  326.         self.original_items = {}  # Store original items for search
  327.         self.itemDoubleClicked.connect(self.on_item_double_clicked)
  328.         # Enable right-click context menu
  329.         self.setContextMenuPolicy(Qt.CustomContextMenu)
  330.         self.customContextMenuRequested.connect(self.show_context_menu)
  331.  
  332.     def show_context_menu(self, position):
  333.         """Show context menu on right-click."""
  334.         item = self.itemAt(position)
  335.         if not item or not item.parent():  # Only allow context menu for media items (not groups)
  336.             return
  337.         # Create context menu
  338.         menu = QMenu(self)
  339.         copy_url_action = QAction("Copy URL", self)
  340.         copy_url_action.triggered.connect(lambda: self.copy_media_url(item))
  341.         menu.addAction(copy_url_action)
  342.         # Show the context menu at the cursor position
  343.         menu.exec_(self.viewport().mapToGlobal(position))
  344.  
  345.     def copy_media_url(self, item):
  346.         """Copy the URL of the selected media item to the clipboard."""
  347.         media_item = item.data(0, Qt.UserRole)
  348.         if media_item and hasattr(media_item, 'stream_url'):
  349.             clipboard = QApplication.clipboard()
  350.             clipboard.setText(media_item.stream_url)
  351.             QMessageBox.information(self, "URL Copied", f"Stream URL copied to clipboard:{media_item.stream_url}")
  352.         else:
  353.             QMessageBox.warning(self, "Error", "Failed to copy URL. No valid media item selected.")
  354.  
  355.     def populate_tree(self, media_dict):
  356.         self.clear()
  357.         self.media_dict = media_dict
  358.         self.original_items = media_dict.copy()  # Store original items
  359.         self.groups = list(media_dict.keys())
  360.         self.current_group = 0
  361.         self.current_item = 0
  362.         # Calculate total items
  363.         self.total_items = sum(len(items) for items in media_dict.values())
  364.         self.loaded_items = 0
  365.         # Start loading the first batch
  366.         QTimer.singleShot(0, self.load_next_batch)
  367.  
  368.     def search(self, query):
  369.         if not query:  # If search is empty, restore original items
  370.             self.media_dict = self.original_items.copy()
  371.             self.populate_tree(self.media_dict)
  372.             return
  373.         # Convert query to lowercase for case-insensitive search
  374.         query = query.lower()
  375.         # Create new dictionary with matching items
  376.         filtered_dict = {}
  377.         for group, items in self.original_items.items():
  378.             matching_items = []
  379.             for item in items:
  380.                 if query in item.name.lower():
  381.                     matching_items.append(item)
  382.             if matching_items:  # Only add groups that have matching items
  383.                 filtered_dict[group] = matching_items
  384.         # Update tree with filtered items
  385.         self.media_dict = filtered_dict
  386.         self.populate_tree_with_highlight(filtered_dict, query)
  387.  
  388.     def populate_tree_with_highlight(self, media_dict, query):
  389.         self.clear()
  390.         self.media_dict = media_dict
  391.         self.groups = list(media_dict.keys())
  392.         self.current_group = 0
  393.         self.current_item = 0
  394.         # Calculate total items
  395.         self.total_items = sum(len(items) for items in media_dict.values())
  396.         self.loaded_items = 0
  397.         # Start loading the first batch
  398.         QTimer.singleShot(0, lambda: self.load_next_batch_with_highlight(query))
  399.  
  400.     def load_next_batch_with_highlight(self, query):
  401.         batch_count = 0
  402.         while self.current_group < len(self.groups) and batch_count < self.batch_size:
  403.             group = self.groups[self.current_group]
  404.             items = self.media_dict[group]
  405.             # Create group item if it's the first item in the group
  406.             if self.current_item == 0:
  407.                 group_item = QTreeWidgetItem(self)
  408.                 group_item.setText(0, group)
  409.                 group_item.setExpanded(False)
  410.             else:
  411.                 group_item = self.topLevelItem(self.current_group)
  412.  
  413.             # Add items for this batch
  414.             while self.current_item < len(items) and batch_count < self.batch_size:
  415.                 media = items[self.current_item]
  416.                 item = QTreeWidgetItem(group_item)
  417.                 # Highlight the search query in the name
  418.                 highlighted_name = media.name.lower().replace(
  419.                     query, f"<b><u>{query}</u></b>", 1
  420.                 )
  421.                 item.setText(0, highlighted_name)
  422.                 item.setData(0, Qt.UserRole, media)
  423.                 self.current_item += 1
  424.                 batch_count += 1
  425.                 self.loaded_items += 1
  426.                 # Emit progress
  427.                 self.loading_progress.emit(self.loaded_items, self.total_items)
  428.  
  429.             # Move to next group if we've finished the current one
  430.             if self.current_item >= len(items):
  431.                 self.current_group += 1
  432.                 self.current_item = 0
  433.  
  434.         # Schedule next batch if there are more items to load
  435.         if self.current_group < len(self.groups):
  436.             QTimer.singleShot(10, lambda: self.load_next_batch_with_highlight(query))
  437.         else:
  438.             self.loading_finished.emit()
  439.  
  440.     def load_next_batch(self):
  441.         batch_count = 0
  442.         while self.current_group < len(self.groups) and batch_count < self.batch_size:
  443.             group = self.groups[self.current_group]
  444.             items = self.media_dict[group]
  445.             # Create group item if it's the first item in the group
  446.             if self.current_item == 0:
  447.                 group_item = QTreeWidgetItem(self)
  448.                 group_item.setText(0, group)
  449.                 group_item.setExpanded(False)
  450.             else:
  451.                 group_item = self.topLevelItem(self.current_group)
  452.             # Add items for this batch
  453.             while self.current_item < len(items) and batch_count < self.batch_size:
  454.                 media = items[self.current_item]
  455.                 item = QTreeWidgetItem(group_item)
  456.                 item.setText(0, media.name)
  457.                 item.setData(0, Qt.UserRole, media)
  458.                 self.current_item += 1
  459.                 batch_count += 1
  460.                 self.loaded_items += 1
  461.                 # Emit progress
  462.                 self.loading_progress.emit(self.loaded_items, self.total_items)
  463.             # Move to next group if we've finished the current one
  464.             if self.current_item >= len(items):
  465.                 self.current_group += 1
  466.                 self.current_item = 0
  467.         # Schedule next batch if there are more items to load
  468.         if self.current_group < len(self.groups):
  469.             QTimer.singleShot(10, self.load_next_batch)
  470.         else:
  471.             self.loading_finished.emit()
  472.  
  473.     def on_item_double_clicked(self, item, column):
  474.         # Check if this is a media item (not a group)
  475.         if item.parent() is not None:  # This means it's a child item (media item)
  476.             media_item = item.data(0, Qt.UserRole)
  477.             if media_item and hasattr(media_item, 'stream_url'):
  478.                 # Create and show the media player window
  479.                 player = MediaPlayer(media_item.stream_url, media_item.name, self)
  480.                 player.show()
  481.                 player.media_player.play()  # Start playing immediately
  482.  
  483.  
  484. class MediaPlayer(QMainWindow):
  485.     def __init__(self, stream_url, title, parent=None):
  486.         super().__init__(parent)
  487.         self.setWindowTitle(title)
  488.         # Set initial size to 800x800
  489.         screen = QApplication.primaryScreen().geometry()
  490.         # Center the window on screen
  491.         x = (screen.width() - 800) // 2
  492.         y = (screen.height() - 600) // 2
  493.         self.setGeometry(x, y, 800, 600)
  494.         # Allow window to be maximized
  495.         self.setWindowFlags(self.windowFlags() | Qt.WindowMaximizeButtonHint)
  496.         # Create VLC instance and media player
  497.         self.instance = vlc.Instance()
  498.         self.media_player = self.instance.media_player_new()
  499.         # Create a widget to hold the video
  500.         self.video_widget = QFrame()
  501.         self.video_widget.setStyleSheet("background-color: black;")
  502.         self.video_widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)  # Make video expand to fill space
  503.         if sys.platform.startswith('win'):
  504.             self.media_player.set_hwnd(self.video_widget.winId())
  505.         elif sys.platform.startswith('linux'):
  506.             self.media_player.set_xwindow(self.video_widget.winId())
  507.         elif sys.platform.startswith('darwin'):
  508.             self.media_player.set_nsobject(int(self.video_widget.winId()))
  509.         # Set up the main layout
  510.         main_widget = QWidget()
  511.         main_layout = QHBoxLayout()  # Changed to horizontal layout
  512.         main_layout.setSpacing(0)  # Remove spacing between layouts
  513.         main_layout.setContentsMargins(0, 0, 0, 0)  # Remove margins
  514.         # Video and controls container
  515.         video_container = QVBoxLayout()
  516.         video_container.setSpacing(5)  # Minimal spacing between video and controls
  517.         video_container.setContentsMargins(0, 0, 0, 5)  # Add small bottom margin
  518.         video_container.addWidget(self.video_widget, stretch=1)  # Video takes all available space
  519.         # Controls layout
  520.         controls_layout = QVBoxLayout()
  521.         controls_layout.setSpacing(2)  # Minimal spacing between controls
  522.         controls_layout.setContentsMargins(5, 0, 5, 0)  # Add horizontal margins
  523.         # Time label and slider (for movies and series only)
  524.         self.time_slider = QSlider(Qt.Horizontal)
  525.         self.time_slider.setEnabled(False)
  526.         self.time_slider.sliderMoved.connect(self.set_position)
  527.         self.time_slider.setFixedHeight(20)  # Set fixed height for slider
  528.         self.time_label = QLabel("00:00:00 / 00:00:00")
  529.         self.time_label.setStyleSheet("color: black; font-size: 10pt; background: transparent;")
  530.         self.time_label.setAlignment(Qt.AlignLeft)
  531.         self.time_label.setFixedHeight(15)  # Set fixed height for label
  532.         # Only show time slider for movies and series
  533.         is_movie_or_series = "/movie/" in stream_url or "/series/" in stream_url
  534.         self.time_slider.setVisible(is_movie_or_series)
  535.         self.time_label.setVisible(is_movie_or_series)
  536.         # Add time controls to layout
  537.         if is_movie_or_series:
  538.             controls_layout.addWidget(self.time_label)
  539.             controls_layout.addWidget(self.time_slider)
  540.         # Button controls layout
  541.         button_layout = QHBoxLayout()
  542.         button_layout.setSpacing(5)  # Space between buttons
  543.         # Play/Pause button
  544.         self.play_button = QPushButton("Play")
  545.         self.play_button.clicked.connect(self.play_pause)
  546.         self.play_button.setFixedHeight(30)  # Set fixed height for buttons
  547.         button_layout.addWidget(self.play_button)
  548.         # Stop button
  549.         stop_button = QPushButton("Stop")
  550.         stop_button.clicked.connect(self.stop)
  551.         stop_button.setFixedHeight(30)
  552.         button_layout.addWidget(stop_button)
  553.         # Maximize button
  554.         maximize_button = QPushButton("Maximize")
  555.         maximize_button.clicked.connect(self.toggle_maximize)
  556.         maximize_button.setFixedHeight(30)
  557.         button_layout.addWidget(maximize_button)
  558.         controls_layout.addLayout(button_layout)
  559.         video_container.addLayout(controls_layout)
  560.         # Add video container to main layout with full stretch
  561.         main_layout.addLayout(video_container, stretch=1)
  562.         # Volume control container
  563.         volume_container = QVBoxLayout()
  564.         volume_container.setContentsMargins(10, 10, 10, 10)  # Add some padding
  565.         # Volume label at top
  566.         volume_label = QLabel("Volume")
  567.         volume_label.setAlignment(Qt.AlignCenter)
  568.         volume_container.addWidget(volume_label)
  569.         # Vertical volume slider
  570.         self.volume_slider = QSlider(Qt.Vertical)
  571.         self.volume_slider.setMinimum(0)
  572.         self.volume_slider.setMaximum(100)
  573.         self.volume_slider.setValue(50)  # Start at 50%
  574.         self.volume_slider.setTickPosition(QSlider.TicksBothSides)
  575.         self.volume_slider.setTickInterval(10)
  576.         self.volume_slider.valueChanged.connect(self.set_volume)
  577.         volume_container.addWidget(self.volume_slider, stretch=1)
  578.         # Volume percentage label at bottom
  579.         self.volume_percent = QLabel("20%")
  580.         self.volume_percent.setAlignment(Qt.AlignCenter)
  581.         volume_container.addWidget(self.volume_percent)
  582.         # Add volume container to main layout
  583.         main_layout.addLayout(volume_container)
  584.         # Set the main layout
  585.         main_widget = QWidget()
  586.         main_widget.setLayout(main_layout)
  587.         self.setCentralWidget(main_widget)
  588.         # Set up the media
  589.         self.media = self.instance.media_new(stream_url)
  590.         self.media_player.set_media(self.media)
  591.         # Set initial volume
  592.         self.media_player.audio_set_volume(100)
  593.         # Start playing
  594.         self.media_player.play()
  595.         self.play_button.setText("Pause")
  596.         # Setup timer for updating UI
  597.         self.timer = QTimer(self)
  598.         self.timer.setInterval(100)
  599.         self.timer.timeout.connect(self.update_ui)
  600.         self.timer.start()
  601.  
  602.     def play_pause(self):
  603.         if self.media_player.is_playing():
  604.             self.media_player.pause()
  605.             self.play_button.setText("Play")
  606.         else:
  607.             self.media_player.play()
  608.             self.play_button.setText("Pause")
  609.  
  610.     def stop(self):
  611.         self.media_player.stop()
  612.         self.play_button.setText("Play")
  613.  
  614.     def toggle_maximize(self):
  615.         if self.isMaximized():
  616.             self.showNormal()
  617.         else:
  618.             self.showMaximized()
  619.  
  620.     def set_volume(self, volume):
  621.         self.media_player.audio_set_volume(volume)
  622.         self.volume_percent.setText(f"{volume}%")
  623.  
  624.     def update_ui(self):
  625.         # Update play/pause button
  626.         if not self.media_player.is_playing():
  627.             self.play_button.setText("Play")
  628.         else:
  629.             self.play_button.setText("Pause")
  630.         # Update time slider and label for movies and series
  631.         if self.time_slider.isVisible():
  632.             media_pos = self.media_player.get_position()
  633.             media_length = self.media_player.get_length() / 1000  # Convert to seconds
  634.             # Update slider
  635.             if not self.time_slider.isSliderDown():
  636.                 self.time_slider.setValue(int(media_pos * 1000))
  637.             if media_length > 0:
  638.                 current_time = int(media_length * media_pos)
  639.                 total_time = int(media_length)
  640.                 # Format time as HH:MM:SS
  641.                 current_str = time.strftime('%H:%M:%S', time.gmtime(current_time))
  642.                 total_str = time.strftime('%H:%M:%S', time.gmtime(total_time))
  643.                 self.time_label.setText(f"{current_str} / {total_str}")
  644.                 # Enable slider once media length is known
  645.                 if not self.time_slider.isEnabled():
  646.                     self.time_slider.setEnabled(True)
  647.                     self.time_slider.setRange(0, 1000)
  648.         # Check for media errors
  649.         state = self.media_player.get_state()
  650.         if state == vlc.State.Error:
  651.             self.handle_error()
  652.  
  653.     def set_position(self, position):
  654.         """Set the media position according to the slider value"""
  655.         self.media_player.set_position(position / 1000.0)
  656.  
  657.     def handle_error(self):
  658.         self.play_button.setEnabled(False)
  659.         QMessageBox.warning(self, "Media Player Error",
  660.                           "Error playing media. Please check the stream URL.")
  661.  
  662.     def closeEvent(self, event):
  663.         self.timer.stop()
  664.         self.media_player.stop()
  665.         # Reset window state before closing
  666.         self.showNormal()
  667.         event.accept()
  668.  
  669.     def showEvent(self, event):
  670.         # Ensure window size is 800x800 every time it's shown
  671.         screen = QApplication.primaryScreen().geometry()
  672.         x = (screen.width() - 800) // 2
  673.         y = (screen.height() - 600) // 2
  674.         self.setGeometry(x, y, 800, 600)
  675.         super().showEvent(event)
  676.  
  677.  
  678. class IPTVPlayer(QMainWindow):
  679.     def __init__(self):
  680.         super().__init__()
  681.         self.setWindowTitle("Najeeb Shah Khan IPTV Player")
  682.         self.setGeometry(100, 100, 1024, 768)
  683.         # Set up directories
  684.         self.app_dir = os.path.dirname(os.path.abspath(__file__))
  685.         self.playlists_dir = os.path.join(self.app_dir, 'playlists')
  686.         self.cache_dir = os.path.join(self.app_dir, 'cache')
  687.         self.playlist_info_file = os.path.join(self.app_dir, 'playlist_info.json')
  688.         # Create directories if they don't exist
  689.         os.makedirs(self.playlists_dir, exist_ok=True)
  690.         os.makedirs(self.cache_dir, exist_ok=True)
  691.         # Load playlist information
  692.         self.playlist_info = self.load_playlist_info()
  693.         # Main widget and layout
  694.         main_widget = QWidget()
  695.         main_layout = QVBoxLayout()
  696.         # Status label
  697.         self.status_label = QLabel()
  698.         self.status_label.setAlignment(Qt.AlignCenter)
  699.         # Download speed label
  700.         self.speed_label = QLabel()
  701.         self.speed_label.setAlignment(Qt.AlignCenter)
  702.         # Playlist Download Section
  703.         download_layout = QHBoxLayout()
  704.         self.playlist_input = QLineEdit()
  705.         self.playlist_input.setPlaceholderText("Enter M3U Playlist URL")
  706.         # Set last used URL if available
  707.         last_url = self.get_last_used_url()
  708.         if last_url:
  709.             self.playlist_input.setText(last_url)
  710.         # Add Browse Button for M3U Files
  711.         browse_button = QPushButton("Browse M3U File")
  712.         browse_button.clicked.connect(self.browse_m3u_file)  # Connect to the browse function    
  713.         # Search Section
  714.         search_layout = QHBoxLayout()
  715.         self.search_input = QLineEdit()
  716.         self.search_input.setPlaceholderText("Search in current tab...")
  717.         self.search_input.returnPressed.connect(self.perform_search)  # Enter key
  718.         search_button = QPushButton("Search")
  719.         search_button.clicked.connect(self.perform_search)
  720.         search_layout.addWidget(self.search_input)
  721.         search_layout.addWidget(search_button)
  722.         # Add existing buttons
  723.         download_button = QPushButton("Download Playlist")
  724.         download_button.clicked.connect(self.download_playlist)
  725.         load_button = QPushButton("Load Playlist")
  726.         load_button.clicked.connect(self.show_playlist_selector)
  727.         update_button = QPushButton("Update Playlist")
  728.         update_button.clicked.connect(self.update_current_playlist)
  729.         clear_cache_btn = QPushButton("Clear Cache")
  730.         clear_cache_btn.clicked.connect(self.clear_cache)
  731.         download_layout.addWidget(self.playlist_input)
  732.         download_layout.addWidget(download_button)
  733.         download_layout.addWidget(load_button)
  734.         download_layout.addWidget(update_button)
  735.         download_layout.addWidget(clear_cache_btn)
  736.         download_layout.addWidget(browse_button)  # Add the browse button here
  737.         # Progress Bar
  738.         self.progress_bar = QProgressBar()
  739.         self.progress_bar.setVisible(False)
  740.         # Main layout assembly
  741.         main_layout.addLayout(download_layout)
  742.         main_layout.addLayout(search_layout)  # Add search section
  743.         main_layout.addWidget(self.progress_bar)
  744.         main_layout.addWidget(self.status_label)
  745.         main_layout.addWidget(self.speed_label)
  746.         # Tabs
  747.         self.tabs = QTabWidget()
  748.         live_tv_tab = QWidget()
  749.         movies_tab = QWidget()
  750.         series_tab = QWidget()
  751.         self.tabs.addTab(live_tv_tab, "Live TV")
  752.         self.tabs.addTab(movies_tab, "Movies")
  753.         self.tabs.addTab(series_tab, "Series")
  754.         # Main layout assembly
  755.         main_layout.addWidget(self.tabs)
  756.         main_widget.setLayout(main_layout)
  757.         self.setCentralWidget(main_widget)
  758.         requests.packages.urllib3.disable_warnings()
  759.  
  760.     def browse_m3u_file(self):
  761.         """Open a file dialog to browse and select an M3U file."""
  762.         options = QFileDialog.Options()
  763.         options |= QFileDialog.ReadOnly  # Open file dialog in read-only mode
  764.         file_path, _ = QFileDialog.getOpenFileName(
  765.             self,
  766.             "Select M3U Playlist File",
  767.             self.playlists_dir,  # Start in the playlists directory
  768.             "M3U Files (*.m3u);;All Files (*)",
  769.             options=options
  770.         )
  771.         if file_path:  # If a file was selected
  772.             self.status_label.setText(f"Selected playlist: {os.path.basename(file_path)}")
  773.             self.playlist_input.setText("")  # Clear the URL input as we are loading locally
  774.             self.load_playlist(file_path)  # Load the selected playlist file
  775.  
  776.     def load_playlist_info(self):
  777.         if os.path.exists(self.playlist_info_file):
  778.             try:
  779.                 with open(self.playlist_info_file, 'r') as f:
  780.                     return json.load(f)
  781.             except:
  782.                 return {}
  783.         return {}
  784.  
  785.     def save_playlist_info(self):
  786.         try:
  787.             with open(self.playlist_info_file, 'w') as f:
  788.                 json.dump(self.playlist_info, f, indent=4)
  789.         except Exception as e:
  790.             print(f"Error saving playlist info: {e}")
  791.  
  792.     def get_last_used_url(self):
  793.         if self.playlist_info:
  794.             # Get the most recently added playlist
  795.             latest_playlist = max(self.playlist_info.items(), key=lambda x: x[1].get('timestamp', 0))
  796.             return latest_playlist[1].get('url', '')
  797.         return ''
  798.  
  799.     def update_progress(self, progress, speed, time_remaining):
  800.         self.progress_bar.setValue(progress)
  801.         status_text = f"Updating playlist... {progress}%"
  802.         self.status_label.setText(status_text)
  803.         self.speed_label.setText(f"{speed} | {time_remaining}")
  804.  
  805.     def update_current_playlist(self):
  806.         url = self.playlist_input.text().strip()
  807.         if not url:
  808.             QMessageBox.warning(self, "Error", "No playlist URL available to update from.")
  809.             return
  810.         # Generate filename from URL
  811.         filename = hashlib.md5(url.encode()).hexdigest() + '.m3u'
  812.         save_path = os.path.join(self.playlists_dir, filename)
  813.         # Show progress bar and reset status
  814.         self.progress_bar.setValue(0)
  815.         self.progress_bar.setVisible(True)
  816.         self.status_label.setText("Updating playlist...")
  817.         self.speed_label.setText("")
  818.         # Create and start download worker
  819.         self.download_worker = DownloadWorker(url, save_path)
  820.         self.download_worker.progress.connect(self.update_progress)
  821.         self.download_worker.finished.connect(self.download_finished)
  822.         self.download_worker.error.connect(self.download_error)
  823.         self.download_worker.start()
  824.  
  825.     def update_finished(self, save_path):
  826.         self.progress_bar.setVisible(False)
  827.         self.speed_label.setText("")
  828.         self.status_label.setText("Playlist updated successfully!")
  829.         # Update playlist info
  830.         url = self.playlist_input.text().strip()
  831.         self.update_playlist_info(url, save_path)
  832.  
  833.     def update_playlist_info(self, url, save_path):
  834.         filename = os.path.basename(save_path)
  835.         self.playlist_info[filename] = {
  836.             'url': url,
  837.             'timestamp': time.time(),
  838.             'path': save_path
  839.         }
  840.         self.save_playlist_info()
  841.  
  842.     def download_finished(self, file_path):
  843.         self.progress_bar.setVisible(False)
  844.         self.playlist_input.setEnabled(True)
  845.         self.status_label.setText("Download completed!")
  846.         self.speed_label.setText("")
  847.         # Update playlist information
  848.         url = self.playlist_input.text().strip()
  849.         filename = os.path.basename(file_path)
  850.         self.playlist_info[filename] = {
  851.             'url': url,
  852.             'timestamp': time.time(),
  853.             'path': file_path
  854.         }
  855.         self.save_playlist_info()
  856.         # Parse and load the playlist content
  857.         self.load_playlist(file_path)
  858.         QMessageBox.information(self, "Success", f"Playlist downloaded to: {file_path}")
  859.  
  860.     def download_error(self, error_msg):
  861.         self.progress_bar.setVisible(False)
  862.         self.status_label.setText("Download failed!")
  863.         self.speed_label.setText("")
  864.         self.playlist_input.setEnabled(True)
  865.         QMessageBox.critical(self, "Download Error", error_msg)
  866.  
  867.     def clear_cache(self):
  868.         try:
  869.             for filename in os.listdir(self.cache_dir):
  870.                 file_path = os.path.join(self.cache_dir, filename)
  871.                 if os.path.isfile(file_path):
  872.                     os.unlink(file_path)
  873.             QMessageBox.information(self, "Success", "Cache cleared successfully!")
  874.         except Exception as e:
  875.             QMessageBox.critical(self, "Error", f"Failed to clear cache: {str(e)}")
  876.  
  877.     def download_playlist(self):
  878.         url = self.playlist_input.text().strip()
  879.         if not url:
  880.             QMessageBox.warning(self, "Error", "Please enter a playlist URL")
  881.             return
  882.         try:
  883.             # Use MD5 hash for consistent filename generation
  884.             filename = hashlib.md5(url.encode()).hexdigest() + '.m3u'
  885.             save_path = os.path.join(self.playlists_dir, filename)
  886.             self.progress_bar.setVisible(True)
  887.             self.progress_bar.setValue(0)
  888.             self.playlist_input.setEnabled(False)
  889.             self.status_label.setText("Starting download...")
  890.             self.speed_label.setText("Connecting...")
  891.             # Update playlist info before starting download
  892.             self.playlist_info[filename] = {
  893.                 'url': url,
  894.                 'timestamp': time.time(),
  895.                 'path': save_path
  896.             }
  897.             self.save_playlist_info()
  898.             self.download_worker = DownloadWorker(url, save_path)
  899.             self.download_worker.progress.connect(self.update_progress)
  900.             self.download_worker.finished.connect(self.download_finished)
  901.             self.download_worker.error.connect(self.download_error)
  902.             self.download_worker.start()
  903.         except Exception as e:
  904.             QMessageBox.critical(self, "Error", str(e))
  905.             self.playlist_input.setEnabled(True)
  906.             self.progress_bar.setVisible(False)
  907.  
  908.     def verify_playlists(self):
  909.         """Verify playlists exist and clean up JSON if needed"""
  910.         playlists_to_remove = []
  911.         for filename, info in self.playlist_info.items():
  912.             file_path = info.get('path', '')
  913.             if not os.path.exists(file_path):
  914.                 playlists_to_remove.append(filename)
  915.         # Remove non-existent playlists from info
  916.         for filename in playlists_to_remove:
  917.             del self.playlist_info[filename]
  918.         # Save cleaned up playlist info
  919.         if playlists_to_remove:
  920.             self.save_playlist_info()
  921.         return len(playlists_to_remove)
  922.  
  923.     def show_playlist_selector(self):
  924.         # Verify playlists before showing selector
  925.         removed_count = self.verify_playlists()
  926.         if removed_count > 0:
  927.             QMessageBox.information(self, "Playlist Cleanup",
  928.                                   f"Removed {removed_count} missing playlist(s) from the list.")
  929.        
  930.         if not self.playlist_info:
  931.             QMessageBox.warning(self, "No Playlists",
  932.                               "No downloaded playlists available.")
  933.             return
  934.            
  935.         dialog = PlaylistSelector(self.playlist_info, self)
  936.         if dialog.exec_() == QDialog.Accepted:
  937.             selected_path = dialog.get_selected_playlist()
  938.             if selected_path:
  939.                 # Find the URL for the selected playlist
  940.                 for info in self.playlist_info.values():
  941.                     if info.get('path') == selected_path:
  942.                         self.playlist_input.setText(info.get('url', ''))
  943.                         self.status_label.setText(f"Loaded playlist: {os.path.basename(selected_path)}")
  944.                         # Parse and load the playlist content
  945.                         self.load_playlist(selected_path)
  946.                         break
  947.  
  948.     def load_playlist(self, playlist_path):
  949.         try:
  950.             # Create tree widgets if they don't exist
  951.             if not hasattr(self, 'live_tv_tree'):
  952.                 self.live_tv_tree = MediaTreeWidget()
  953.                 self.movies_tree = MediaTreeWidget()
  954.                 self.series_tree = MediaTreeWidget()
  955.                
  956.                 # Connect loading progress signals
  957.                 self.live_tv_tree.loading_progress.connect(lambda c, t: self.update_loading_progress("Live TV", c, t))
  958.                 self.movies_tree.loading_progress.connect(lambda c, t: self.update_loading_progress("Movies", c, t))
  959.                 self.series_tree.loading_progress.connect(lambda c, t: self.update_loading_progress("Series", c, t))
  960.                
  961.                 self.live_tv_tree.loading_finished.connect(lambda: self.loading_finished("Live TV"))
  962.                 self.movies_tree.loading_finished.connect(lambda: self.loading_finished("Movies"))
  963.                 self.series_tree.loading_finished.connect(lambda: self.loading_finished("Series"))
  964.                
  965.                 # Add trees to their respective tabs
  966.                 live_tv_layout = QVBoxLayout()
  967.                 live_tv_layout.addWidget(self.live_tv_tree)
  968.                 self.tabs.widget(0).setLayout(live_tv_layout)
  969.                
  970.                 movies_layout = QVBoxLayout()
  971.                 movies_layout.addWidget(self.movies_tree)
  972.                 self.tabs.widget(1).setLayout(movies_layout)
  973.                
  974.                 series_layout = QVBoxLayout()
  975.                 series_layout.addWidget(self.series_tree)
  976.                 self.tabs.widget(2).setLayout(series_layout)
  977.            
  978.             # Show loading progress
  979.             self.progress_bar.setValue(0)
  980.             self.progress_bar.setVisible(True)
  981.             self.status_label.setText("Parsing playlist...")
  982.            
  983.             # Create and start parser worker
  984.             self.parser_worker = PlaylistParserWorker(playlist_path)
  985.             self.parser_worker.progress.connect(self.update_parse_progress)
  986.             self.parser_worker.finished.connect(self.parser_finished)
  987.             self.parser_worker.error.connect(self.parser_error)
  988.             self.parser_worker.start()
  989.            
  990.         except Exception as e:
  991.             QMessageBox.critical(self, "Error", f"Failed to load playlist: {str(e)}")
  992.            
  993.     def update_parse_progress(self, current, total):
  994.         progress = int((current / total) * 100)
  995.         self.progress_bar.setValue(progress)
  996.         self.status_label.setText(f"Parsing playlist... {progress}%")
  997.        
  998.     def update_loading_progress(self, section, current, total):
  999.         progress = int((current / total) * 100)
  1000.         self.progress_bar.setValue(progress)
  1001.         self.status_label.setText(f"Loading {section}... {progress}%")
  1002.        
  1003.     def loading_finished(self, section):
  1004.         self.status_label.setText(f"Finished loading {section}")
  1005.         if section == "Series":  # Last section to load
  1006.             self.progress_bar.setVisible(False)
  1007.             self.status_label.setText("Playlist loaded successfully!")
  1008.        
  1009.     def parser_finished(self, channels, movies, series):
  1010.         self.status_label.setText("Organizing content...")
  1011.        
  1012.         # Store content for reuse
  1013.         self.channels = channels
  1014.         self.movies = movies
  1015.         self.series = series
  1016.        
  1017.         # Start populating trees with batch loading
  1018.         self.live_tv_tree.populate_tree(channels)
  1019.         self.movies_tree.populate_tree(movies)
  1020.         self.series_tree.populate_tree(series)
  1021.        
  1022.     def parser_error(self, error_msg):
  1023.         self.progress_bar.setVisible(False)
  1024.         self.status_label.setText("Failed to parse playlist!")
  1025.         QMessageBox.critical(self, "Error", f"Failed to parse playlist: {error_msg}")
  1026.        
  1027.     def perform_search(self):
  1028.         query = self.search_input.text().strip()
  1029.         current_tab = self.tabs.currentWidget()
  1030.        
  1031.         # Get the tree widget for the current tab
  1032.         tree_widget = None
  1033.         if current_tab == self.tabs.widget(0):  # Live TV
  1034.             tree_widget = self.live_tv_tree
  1035.             content_type = "Live TV"
  1036.         elif current_tab == self.tabs.widget(1):  # Movies
  1037.             tree_widget = self.movies_tree
  1038.             content_type = "Movies"
  1039.         elif current_tab == self.tabs.widget(2):  # Series
  1040.             tree_widget = self.series_tree
  1041.             content_type = "Series"
  1042.            
  1043.         if tree_widget:
  1044.             if query:
  1045.                 tree_widget.search(query)
  1046.                 self.status_label.setText(f"Showing search results for: {query}")
  1047.             else:
  1048.                 # When search is cleared, reload the content for current tab
  1049.                 self.status_label.setText(f"Reloading {content_type}...")
  1050.                 if content_type == "Live TV":
  1051.                     tree_widget.populate_tree(self.channels)
  1052.                 elif content_type == "Movies":
  1053.                     tree_widget.populate_tree(self.movies)
  1054.                 elif content_type == "Series":
  1055.                     tree_widget.populate_tree(self.series)
  1056.                 self.status_label.setText(f"Showing all {content_type}")
  1057.                
  1058.     def closeEvent(self, event):
  1059.         self.save_playlist_info()
  1060.         event.accept()
  1061.  
  1062. def main():
  1063.     os.environ['PYTHONHTTPSVERIFY'] = '0'
  1064.    
  1065.     app = QApplication(sys.argv)
  1066.     player = IPTVPlayer()
  1067.     player.show()
  1068.     sys.exit(app.exec_())
  1069.  
  1070. if __name__ == "__main__":
  1071.     import re
  1072.     main()
  1073.  
Add Comment
Please, Sign In to add comment