1
+ #!/usr/bin/env python
2
+ import sys
3
+ import re
4
+ import urllib .request
5
+ from youtube_transcript_api import YouTubeTranscriptApi
6
+
7
+ from PyQt5 .QtCore import QThread , pyqtSignal , Qt
8
+ from PyQt5 .QtGui import QPixmap , QPalette , QColor
9
+ from PyQt5 .QtWidgets import (
10
+ QApplication , QMainWindow , QWidget , QVBoxLayout , QHBoxLayout ,
11
+ QLabel , QLineEdit , QPushButton , QTextEdit , QMessageBox , QFileDialog ,
12
+ QComboBox , QCheckBox , QGroupBox , QFrame , QStyleFactory
13
+ )
14
+
15
+
16
+ def extract_video_id (url ):
17
+ """
18
+ Extracts the YouTube video ID from a given URL.
19
+ Supports both standard and shortened URLs.
20
+ """
21
+ pattern = r'(?:v=|\/)([0-9A-Za-z_-]{11}).*'
22
+ match = re .search (pattern , url )
23
+ if match :
24
+ return match .group (1 )
25
+ return None
26
+
27
+
28
+ def get_transcript (video_id , language = 'en' ):
29
+ """
30
+ Retrieves the transcript for the given YouTube video ID.
31
+ Returns the concatenated transcript text.
32
+ """
33
+ transcript_list = YouTubeTranscriptApi .get_transcript (video_id , languages = [language ])
34
+ transcript_text = " " .join ([entry ['text' ] for entry in transcript_list ])
35
+ return transcript_text
36
+
37
+
38
+ # Worker thread to fetch transcript without freezing the UI
39
+ class TranscriptFetcher (QThread ):
40
+ transcript_fetched = pyqtSignal (str )
41
+ error_occurred = pyqtSignal (str )
42
+
43
+ def __init__ (self , video_id , language = 'en' ):
44
+ super ().__init__ ()
45
+ self .video_id = video_id
46
+ self .language = language
47
+
48
+ def run (self ):
49
+ try :
50
+ transcript = get_transcript (self .video_id , self .language )
51
+ self .transcript_fetched .emit (transcript )
52
+ except Exception as e :
53
+ self .error_occurred .emit (str (e ))
54
+
55
+
56
+ class MainWindow (QMainWindow ):
57
+ def __init__ (self ):
58
+ super ().__init__ ()
59
+ self .setWindowTitle ("YouTube Transcript Fetcher" )
60
+ self .setGeometry (100 , 100 , 900 , 600 )
61
+ self .transcript = ""
62
+ self .setup_ui ()
63
+
64
+ def setup_ui (self ):
65
+ # Set a main widget and apply a vertical layout
66
+ self .main_widget = QWidget ()
67
+ self .setCentralWidget (self .main_widget )
68
+ self .main_layout = QVBoxLayout (self .main_widget )
69
+ self .main_widget .setLayout (self .main_layout )
70
+
71
+ # Title Section: A banner frame for the header
72
+ self .banner_frame = QFrame ()
73
+ self .banner_frame .setObjectName ("bannerFrame" )
74
+ self .banner_layout = QHBoxLayout ()
75
+ self .banner_frame .setLayout (self .banner_layout )
76
+
77
+ self .header_label = QLabel ("YouTube Transcript Fetcher" )
78
+ self .header_label .setObjectName ("headerLabel" )
79
+ self .header_label .setAlignment (Qt .AlignCenter )
80
+
81
+ self .banner_layout .addWidget (self .header_label )
82
+ self .main_layout .addWidget (self .banner_frame )
83
+
84
+ # Description below the header
85
+ self .description_label = QLabel ("Easily fetch and save YouTube transcripts with a single click." )
86
+ self .description_label .setAlignment (Qt .AlignCenter )
87
+ self .main_layout .addWidget (self .description_label )
88
+
89
+ # Group box for URL input
90
+ self .url_group = QGroupBox ("Video URL" )
91
+ self .url_group_layout = QHBoxLayout ()
92
+ self .url_group .setLayout (self .url_group_layout )
93
+
94
+ self .url_input = QLineEdit ()
95
+ self .url_input .setPlaceholderText ("Enter YouTube URL here..." )
96
+ self .fetch_button = QPushButton ("Fetch Transcript" )
97
+ self .fetch_button .clicked .connect (self .fetch_transcript )
98
+
99
+ self .url_group_layout .addWidget (self .url_input )
100
+ self .url_group_layout .addWidget (self .fetch_button )
101
+ self .main_layout .addWidget (self .url_group )
102
+
103
+ # Group box for Video Thumbnail
104
+ self .thumbnail_group = QGroupBox ("Video Preview" )
105
+ self .thumbnail_layout = QVBoxLayout ()
106
+ self .thumbnail_group .setLayout (self .thumbnail_layout )
107
+
108
+ self .thumbnail_label = QLabel ()
109
+ self .thumbnail_label .setAlignment (Qt .AlignCenter )
110
+ self .thumbnail_layout .addWidget (self .thumbnail_label )
111
+ self .main_layout .addWidget (self .thumbnail_group )
112
+
113
+ # Group box for Transcript display
114
+ self .transcript_group = QGroupBox ("Transcript" )
115
+ self .transcript_group_layout = QVBoxLayout ()
116
+ self .transcript_group .setLayout (self .transcript_group_layout )
117
+
118
+ self .transcript_display = QTextEdit ()
119
+ self .transcript_display .setReadOnly (True )
120
+ self .transcript_group_layout .addWidget (self .transcript_display )
121
+ self .main_layout .addWidget (self .transcript_group )
122
+
123
+ # Group box for Actions (Save + Status)
124
+ self .actions_group = QGroupBox ("Actions" )
125
+ self .actions_layout = QHBoxLayout ()
126
+ self .actions_group .setLayout (self .actions_layout )
127
+
128
+ self .save_button = QPushButton ("Save Transcript" )
129
+ self .save_button .clicked .connect (self .save_transcript )
130
+ self .status_label = QLabel ("Ready" )
131
+
132
+ self .actions_layout .addWidget (self .save_button )
133
+ self .actions_layout .addWidget (self .status_label )
134
+ self .main_layout .addWidget (self .actions_group )
135
+
136
+ # Group box for Settings (Language + Theme)
137
+ self .settings_group = QGroupBox ("Settings" )
138
+ self .settings_layout = QHBoxLayout ()
139
+ self .settings_group .setLayout (self .settings_layout )
140
+
141
+ self .language_label = QLabel ("Language:" )
142
+ self .language_combo = QComboBox ()
143
+ self .language_combo .addItems (["en" , "es" , "fr" , "de" , "it" ]) # Example languages
144
+
145
+ self .theme_toggle = QCheckBox ("Dark Mode" )
146
+ self .theme_toggle .stateChanged .connect (self .toggle_theme )
147
+
148
+ self .settings_layout .addWidget (self .language_label )
149
+ self .settings_layout .addWidget (self .language_combo )
150
+ self .settings_layout .addStretch (1 )
151
+ self .settings_layout .addWidget (self .theme_toggle )
152
+ self .main_layout .addWidget (self .settings_group )
153
+
154
+ # Apply a custom style sheet for a more modern look
155
+ self .apply_style_sheet ()
156
+
157
+ def apply_style_sheet (self ):
158
+ """
159
+ Applies a style sheet to give the UI a more modern, consistent look.
160
+ """
161
+ self .setStyleSheet ("""
162
+ /* Overall Window Style */
163
+ QMainWindow {
164
+ background-color: #f7f7f7;
165
+ }
166
+
167
+ /* Banner Frame */
168
+ #bannerFrame {
169
+ background-color: #1976D2; /* A modern blue color */
170
+ padding: 12px;
171
+ }
172
+ /* Header Label in Banner */
173
+ #headerLabel {
174
+ color: white;
175
+ font-size: 22px;
176
+ font-weight: 600;
177
+ letter-spacing: 0.5px;
178
+ }
179
+
180
+ /* Group Boxes */
181
+ QGroupBox {
182
+ font: 14px 'Arial';
183
+ font-weight: bold;
184
+ margin-top: 10px;
185
+ border: 1px solid #ccc;
186
+ border-radius: 8px;
187
+ padding: 10px;
188
+ }
189
+ QGroupBox::title {
190
+ subcontrol-origin: margin;
191
+ subcontrol-position: top left;
192
+ padding: 2px 5px;
193
+ }
194
+
195
+ /* Labels */
196
+ QLabel {
197
+ font: 13px 'Arial';
198
+ color: #333;
199
+ }
200
+
201
+ /* Line Edit */
202
+ QLineEdit {
203
+ font: 13px 'Arial';
204
+ border-radius: 5px;
205
+ padding: 6px;
206
+ border: 1px solid #bbb;
207
+ background-color: #fff;
208
+ }
209
+
210
+ /* Text Edit */
211
+ QTextEdit {
212
+ font: 13px 'Arial';
213
+ border-radius: 5px;
214
+ border: 1px solid #bbb;
215
+ background-color: #fff;
216
+ }
217
+
218
+ /* Push Buttons */
219
+ QPushButton {
220
+ font: 13px 'Arial';
221
+ border-radius: 5px;
222
+ padding: 6px 14px;
223
+ background-color: #2196F3;
224
+ color: white;
225
+ border: none;
226
+ }
227
+ QPushButton:hover {
228
+ background-color: #1976D2;
229
+ }
230
+ QPushButton:disabled {
231
+ background-color: #9e9e9e;
232
+ color: #f0f0f0;
233
+ }
234
+
235
+ /* Check Box */
236
+ QCheckBox {
237
+ font: 13px 'Arial';
238
+ color: #333;
239
+ }
240
+
241
+ /* Combo Box */
242
+ QComboBox {
243
+ font: 13px 'Arial';
244
+ border-radius: 5px;
245
+ padding: 4px;
246
+ border: 1px solid #bbb;
247
+ background-color: #fff;
248
+ }
249
+ """ )
250
+
251
+ def fetch_transcript (self ):
252
+ url = self .url_input .text ().strip ()
253
+ if not url :
254
+ QMessageBox .warning (self , "Input Error" , "Please enter a YouTube URL." )
255
+ return
256
+
257
+ video_id = extract_video_id (url ) or url
258
+ self .status_label .setText (f"Fetching transcript for video ID: { video_id } ..." )
259
+ self .fetch_button .setEnabled (False )
260
+ self .transcript_display .clear ()
261
+ self .thumbnail_label .clear ()
262
+
263
+ # Load video thumbnail if available
264
+ thumbnail_url = f"https://img.youtube.com/vi/{ video_id } /0.jpg"
265
+ try :
266
+ data = urllib .request .urlopen (thumbnail_url ).read ()
267
+ pixmap = QPixmap ()
268
+ pixmap .loadFromData (data )
269
+ self .thumbnail_label .setPixmap (pixmap .scaled (320 , 180 , Qt .KeepAspectRatio ))
270
+ except Exception :
271
+ # Thumbnail is optional; ignore errors if not available
272
+ pass
273
+
274
+ language = self .language_combo .currentText ()
275
+ # Start background thread to fetch transcript
276
+ self .worker = TranscriptFetcher (video_id , language )
277
+ self .worker .transcript_fetched .connect (self .on_transcript_fetched )
278
+ self .worker .error_occurred .connect (self .on_error )
279
+ self .worker .start ()
280
+
281
+ def on_transcript_fetched (self , transcript ):
282
+ self .transcript = transcript
283
+ self .transcript_display .setPlainText (transcript )
284
+ word_count = len (transcript .split ())
285
+ self .status_label .setText (f"Transcript retrieved. Word count: { word_count } " )
286
+ self .fetch_button .setEnabled (True )
287
+
288
+ def on_error (self , error_message ):
289
+ QMessageBox .critical (self , "Error Fetching Transcript" , error_message )
290
+ self .status_label .setText ("Error fetching transcript." )
291
+ self .fetch_button .setEnabled (True )
292
+
293
+ def save_transcript (self ):
294
+ if not self .transcript :
295
+ QMessageBox .warning (self , "No Transcript" , "There is no transcript to save." )
296
+ return
297
+ options = QFileDialog .Options ()
298
+ filename , _ = QFileDialog .getSaveFileName (
299
+ self , "Save Transcript" , "" , "Text Files (*.txt);;All Files (*)" , options = options
300
+ )
301
+ if filename :
302
+ try :
303
+ with open (filename , "w" , encoding = "utf-8" ) as f :
304
+ f .write (self .transcript )
305
+ self .status_label .setText (f"Transcript saved to { filename } " )
306
+ except Exception as e :
307
+ QMessageBox .critical (self , "Save Error" , str (e ))
308
+ self .status_label .setText ("Error saving transcript." )
309
+
310
+ def toggle_theme (self , state ):
311
+ """
312
+ Switches between light and dark themes by applying custom palettes.
313
+ """
314
+ if state == Qt .Checked :
315
+ # Define dark palette
316
+ dark_palette = QPalette ()
317
+ dark_palette .setColor (QPalette .Window , QColor (53 , 53 , 53 ))
318
+ dark_palette .setColor (QPalette .WindowText , Qt .white )
319
+ dark_palette .setColor (QPalette .Base , QColor (25 , 25 , 25 ))
320
+ dark_palette .setColor (QPalette .AlternateBase , QColor (53 , 53 , 53 ))
321
+ dark_palette .setColor (QPalette .ToolTipBase , Qt .white )
322
+ dark_palette .setColor (QPalette .ToolTipText , Qt .white )
323
+ dark_palette .setColor (QPalette .Text , Qt .white )
324
+ dark_palette .setColor (QPalette .Button , QColor (53 , 53 , 53 ))
325
+ dark_palette .setColor (QPalette .ButtonText , Qt .white )
326
+ dark_palette .setColor (QPalette .BrightText , Qt .red )
327
+ dark_palette .setColor (QPalette .Highlight , QColor (142 , 45 , 197 ))
328
+ dark_palette .setColor (QPalette .HighlightedText , Qt .black )
329
+
330
+ QApplication .instance ().setPalette (dark_palette )
331
+ else :
332
+ # Define light palette
333
+ light_palette = QPalette ()
334
+ light_palette .setColor (QPalette .Window , Qt .white )
335
+ light_palette .setColor (QPalette .WindowText , Qt .black )
336
+ light_palette .setColor (QPalette .Base , Qt .white )
337
+ light_palette .setColor (QPalette .AlternateBase , QColor (240 , 240 , 240 ))
338
+ light_palette .setColor (QPalette .ToolTipBase , Qt .white )
339
+ light_palette .setColor (QPalette .ToolTipText , Qt .black )
340
+ light_palette .setColor (QPalette .Text , Qt .black )
341
+ light_palette .setColor (QPalette .Button , QColor (240 , 240 , 240 ))
342
+ light_palette .setColor (QPalette .ButtonText , Qt .black )
343
+ light_palette .setColor (QPalette .BrightText , Qt .red )
344
+ light_palette .setColor (QPalette .Highlight , QColor (0 , 120 , 215 ))
345
+ light_palette .setColor (QPalette .HighlightedText , Qt .white )
346
+
347
+ QApplication .instance ().setPalette (light_palette )
348
+
349
+
350
+ if __name__ == "__main__" :
351
+ app = QApplication (sys .argv )
352
+ # Use the modern Fusion style
353
+ app .setStyle (QStyleFactory .create ("Fusion" ))
354
+
355
+ # Apply a light palette by default
356
+ light_palette = QPalette ()
357
+ light_palette .setColor (QPalette .Window , Qt .white )
358
+ light_palette .setColor (QPalette .WindowText , Qt .black )
359
+ light_palette .setColor (QPalette .Base , Qt .white )
360
+ light_palette .setColor (QPalette .AlternateBase , QColor (240 , 240 , 240 ))
361
+ light_palette .setColor (QPalette .ToolTipBase , Qt .white )
362
+ light_palette .setColor (QPalette .ToolTipText , Qt .black )
363
+ light_palette .setColor (QPalette .Text , Qt .black )
364
+ light_palette .setColor (QPalette .Button , QColor (240 , 240 , 240 ))
365
+ light_palette .setColor (QPalette .ButtonText , Qt .black )
366
+ light_palette .setColor (QPalette .BrightText , Qt .red )
367
+ light_palette .setColor (QPalette .Highlight , QColor (0 , 120 , 215 ))
368
+ light_palette .setColor (QPalette .HighlightedText , Qt .white )
369
+
370
+ app .setPalette (light_palette )
371
+
372
+ window = MainWindow ()
373
+ window .show ()
374
+ sys .exit (app .exec_ ())
0 commit comments