├── .gitignore ├── cli_requirements.txt ├── requirements.txt ├── download-icon.png ├── cli.py ├── README.md ├── core.py └── gui.py /.gitignore: -------------------------------------------------------------------------------- 1 | .venv 2 | venv 3 | 4 | .idea/* -------------------------------------------------------------------------------- /cli_requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.26.0 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | PyQt5==5.15.11 2 | requests==2.26.0 3 | -------------------------------------------------------------------------------- /download-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ali-0315/aparat_playlist_downloader/HEAD/download-icon.png -------------------------------------------------------------------------------- /cli.py: -------------------------------------------------------------------------------- 1 | from core import AparatDownloader 2 | 3 | if __name__ == "__main__": 4 | playlist_id = input("Give me a Aparat playlist id: ") 5 | quality = input( 6 | "Give me the quality: (Examples: 144 , 240 , 360 , 480 , 720 , 1080) :" 7 | ) 8 | for_download_manager = ( 9 | input( 10 | 'Type "y" if you want to create a .txt file that contain all the videos link otherwise ' 11 | 'type "n" to start download now:' 12 | ) 13 | == "y" 14 | ) 15 | destination_path = input("Give me the destination path (default: ./Downloads):") 16 | 17 | downloader = AparatDownloader( 18 | playlist_id=playlist_id, 19 | quality=quality, 20 | for_download_manager=for_download_manager, 21 | destination_path=destination_path, 22 | ) 23 | downloader.download_playlist() 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Aparat Playlist Downloader 2 | 3 | ![Python](https://img.shields.io/badge/Python-3.6+-blue.svg) 4 | ![Platform](https://img.shields.io/badge/Platform-Windows%20|%20Linux%20|%20macOS-lightgrey.svg) 5 | 6 | دانلودر حرفه‌ای پلی‌لیست‌های آپارات با دو رابط کاربری CLI و GUI 7 | 8 | 9 | ## 🔧 پیش‌نیازها 10 | 11 | - Python 3.6 یا بالاتر 12 | - pip (Python package manager) 13 | - اتصال به اینترنت 14 | 15 | ## 📦 نصب 16 | 17 | ### نصب سریع 18 | 19 | ```bash 20 | git clone https://github.com/ali-0315/aparat_playlist_downloader.git 21 | cd aparat_playlist_downloader 22 | pip install -r requirements.txt 23 | # یا اگر میخواهید فقط از cli استفاده کنید 24 | pip install -r cli_requirements 25 | ``` 26 | 27 | ## 🚀 نحوه استفاده 28 | 29 | ### رابط گرافیکی (GUI) 30 | 31 | رابط گرافیکی مدرن و کاربرپسند با قابلیت‌های پیشرفته: 32 | 33 | ```bash 34 | python gui.py 35 | ``` 36 | 37 | **مراحل استفاده:** 38 | 1. **انتخاب عملیات**: دانلود یا استخراج لینک 39 | 2. **وارد کردن شناسه**: یکی از فرمت‌های زیر: 40 | - شناسه عددی: `822374` 41 | - لینک کامل: `https://www.aparat.com/playlist/822374` 42 | 3. **انتخاب کیفیت**: 144, 240, 360, 480, 720, 1080 43 | 4. **انتخاب مسیر خروجی**: با کلیک روی "انتخاب" 44 | 5. **کلیک روی "اجرا"** 45 | 46 | ### خط فرمان (CLI) 47 | 48 | برای استفاده ساده و سریع: 49 | 50 | ```bash 51 | python cli.py 52 | ``` 53 | 54 | **نمونه اجرا:** 55 | ``` 56 | Give me a Aparat playlist id: 822374 57 | Give me the quality: (Examples: 144 , 240 , 360 , 480 , 720 , 1080) :720 58 | Type "y" if you want to create a .txt file that contain all the videos link otherwise type "n" to start download now:n 59 | Give me the destination path (default: ./Downloads):./MyDownloads 60 | ``` 61 | 62 | ## 📁 ساختار پروژه 63 | 64 | ``` 65 | aparat_playlist_downloader/ 66 | ├── core.py # کلاس اصلی AparatDownloader 67 | ├── gui.py # رابط گرافیکی PyQt5 68 | ├── cli.py # رابط خط فرمان 69 | ├── requirements.txt # وابستگی‌های کامل پروژه 70 | ├── cli_requirements.txt # وابستگی‌های CLI فقط 71 | └── README.md # مستندات پروژه 72 | ``` 73 | 74 | ### توضیحات فایل‌ها 75 | 76 | #### `core.py` - هسته اصلی 77 | ```python 78 | class AparatDownloader: 79 | def __init__(self, playlist_id, quality, for_download_manager, destination_path) 80 | def download_playlist() # دانلود کامل پلی‌لیست 81 | def download_video() # دانلود تک ویدئو 82 | def get_video_download_urls() # دریافت لینک‌های دانلود 83 | ``` 84 | 85 | ## 🔌 API آپارات 86 | 87 | پروژه از API های زیر آپارات استفاده می‌کند: 88 | 89 | ``` 90 | # دریافت اطلاعات پلی‌لیست 91 | GET https://www.aparat.com/api/fa/v1/video/playlist/one/playlist_id/{playlist_id} 92 | 93 | # دریافت لینک‌های دانلود ویدئو 94 | GET https://www.aparat.com/api/fa/v1/video/video/show/videohash/{video_uid} 95 | ``` 96 | 97 | **پاسخ نمونه API:** 98 | ```json 99 | { 100 | "data": { 101 | "attributes": { 102 | "title": "نام پلی‌لیست", 103 | "file_link_all": [ 104 | { 105 | "profile": "720p", 106 | "urls": ["https://example.com/video.mp4"] 107 | } 108 | ] 109 | } 110 | }, 111 | "included": [/* ویدئوهای پلی‌لیست */] 112 | } 113 | ``` 114 | 115 | ## 🔄 نمونه استفاده برنامه‌نویسی 116 | 117 | ```python 118 | from core import AparatDownloader 119 | 120 | # ایجاد instance 121 | downloader = AparatDownloader( 122 | playlist_id="822374", 123 | quality="720", 124 | for_download_manager=False, # True برای txt فایل 125 | destination_path="./Downloads" 126 | ) 127 | 128 | # شروع دانلود 129 | try: 130 | downloader.download_playlist() 131 | print("دانلود با موفقیت انجام شد!") 132 | except Exception as e: 133 | print(f"خطا: {e}") 134 | ``` 135 | 136 | ## 🤝 مشارکت 137 | 138 | ### مراحل مشارکت 139 | 140 | 1. **Fork** کردن پروژه 141 | 2. ایجاد **branch** جدید: 142 | ```bash 143 | git checkout -b feature/amazing-feature 144 | ``` 145 | 3. **Commit** تغییرات: 146 | ```bash 147 | git commit -m 'Add some amazing feature' 148 | ``` 149 | 4. **Push** به branch: 150 | ```bash 151 | git push origin feature/amazing-feature 152 | ``` 153 | 5. ایجاد **Pull Request** 154 | 155 | ## 🙏 تشکر و قدردانی 156 |
157 |

با تشکر ویژه از عزیزان

158 | 159 | 160 | 167 | 174 | 175 |
161 | 162 | علی اکبر سبحانپور 163 |
164 | علی اکبر سبحانپور 165 |
166 |
168 | 169 | علیرضا 170 |
171 | علیرضا 172 |
173 |
176 |
177 | 178 | --- 179 | 180 |
181 | 182 | **⭐ اگر این پروژه مفید بود، ستاره بدهید!** 183 | 184 | `نوشته شده با ❤️ ` 185 | 186 |
187 | 188 | ## 🏷️ تگ‌ها 189 | 190 | `aparat` `downloader` `playlist` `python` `pyqt5` `gui` `cli` `video-downloader` -------------------------------------------------------------------------------- /core.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import os 3 | import logging 4 | 5 | 6 | class AparatDownloader: 7 | def __init__( 8 | self, 9 | playlist_id=None, 10 | quality=None, 11 | for_download_manager=False, 12 | destination_path="Downloads", 13 | ): 14 | self.playlist_id = playlist_id 15 | self.quality = quality 16 | self.for_download_manager = for_download_manager 17 | self.destination_path = destination_path 18 | self.current_directory = os.getcwd() 19 | self.logger = self.setup_logger() 20 | 21 | if not os.path.exists(destination_path): 22 | os.mkdir(destination_path) 23 | 24 | @staticmethod 25 | def setup_logger(): 26 | logger = logging.getLogger("AparatDownloader") 27 | logger.setLevel(logging.INFO) 28 | 29 | formatter = logging.Formatter( 30 | "%(asctime)s - %(name)s - %(levelname)s - %(message)s" 31 | ) 32 | 33 | console_handler = logging.StreamHandler() 34 | console_handler.setFormatter(formatter) 35 | 36 | logger.addHandler(console_handler) 37 | return logger 38 | 39 | def download_video(self, video_url, output_path): 40 | response = requests.get(video_url, stream=True) 41 | if response.status_code == 200: 42 | with open(output_path, "wb") as file: 43 | for chunk in response.iter_content(chunk_size=1024): 44 | file.write(chunk) 45 | full_output_path = os.path.join(self.current_directory, output_path) 46 | self.logger.info(f"Downloaded to {full_output_path}") 47 | else: 48 | self.logger.error("Failed to download the video.") 49 | 50 | @staticmethod 51 | def get_video_download_urls(video_uid): 52 | video_url = ( 53 | f"https://www.aparat.com/api/fa/v1/video/video/show/videohash/{video_uid}" 54 | ) 55 | 56 | video_response = requests.get(video_url) 57 | video_data = video_response.json() 58 | return video_data["data"]["attributes"]["file_link_all"] 59 | 60 | def download_playlist(self): 61 | assert self.playlist_id is not None 62 | assert self.quality is not None 63 | 64 | api_url = f"https://www.aparat.com/api/fa/v1/video/playlist/one/playlist_id/{self.playlist_id}" 65 | try: 66 | response = requests.get(api_url) 67 | data = response.json() 68 | videos = data["included"] 69 | play_list_title = data["data"]["attributes"]["title"] 70 | 71 | if self.for_download_manager: 72 | self.logger.info(f"Start creating {play_list_title}.txt file") 73 | else: 74 | self.logger.info(f"Downloading Playlist {play_list_title} ...") 75 | 76 | if not os.path.exists(f"{self.destination_path}/{play_list_title}"): 77 | os.mkdir(f"{self.destination_path}/{play_list_title}") 78 | 79 | for video in videos: 80 | if video["type"] == "Video": 81 | video_uid = video["attributes"]["uid"] 82 | video_title = video["attributes"]["title"] 83 | video_download_link_all = self.get_video_download_urls(video_uid) 84 | found_flag = False 85 | for video_download_link in video_download_link_all: 86 | if video_download_link["profile"] == self.quality + "p": 87 | found_flag = True 88 | 89 | if self.for_download_manager: 90 | with open( 91 | f"{self.destination_path}/{play_list_title}.txt", 92 | "a", 93 | ) as links_txt: 94 | links_txt.write( 95 | f"{video_download_link['urls'][0]}\n" 96 | ) 97 | else: 98 | download_url = video_download_link["urls"][0] 99 | output_path = f"{self.destination_path}/{play_list_title}/{video_title}-{self.quality}p.mp4" 100 | self.download_video(download_url, output_path) 101 | 102 | if not found_flag: 103 | video_download_link = video_download_link_all[-1] 104 | 105 | if self.for_download_manager: 106 | self.logger.warning( 107 | f"Failed to find video ({video_title}) with selected quality; Add another quality link inside" 108 | ) 109 | with open(f"{play_list_title}.txt", "a") as links_txt: 110 | links_txt.write(f"{video_download_link['urls'][0]}\n") 111 | else: 112 | self.logger.warning( 113 | f"Failed to find video ({video_title}) with selected quality; Download another quality inside" 114 | ) 115 | download_url = video_download_link["urls"][0] 116 | output_path = f"{self.destination_path}/{play_list_title}/{video_title}-{self.quality}p.mp4" 117 | self.download_video(download_url, output_path) 118 | 119 | if self.for_download_manager: 120 | self.logger.info(f"{play_list_title}.txt created") 121 | 122 | except KeyError: 123 | self.logger.error("We have some errors in getting API data!") 124 | 125 | except ConnectionError: 126 | self.logger.error("Please check your internet connection.") 127 | 128 | except Exception as e: 129 | self.logger.error(e) 130 | -------------------------------------------------------------------------------- /gui.py: -------------------------------------------------------------------------------- 1 | import os 2 | import platform 3 | import re 4 | import subprocess 5 | import sys 6 | 7 | from PyQt5.QtCore import Qt, QThread, pyqtSignal, QTimer 8 | from PyQt5.QtGui import QFont 9 | from PyQt5.QtWidgets import ( 10 | QApplication, 11 | QMainWindow, 12 | QComboBox, 13 | QLineEdit, 14 | QPushButton, 15 | QVBoxLayout, 16 | QHBoxLayout, 17 | QWidget, 18 | QLabel, 19 | QFileDialog, 20 | QFrame, 21 | QStyle, 22 | QListView, 23 | QMessageBox, 24 | QProgressBar, 25 | ) 26 | 27 | from core import AparatDownloader 28 | 29 | 30 | class DownloadWorker(QThread): 31 | finished = pyqtSignal(bool, str) # success, message 32 | 33 | def __init__(self, playlist_id, quality, for_download_manager, destination_path): 34 | super().__init__() 35 | self.playlist_id = playlist_id 36 | self.quality = quality 37 | self.for_download_manager = for_download_manager 38 | self.destination_path = destination_path 39 | 40 | def run(self): 41 | try: 42 | downloader = AparatDownloader( 43 | playlist_id=self.playlist_id, 44 | quality=self.quality, 45 | for_download_manager=self.for_download_manager, 46 | destination_path=self.destination_path, 47 | ) 48 | downloader.download_playlist() 49 | self.finished.emit(True, "عملیات با موفقیت به پایان رسید.") 50 | except Exception as e: 51 | error_msg = f"ارور هنگام انجام عملیات: {str(e)}\nلطفا جزئیات ارور رو به عنوان issue در گیت هاب ارسال کنید تا بررسی کنیم." 52 | self.finished.emit(False, error_msg) 53 | 54 | 55 | class LoadingDialog(QWidget): 56 | def __init__(self, parent=None): 57 | super().__init__(parent) 58 | self.setWindowTitle("در حال پردازش...") 59 | self.setFixedSize(300, 120) 60 | self.setWindowFlags(Qt.Dialog | Qt.WindowTitleHint) 61 | self.setWindowModality(Qt.ApplicationModal) 62 | 63 | # Center the dialog 64 | if parent: 65 | parent_geo = parent.geometry() 66 | x = parent_geo.x() + (parent_geo.width() - self.width()) // 2 67 | y = parent_geo.y() + (parent_geo.height() - self.height()) // 2 68 | self.move(x, y) 69 | 70 | layout = QVBoxLayout(self) 71 | layout.setContentsMargins(20, 20, 20, 20) 72 | layout.setSpacing(15) 73 | 74 | # Loading label 75 | self.loading_label = QLabel("در حال پردازش...") 76 | self.loading_label.setAlignment(Qt.AlignCenter) 77 | font = QFont() 78 | font.setFamily("Segoe UI") 79 | font.setPointSize(12) 80 | self.loading_label.setFont(font) 81 | layout.addWidget(self.loading_label) 82 | 83 | # Progress bar 84 | self.progress_bar = QProgressBar() 85 | self.progress_bar.setRange(0, 0) # Indeterminate progress 86 | self.progress_bar.setStyleSheet(""" 87 | QProgressBar { 88 | border: 2px solid #e0e0e0; 89 | border-radius: 5px; 90 | text-align: center; 91 | background-color: #f5f5f5; 92 | } 93 | QProgressBar::chunk { 94 | background-color: #2196F3; 95 | border-radius: 3px; 96 | } 97 | """) 98 | layout.addWidget(self.progress_bar) 99 | 100 | # Animated dots for loading text 101 | self.dots_timer = QTimer() 102 | self.dots_timer.timeout.connect(self.animate_dots) 103 | self.dots_count = 0 104 | self.base_text = "در حال پردازش" 105 | 106 | def show(self): 107 | super().show() 108 | self.dots_timer.start(500) # Update every 500ms 109 | 110 | def hide(self): 111 | super().hide() 112 | self.dots_timer.stop() 113 | 114 | def animate_dots(self): 115 | self.dots_count = (self.dots_count + 1) % 4 116 | dots = "." * self.dots_count 117 | self.loading_label.setText(f"{self.base_text}{dots}") 118 | 119 | 120 | class ModernApp(QMainWindow): 121 | def __init__(self): 122 | super().__init__() 123 | self.quality_input = None 124 | self.browse_button = None 125 | self.folder_input = None 126 | self.link_input = None 127 | self.run_button = None 128 | self.combo_box = None 129 | self.frame_style = None 130 | self.worker = None 131 | self.loading_dialog = None 132 | self.init_ui() 133 | 134 | def init_ui(self): 135 | self.setWindowTitle("دانلود لیست پخش آپارات") 136 | self.setFixedSize(600, 500) 137 | 138 | main_widget = QWidget() 139 | self.setCentralWidget(main_widget) 140 | main_layout = QVBoxLayout(main_widget) 141 | main_layout.setContentsMargins(20, 20, 20, 20) 142 | main_layout.setSpacing(15) 143 | self.setLayoutDirection(Qt.RightToLeft) 144 | 145 | self.frame_style = """ 146 | QFrame { 147 | background-color: #f5f5f5; 148 | border-radius: 8px; 149 | border: 1px solid #e0e0e0; 150 | } 151 | """ 152 | 153 | font = QFont() 154 | font.setFamily("Segoe UI") 155 | font.setPointSize(10) 156 | 157 | combo_frame = QFrame() 158 | combo_frame.setStyleSheet(self.frame_style) 159 | combo_layout = QVBoxLayout(combo_frame) 160 | 161 | combo_label = QLabel("عملیات:") 162 | combo_label.setFont(font) 163 | combo_label.setStyleSheet(""" 164 | QLabel { 165 | border: 0; 166 | } 167 | """) 168 | self.combo_box = QComboBox() 169 | self.combo_box.addItems(["دانلود", "استخراج لینک ها"]) 170 | self.combo_box.setMinimumHeight(35) 171 | self.combo_box.setFont(font) 172 | self.combo_box.setView(QListView()) 173 | 174 | combo_layout.addWidget(combo_label) 175 | combo_layout.addWidget(self.combo_box) 176 | main_layout.addWidget(combo_frame) 177 | 178 | link_frame = QFrame() 179 | link_frame.setStyleSheet(self.frame_style) 180 | link_layout = QVBoxLayout(link_frame) 181 | 182 | link_label = QLabel("لینک و یا شناسه لیست پخش (playlist) آپارات را وارد کنید:") 183 | link_label.setStyleSheet(""" 184 | QLabel { 185 | border: 0; 186 | } 187 | """) 188 | link_label.setFont(font) 189 | self.link_input = QLineEdit() 190 | self.link_input.setPlaceholderText( 191 | "نمونه: 822374 یا https://www.aparat.com/playlist/822374" 192 | ) 193 | self.link_input.setMinimumHeight(35) 194 | self.link_input.setFont(font) 195 | self.link_input.setStyleSheet(""" 196 | QLineEdit { 197 | border: 1px solid #bdbdbd; 198 | border-radius: 4px; 199 | padding: 5px 10px; 200 | background-color: white; 201 | } 202 | QLineEdit:hover { 203 | border: 1px solid #2196F3; 204 | } 205 | QLineEdit:focus { 206 | border: 1px solid #2196F3; 207 | } 208 | """) 209 | 210 | link_layout.addWidget(link_label) 211 | link_layout.addWidget(self.link_input) 212 | main_layout.addWidget(link_frame) 213 | 214 | quality_frame = QFrame() 215 | quality_frame.setStyleSheet(self.frame_style) 216 | link_layout = QVBoxLayout(quality_frame) 217 | 218 | quality_label = QLabel("کیفیت مورد نظر را وارد کنید:") 219 | quality_label.setStyleSheet(""" 220 | QLabel { 221 | border: 0; 222 | } 223 | """) 224 | quality_label.setFont(font) 225 | self.quality_input = QLineEdit() 226 | self.quality_input.setPlaceholderText( 227 | "نمونه: 144 , 240 , 360 , 480 , 720 , 1080" 228 | ) 229 | self.quality_input.setMinimumHeight(35) 230 | self.quality_input.setFont(font) 231 | self.quality_input.setStyleSheet(""" 232 | QLineEdit { 233 | border: 1px solid #bdbdbd; 234 | border-radius: 4px; 235 | padding: 5px 10px; 236 | background-color: white; 237 | } 238 | QLineEdit:hover { 239 | border: 1px solid #2196F3; 240 | } 241 | QLineEdit:focus { 242 | border: 1px solid #2196F3; 243 | } 244 | """) 245 | 246 | link_layout.addWidget(quality_label) 247 | link_layout.addWidget(self.quality_input) 248 | main_layout.addWidget(quality_frame) 249 | 250 | folder_frame = QFrame() 251 | folder_frame.setStyleSheet(self.frame_style) 252 | folder_layout = QVBoxLayout(folder_frame) 253 | 254 | folder_label = QLabel("مسیر:") 255 | folder_label.setStyleSheet(""" 256 | QLabel { 257 | border: 0; 258 | } 259 | """) 260 | folder_label.setFont(font) 261 | 262 | folder_input_layout = QHBoxLayout() 263 | self.folder_input = QLineEdit() 264 | self.folder_input.setPlaceholderText("مسیر خروجی را وارد کنید...") 265 | self.folder_input.setMinimumHeight(35) 266 | self.folder_input.setFont(font) 267 | self.folder_input.setStyleSheet(""" 268 | QLineEdit { 269 | border: 1px solid #bdbdbd; 270 | border-radius: 4px; 271 | padding: 5px 10px; 272 | background-color: white; 273 | } 274 | QLineEdit:hover { 275 | border: 1px solid #2196F3; 276 | } 277 | """) 278 | 279 | self.browse_button = QPushButton("انتخاب") 280 | self.browse_button.setMinimumHeight(35) 281 | self.browse_button.setFont(font) 282 | self.browse_button.setIcon(self.style().standardIcon(QStyle.SP_DirOpenIcon)) 283 | self.browse_button.setCursor(Qt.PointingHandCursor) 284 | self.browse_button.setStyleSheet(""" 285 | QPushButton { 286 | background-color: #e0e0e0; 287 | border-radius: 4px; 288 | border: none; 289 | padding: 5px 15px; 290 | } 291 | QPushButton:hover { 292 | background-color: #d0d0d0; 293 | } 294 | QPushButton:pressed { 295 | background-color: #c0c0c0; 296 | } 297 | """) 298 | self.browse_button.clicked.connect(self.browse_folder) 299 | 300 | folder_input_layout.addWidget(self.folder_input) 301 | folder_input_layout.addWidget(self.browse_button) 302 | 303 | folder_layout.addWidget(folder_label) 304 | folder_layout.addLayout(folder_input_layout) 305 | main_layout.addWidget(folder_frame) 306 | 307 | self.run_button = QPushButton("اجرا") 308 | self.run_button.setMinimumHeight(45) 309 | self.run_button.setFont(font) 310 | self.run_button.setCursor(Qt.PointingHandCursor) 311 | self.run_button.setStyleSheet(""" 312 | QPushButton { 313 | background-color: #2196F3; 314 | color: white; 315 | border-radius: 5px; 316 | border: none; 317 | padding: 5px 15px; 318 | font-weight: bold; 319 | } 320 | QPushButton:hover { 321 | background-color: #1976D2; 322 | } 323 | QPushButton:pressed { 324 | background-color: #0D47A1; 325 | } 326 | QPushButton:disabled { 327 | background-color: #bdbdbd; 328 | } 329 | """) 330 | self.run_button.clicked.connect(self.run_action) 331 | 332 | main_layout.addWidget(self.run_button) 333 | 334 | main_layout.addStretch() 335 | 336 | self.center() 337 | self.show() 338 | 339 | def center(self): 340 | frame_geometry = self.frameGeometry() 341 | screen_center = QApplication.desktop().availableGeometry().center() 342 | frame_geometry.moveCenter(screen_center) 343 | self.move(frame_geometry.topLeft()) 344 | 345 | def browse_folder(self): 346 | folder_path = QFileDialog.getExistingDirectory(self, "انتخاب پوشه") 347 | if folder_path: 348 | self.folder_input.setText(folder_path) 349 | 350 | @staticmethod 351 | def show_error_message(errors): 352 | error_msg = QMessageBox() 353 | error_msg.setIcon(QMessageBox.Critical) 354 | error_msg.setWindowTitle("خطا") 355 | error_msg.setText("لطفاً خطاهای زیر را برطرف کنید:") 356 | 357 | error_text = "\n".join([f"- {err}" for err in errors]) 358 | error_msg.setInformativeText(error_text) 359 | 360 | error_msg.setStandardButtons(QMessageBox.Ok) 361 | ok_button = error_msg.button(QMessageBox.Ok) 362 | ok_button.setText("باشه") 363 | error_msg.exec_() 364 | 365 | def set_ui_enabled(self, enabled): 366 | """Enable/disable UI elements during operation""" 367 | self.link_input.setEnabled(enabled) 368 | self.quality_input.setEnabled(enabled) 369 | self.folder_input.setEnabled(enabled) 370 | self.browse_button.setEnabled(enabled) 371 | self.combo_box.setEnabled(enabled) 372 | self.run_button.setEnabled(enabled) 373 | 374 | if not enabled: 375 | self.run_button.setText("در حال پردازش...") 376 | else: 377 | self.run_button.setText("اجرا") 378 | 379 | def on_download_finished(self, success, message): 380 | """Handle download completion""" 381 | # Hide loading dialog 382 | if self.loading_dialog: 383 | self.loading_dialog.hide() 384 | 385 | # Re-enable UI 386 | self.set_ui_enabled(True) 387 | 388 | # Show result message 389 | if success: 390 | msg_box = QMessageBox() 391 | msg_box.setWindowTitle("موفقیت") 392 | msg_box.setText(message) 393 | msg_box.addButton("تایید", QMessageBox.AcceptRole) 394 | show_output_button = msg_box.addButton( 395 | "نمایش خروجی", QMessageBox.ActionRole 396 | ) 397 | msg_box.exec_() 398 | if msg_box.clickedButton() == show_output_button: 399 | folder_path = self.folder_input.text() 400 | if os.path.exists(folder_path): 401 | if platform.system() == "Windows": 402 | os.startfile(folder_path) 403 | elif platform.system() == "Darwin": 404 | subprocess.call(["open", folder_path]) 405 | else: 406 | subprocess.call(["xdg-open", folder_path]) 407 | else: 408 | msg_box = QMessageBox() 409 | msg_box.setWindowTitle("ناموفق") 410 | msg_box.setText(message) 411 | msg_box.addButton("تایید", QMessageBox.AcceptRole) 412 | msg_box.exec_() 413 | 414 | self.link_input.clear() 415 | self.folder_input.clear() 416 | 417 | # Clean up worker 418 | if self.worker: 419 | self.worker.deleteLater() 420 | self.worker = None 421 | 422 | def run_action(self): 423 | selected_option = self.combo_box.currentText() 424 | link = self.link_input.text() 425 | quality = self.quality_input.text() 426 | folder_path = self.folder_input.text() 427 | 428 | errors = [] 429 | 430 | if not link: 431 | errors.append("لطفاً لینک و یا شناسه را وارد کنید.") 432 | self.link_input.setStyleSheet(""" 433 | QLineEdit { 434 | border: 1px solid #f44336; 435 | border-radius: 6px; 436 | padding: 8px 15px; 437 | background-color: #ffebee; 438 | } 439 | """) 440 | else: 441 | is_valid = False 442 | 443 | if link.isdigit(): 444 | is_valid = True 445 | else: 446 | aparat_pattern = re.compile( 447 | r"^https://www\.aparat\.com/playlist/\d+/?$" 448 | ) 449 | if aparat_pattern.match(link): 450 | is_valid = True 451 | 452 | if not is_valid: 453 | errors.append( 454 | "ورودی باید شناسه عددی و یا به فرمت https://www.aparat.com/playlist/822374 باشد." 455 | ) 456 | self.link_input.setStyleSheet(""" 457 | QLineEdit { 458 | border: 1px solid #f44336; 459 | border-radius: 6px; 460 | padding: 8px 15px; 461 | background-color: #ffebee; 462 | } 463 | """) 464 | else: 465 | self.link_input.setStyleSheet(""" 466 | QLineEdit { 467 | border: 1px solid #bdbdbd; 468 | border-radius: 6px; 469 | padding: 8px 15px; 470 | background-color: white; 471 | } 472 | QLineEdit:hover { 473 | border: 1px solid #2196F3; 474 | } 475 | """) 476 | 477 | if not quality: 478 | errors.append("لطفاً کیفیت را وارد کنید.") 479 | self.quality_input.setStyleSheet(""" 480 | QLineEdit { 481 | border: 1px solid #f44336; 482 | border-radius: 6px; 483 | padding: 8px 15px; 484 | background-color: #ffebee; 485 | } 486 | """) 487 | else: 488 | if not quality.isdigit(): 489 | errors.append("ورودی باید عددی باشد.") 490 | self.quality_input.setStyleSheet(""" 491 | QLineEdit { 492 | border: 1px solid #f44336; 493 | border-radius: 6px; 494 | padding: 8px 15px; 495 | background-color: #ffebee; 496 | } 497 | """) 498 | else: 499 | self.quality_input.setStyleSheet(""" 500 | QLineEdit { 501 | border: 1px solid #bdbdbd; 502 | border-radius: 6px; 503 | padding: 8px 15px; 504 | background-color: white; 505 | } 506 | QLineEdit:hover { 507 | border: 1px solid #2196F3; 508 | } 509 | """) 510 | 511 | if not folder_path: 512 | errors.append("لطفاً مسیر خروجی را وارد کنید.") 513 | self.folder_input.setStyleSheet(""" 514 | QLineEdit { 515 | border: 1px solid #f44336; 516 | border-radius: 6px; 517 | padding: 8px 15px; 518 | background-color: #ffebee; 519 | } 520 | """) 521 | self.browse_button.setStyleSheet(""" 522 | QPushButton { 523 | border: 1px solid #f44336; 524 | border-radius: 6px; 525 | padding: 8px 15px; 526 | background-color: #ffebee; 527 | } 528 | """) 529 | else: 530 | if not os.path.exists(folder_path) or not os.path.isdir(folder_path): 531 | errors.append("مسیر پوشه وارد شده وجود ندارد یا یک پوشه نیست.") 532 | self.folder_input.setStyleSheet(""" 533 | QLineEdit { 534 | border: 1px solid #f44336; 535 | border-radius: 6px; 536 | padding: 8px 15px; 537 | background-color: #ffebee; 538 | } 539 | """) 540 | else: 541 | self.folder_input.setStyleSheet(""" 542 | QLineEdit { 543 | border: 1px solid #bdbdbd; 544 | border-radius: 6px; 545 | padding: 8px 15px; 546 | background-color: white; 547 | } 548 | QLineEdit:hover { 549 | border: 1px solid #2196F3; 550 | } 551 | """) 552 | self.browse_button.setStyleSheet(""" 553 | QPushButton { 554 | border: 1px solid #bdbdbd; 555 | border-radius: 6px; 556 | padding: 8px 15px; 557 | background-color: white; 558 | } 559 | """) 560 | 561 | if errors: 562 | self.show_error_message(errors) 563 | return 564 | 565 | # Disable UI during operation 566 | self.set_ui_enabled(False) 567 | 568 | # Show loading dialog 569 | self.loading_dialog = LoadingDialog(self) 570 | self.loading_dialog.show() 571 | 572 | # Start download in background thread 573 | playlist_id = link if link.isdigit() else link.split("/")[-1] 574 | for_download_manager = selected_option == "استخراج لینک ها" 575 | 576 | self.worker = DownloadWorker( 577 | playlist_id=playlist_id, 578 | quality=quality, 579 | for_download_manager=for_download_manager, 580 | destination_path=folder_path 581 | ) 582 | 583 | self.worker.finished.connect(self.on_download_finished) 584 | self.worker.start() 585 | 586 | 587 | if __name__ == "__main__": 588 | app = QApplication(sys.argv) 589 | app.setStyle("Fusion") 590 | 591 | app.setStyleSheet(""" 592 | QMainWindow { 593 | background-color: white; 594 | } 595 | QLabel { 596 | color: #424242; 597 | } 598 | """) 599 | 600 | window = ModernApp() 601 | sys.exit(app.exec_()) 602 | --------------------------------------------------------------------------------