├── .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 | 
4 | 
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 |
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 |
--------------------------------------------------------------------------------