christian commited on
Commit
18bf216
·
1 Parent(s): 76380ba
requirements.txt CHANGED
Binary files a/requirements.txt and b/requirements.txt differ
 
test/test_video_downloader.py CHANGED
@@ -10,7 +10,8 @@ sys.path.insert(0, str(Path(__file__).parent.parent))
10
  from video_downloader import VideoDownloader
11
 
12
 
13
- def test_video_downloader_init_default():
 
14
  # Setup
15
  temp_dir = None
16
  downloader = None
@@ -26,6 +27,7 @@ def test_video_downloader_init_default():
26
  assert 'outtmpl' in downloader.ydl_opts
27
  assert 'format' in downloader.ydl_opts
28
  assert 'noplaylist' in downloader.ydl_opts
 
29
 
30
  finally:
31
  # Teardown
@@ -33,7 +35,8 @@ def test_video_downloader_init_default():
33
  shutil.rmtree("downloads", ignore_errors=True)
34
 
35
 
36
- def test_video_downloader_init_custom():
 
37
  # Setup
38
  temp_dir = tempfile.mkdtemp()
39
  test_output_dir = os.path.join(temp_dir, "test_downloads")
@@ -46,6 +49,7 @@ def test_video_downloader_init_custom():
46
  # Verify
47
  assert str(downloader.output_dir) == test_output_dir
48
  assert downloader.output_dir.exists()
 
49
 
50
  finally:
51
  # Teardown
@@ -53,7 +57,25 @@ def test_video_downloader_init_custom():
53
  shutil.rmtree(temp_dir, ignore_errors=True)
54
 
55
 
56
- def test_validate_valid_http_url():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
57
  # Setup
58
  temp_dir = tempfile.mkdtemp()
59
  downloader = VideoDownloader(temp_dir)
@@ -71,7 +93,8 @@ def test_validate_valid_http_url():
71
  shutil.rmtree(temp_dir, ignore_errors=True)
72
 
73
 
74
- def test_validate_valid_https_url():
 
75
  # Setup
76
  temp_dir = tempfile.mkdtemp()
77
  downloader = VideoDownloader(temp_dir)
@@ -89,7 +112,8 @@ def test_validate_valid_https_url():
89
  shutil.rmtree(temp_dir, ignore_errors=True)
90
 
91
 
92
- def test_validate_invalid_url():
 
93
  # Setup
94
  temp_dir = tempfile.mkdtemp()
95
  downloader = VideoDownloader(temp_dir)
@@ -107,7 +131,8 @@ def test_validate_invalid_url():
107
  shutil.rmtree(temp_dir, ignore_errors=True)
108
 
109
 
110
- def test_validate_empty_url():
 
111
  # Setup
112
  temp_dir = tempfile.mkdtemp()
113
  downloader = VideoDownloader(temp_dir)
@@ -125,7 +150,8 @@ def test_validate_empty_url():
125
  shutil.rmtree(temp_dir, ignore_errors=True)
126
 
127
 
128
- def test_is_direct_video_link_mp4():
 
129
  # Setup
130
  temp_dir = tempfile.mkdtemp()
131
  downloader = VideoDownloader(temp_dir)
@@ -143,7 +169,8 @@ def test_is_direct_video_link_mp4():
143
  shutil.rmtree(temp_dir, ignore_errors=True)
144
 
145
 
146
- def test_is_direct_video_link_avi():
 
147
  # Setup
148
  temp_dir = tempfile.mkdtemp()
149
  downloader = VideoDownloader(temp_dir)
@@ -161,7 +188,8 @@ def test_is_direct_video_link_avi():
161
  shutil.rmtree(temp_dir, ignore_errors=True)
162
 
163
 
164
- def test_is_not_direct_video_link_youtube():
 
165
  # Setup
166
  temp_dir = tempfile.mkdtemp()
167
  downloader = VideoDownloader(temp_dir)
@@ -179,7 +207,8 @@ def test_is_not_direct_video_link_youtube():
179
  shutil.rmtree(temp_dir, ignore_errors=True)
180
 
181
 
182
- def test_is_not_direct_video_link_html():
 
183
  # Setup
184
  temp_dir = tempfile.mkdtemp()
185
  downloader = VideoDownloader(temp_dir)
@@ -197,8 +226,9 @@ def test_is_not_direct_video_link_html():
197
  shutil.rmtree(temp_dir, ignore_errors=True)
198
 
199
 
 
200
  @patch('video_downloader.requests.get')
201
- def test_download_direct_video_success(mock_get):
202
  # Setup
203
  temp_dir = tempfile.mkdtemp()
204
  downloader = VideoDownloader(temp_dir)
@@ -226,8 +256,9 @@ def test_download_direct_video_success(mock_get):
226
  shutil.rmtree(temp_dir, ignore_errors=True)
227
 
228
 
 
229
  @patch('video_downloader.requests.get')
230
- def test_download_direct_video_no_filename(mock_get):
231
  # Setup
232
  temp_dir = tempfile.mkdtemp()
233
  downloader = VideoDownloader(temp_dir)
@@ -252,8 +283,9 @@ def test_download_direct_video_no_filename(mock_get):
252
  shutil.rmtree(temp_dir, ignore_errors=True)
253
 
254
 
 
255
  @patch('video_downloader.requests.get')
256
- def test_download_direct_video_request_error(mock_get):
257
  # Setup
258
  temp_dir = tempfile.mkdtemp()
259
  downloader = VideoDownloader(temp_dir)
@@ -273,8 +305,9 @@ def test_download_direct_video_request_error(mock_get):
273
  shutil.rmtree(temp_dir, ignore_errors=True)
274
 
275
 
 
276
  @patch('video_downloader.yt_dlp.YoutubeDL')
277
- def test_download_with_ytdlp_success(mock_ytdl_class):
278
  # Setup
279
  temp_dir = tempfile.mkdtemp()
280
  downloader = VideoDownloader(temp_dir)
@@ -303,8 +336,9 @@ def test_download_with_ytdlp_success(mock_ytdl_class):
303
  # Teardown
304
  shutil.rmtree(temp_dir, ignore_errors=True)
305
 
 
306
  @patch('video_downloader.VideoFileClip')
307
- def test_extract_audio_success(mock_video_clip):
308
  # Setup
309
  temp_dir = tempfile.mkdtemp()
310
  downloader = VideoDownloader(temp_dir)
@@ -333,8 +367,9 @@ def test_extract_audio_success(mock_video_clip):
333
  shutil.rmtree(temp_dir, ignore_errors=True)
334
 
335
 
 
336
  @patch('video_downloader.VideoFileClip')
337
- def test_extract_audio_mp3_format(mock_video_clip):
338
  # Setup
339
  temp_dir = tempfile.mkdtemp()
340
  downloader = VideoDownloader(temp_dir)
@@ -366,7 +401,8 @@ def test_extract_audio_mp3_format(mock_video_clip):
366
  @patch.object(VideoDownloader, 'download_direct_video')
367
  @patch.object(VideoDownloader, 'is_direct_video_link')
368
  @patch.object(VideoDownloader, 'validate_url')
369
- def test_process_url_direct_video_with_audio(mock_validate, mock_is_direct, mock_download, mock_extract):
 
370
  # Setup
371
  temp_dir = tempfile.mkdtemp()
372
  downloader = VideoDownloader(temp_dir)
@@ -397,7 +433,8 @@ def test_process_url_direct_video_with_audio(mock_validate, mock_is_direct, mock
397
 
398
 
399
  @patch.object(VideoDownloader, 'validate_url')
400
- def test_process_url_invalid_url(mock_validate):
 
401
  # Setup
402
  temp_dir = tempfile.mkdtemp()
403
  downloader = VideoDownloader(temp_dir)
 
10
  from video_downloader import VideoDownloader
11
 
12
 
13
+ @patch.object(VideoDownloader, '_configure_cookies')
14
+ def test_video_downloader_init_default(mock_configure_cookies):
15
  # Setup
16
  temp_dir = None
17
  downloader = None
 
27
  assert 'outtmpl' in downloader.ydl_opts
28
  assert 'format' in downloader.ydl_opts
29
  assert 'noplaylist' in downloader.ydl_opts
30
+ mock_configure_cookies.assert_called_once()
31
 
32
  finally:
33
  # Teardown
 
35
  shutil.rmtree("downloads", ignore_errors=True)
36
 
37
 
38
+ @patch.object(VideoDownloader, '_configure_cookies')
39
+ def test_video_downloader_init_custom(mock_configure_cookies):
40
  # Setup
41
  temp_dir = tempfile.mkdtemp()
42
  test_output_dir = os.path.join(temp_dir, "test_downloads")
 
49
  # Verify
50
  assert str(downloader.output_dir) == test_output_dir
51
  assert downloader.output_dir.exists()
52
+ mock_configure_cookies.assert_called_once()
53
 
54
  finally:
55
  # Teardown
 
57
  shutil.rmtree(temp_dir, ignore_errors=True)
58
 
59
 
60
+ @patch.object(VideoDownloader, '_configure_cookies')
61
+ def test_configure_cookies_success(mock_configure_cookies):
62
+ # Setup
63
+ temp_dir = tempfile.mkdtemp()
64
+
65
+ try:
66
+ # Exercise
67
+ downloader = VideoDownloader(temp_dir)
68
+
69
+ # Verify
70
+ mock_configure_cookies.assert_called_once()
71
+
72
+ finally:
73
+ # Teardown
74
+ shutil.rmtree(temp_dir, ignore_errors=True)
75
+
76
+
77
+ @patch.object(VideoDownloader, '_configure_cookies')
78
+ def test_validate_valid_http_url(mock_configure_cookies):
79
  # Setup
80
  temp_dir = tempfile.mkdtemp()
81
  downloader = VideoDownloader(temp_dir)
 
93
  shutil.rmtree(temp_dir, ignore_errors=True)
94
 
95
 
96
+ @patch.object(VideoDownloader, '_configure_cookies')
97
+ def test_validate_valid_https_url(mock_configure_cookies):
98
  # Setup
99
  temp_dir = tempfile.mkdtemp()
100
  downloader = VideoDownloader(temp_dir)
 
112
  shutil.rmtree(temp_dir, ignore_errors=True)
113
 
114
 
115
+ @patch.object(VideoDownloader, '_configure_cookies')
116
+ def test_validate_invalid_url(mock_configure_cookies):
117
  # Setup
118
  temp_dir = tempfile.mkdtemp()
119
  downloader = VideoDownloader(temp_dir)
 
131
  shutil.rmtree(temp_dir, ignore_errors=True)
132
 
133
 
134
+ @patch.object(VideoDownloader, '_configure_cookies')
135
+ def test_validate_empty_url(mock_configure_cookies):
136
  # Setup
137
  temp_dir = tempfile.mkdtemp()
138
  downloader = VideoDownloader(temp_dir)
 
150
  shutil.rmtree(temp_dir, ignore_errors=True)
151
 
152
 
153
+ @patch.object(VideoDownloader, '_configure_cookies')
154
+ def test_is_direct_video_link_mp4(mock_configure_cookies):
155
  # Setup
156
  temp_dir = tempfile.mkdtemp()
157
  downloader = VideoDownloader(temp_dir)
 
169
  shutil.rmtree(temp_dir, ignore_errors=True)
170
 
171
 
172
+ @patch.object(VideoDownloader, '_configure_cookies')
173
+ def test_is_direct_video_link_avi(mock_configure_cookies):
174
  # Setup
175
  temp_dir = tempfile.mkdtemp()
176
  downloader = VideoDownloader(temp_dir)
 
188
  shutil.rmtree(temp_dir, ignore_errors=True)
189
 
190
 
191
+ @patch.object(VideoDownloader, '_configure_cookies')
192
+ def test_is_not_direct_video_link_youtube(mock_configure_cookies):
193
  # Setup
194
  temp_dir = tempfile.mkdtemp()
195
  downloader = VideoDownloader(temp_dir)
 
207
  shutil.rmtree(temp_dir, ignore_errors=True)
208
 
209
 
210
+ @patch.object(VideoDownloader, '_configure_cookies')
211
+ def test_is_not_direct_video_link_html(mock_configure_cookies):
212
  # Setup
213
  temp_dir = tempfile.mkdtemp()
214
  downloader = VideoDownloader(temp_dir)
 
226
  shutil.rmtree(temp_dir, ignore_errors=True)
227
 
228
 
229
+ @patch.object(VideoDownloader, '_configure_cookies')
230
  @patch('video_downloader.requests.get')
231
+ def test_download_direct_video_success(mock_get, mock_configure_cookies):
232
  # Setup
233
  temp_dir = tempfile.mkdtemp()
234
  downloader = VideoDownloader(temp_dir)
 
256
  shutil.rmtree(temp_dir, ignore_errors=True)
257
 
258
 
259
+ @patch.object(VideoDownloader, '_configure_cookies')
260
  @patch('video_downloader.requests.get')
261
+ def test_download_direct_video_no_filename(mock_get, mock_configure_cookies):
262
  # Setup
263
  temp_dir = tempfile.mkdtemp()
264
  downloader = VideoDownloader(temp_dir)
 
283
  shutil.rmtree(temp_dir, ignore_errors=True)
284
 
285
 
286
+ @patch.object(VideoDownloader, '_configure_cookies')
287
  @patch('video_downloader.requests.get')
288
+ def test_download_direct_video_request_error(mock_get, mock_configure_cookies):
289
  # Setup
290
  temp_dir = tempfile.mkdtemp()
291
  downloader = VideoDownloader(temp_dir)
 
305
  shutil.rmtree(temp_dir, ignore_errors=True)
306
 
307
 
308
+ @patch.object(VideoDownloader, '_configure_cookies')
309
  @patch('video_downloader.yt_dlp.YoutubeDL')
310
+ def test_download_with_ytdlp_success(mock_ytdl_class, mock_configure_cookies):
311
  # Setup
312
  temp_dir = tempfile.mkdtemp()
313
  downloader = VideoDownloader(temp_dir)
 
336
  # Teardown
337
  shutil.rmtree(temp_dir, ignore_errors=True)
338
 
339
+ @patch.object(VideoDownloader, '_configure_cookies')
340
  @patch('video_downloader.VideoFileClip')
341
+ def test_extract_audio_success(mock_video_clip, mock_configure_cookies):
342
  # Setup
343
  temp_dir = tempfile.mkdtemp()
344
  downloader = VideoDownloader(temp_dir)
 
367
  shutil.rmtree(temp_dir, ignore_errors=True)
368
 
369
 
370
+ @patch.object(VideoDownloader, '_configure_cookies')
371
  @patch('video_downloader.VideoFileClip')
372
+ def test_extract_audio_mp3_format(mock_video_clip, mock_configure_cookies):
373
  # Setup
374
  temp_dir = tempfile.mkdtemp()
375
  downloader = VideoDownloader(temp_dir)
 
401
  @patch.object(VideoDownloader, 'download_direct_video')
402
  @patch.object(VideoDownloader, 'is_direct_video_link')
403
  @patch.object(VideoDownloader, 'validate_url')
404
+ @patch.object(VideoDownloader, '_configure_cookies')
405
+ def test_process_url_direct_video_with_audio(mock_configure_cookies, mock_validate, mock_is_direct, mock_download, mock_extract):
406
  # Setup
407
  temp_dir = tempfile.mkdtemp()
408
  downloader = VideoDownloader(temp_dir)
 
433
 
434
 
435
  @patch.object(VideoDownloader, 'validate_url')
436
+ @patch.object(VideoDownloader, '_configure_cookies')
437
+ def test_process_url_invalid_url(mock_configure_cookies, mock_validate):
438
  # Setup
439
  temp_dir = tempfile.mkdtemp()
440
  downloader = VideoDownloader(temp_dir)
video_downloader.py CHANGED
@@ -8,8 +8,6 @@ from pathlib import Path
8
  from moviepy.editor import VideoFileClip
9
  from urllib.parse import urlparse
10
  from logger import get_logger
11
- import json
12
- import browser_cookie3
13
  import platform
14
 
15
  # Get logger for this module
@@ -21,85 +19,52 @@ class VideoDownloader:
21
  self.output_dir = Path(output_dir)
22
  self.output_dir.mkdir(exist_ok=True)
23
 
24
- # Get cookies from browser
25
- self.cookies = self._get_browser_cookies()
26
-
27
- # yt-dlp options
28
  self.ydl_opts = {
29
  'outtmpl': str(self.output_dir / '%(title)s.%(ext)s'),
30
  'format': 'best',
31
  'noplaylist': True,
32
- 'cookiefile': self._save_cookies_to_file() if self.cookies else None,
33
  'quiet': True,
34
  'no_warnings': True,
35
  'extract_flat': False,
36
  }
 
 
 
37
 
38
- def _get_browser_cookies(self):
39
- """Get cookies from the user's browser"""
40
  try:
41
  # Try different browsers in order of preference
42
- browsers = [
43
- ('chrome', browser_cookie3.chrome),
44
- ('firefox', browser_cookie3.firefox),
45
- ('edge', browser_cookie3.edge),
46
- ('safari', browser_cookie3.safari),
47
- ('opera', browser_cookie3.opera),
48
- ('brave', browser_cookie3.brave),
49
- ]
50
-
51
- for browser_name, browser_func in browsers:
52
  try:
53
- cookies = browser_func(domain_name='.youtube.com')
54
- if cookies:
55
- logger.info(f"Successfully loaded cookies from {browser_name}")
56
- return cookies
 
 
 
 
 
 
 
 
 
 
 
 
57
  except Exception as e:
58
- logger.debug(f"Failed to get cookies from {browser_name}: {e}")
59
  continue
60
 
61
- logger.warning("No browser cookies found")
62
- return None
63
-
64
- except Exception as e:
65
- logger.error(f"Error getting browser cookies: {e}")
66
- return None
67
-
68
- def _save_cookies_to_file(self):
69
- """Save cookies to a temporary file in Netscape format"""
70
- if not self.cookies:
71
- return None
72
-
73
- try:
74
- # Create a temporary file for cookies
75
- cookie_file = tempfile.NamedTemporaryFile(delete=False, mode='w', suffix='.txt')
76
-
77
- # Write Netscape format cookie file
78
- cookie_file.write("# Netscape HTTP Cookie File\n")
79
- cookie_file.write("# https://curl.haxx.se/rfc/cookie_spec.html\n")
80
- cookie_file.write("# This file was generated by yt-dlp\n\n")
81
-
82
- for cookie in self.cookies:
83
- if cookie.domain.endswith('.youtube.com'):
84
- secure = "TRUE" if cookie.secure else "FALSE"
85
- http_only = "TRUE" if cookie.has_nonstandard_attr('HttpOnly') else "FALSE"
86
- cookie_file.write(f"{cookie.domain}\tTRUE\t{cookie.path}\t{secure}\t{cookie.expires}\t{cookie.name}\t{cookie.value}\n")
87
-
88
- cookie_file.close()
89
- logger.info(f"Saved cookies to temporary file: {cookie_file.name}")
90
- return cookie_file.name
91
 
92
  except Exception as e:
93
- logger.error(f"Error saving cookies to file: {e}")
94
- return None
95
-
96
- def _cleanup_cookie_file(self):
97
- """Clean up the temporary cookie file"""
98
- if hasattr(self, 'ydl_opts') and self.ydl_opts.get('cookiefile'):
99
- try:
100
- os.unlink(self.ydl_opts['cookiefile'])
101
- except Exception as e:
102
- logger.error(f"Error cleaning up cookie file: {e}")
103
 
104
  def validate_url(self, url):
105
  # Handle empty or None URLs
@@ -184,13 +149,14 @@ class VideoDownloader:
184
  logger.error("HTTP 403 Forbidden error detected!")
185
  print("YOUTUBE ACCESS ERROR (403 Forbidden)")
186
  print("This error typically occurs due to:")
187
- print("1. Browser cookies interfering with yt-dlp")
188
- print("2. Outdated yt-dlp version")
 
189
  print("\n SOLUTIONS:")
190
- print("1. Clear your browser cookies for YouTube")
191
  print("2. Update yt-dlp: pip install --upgrade yt-dlp")
192
- print("3. Try again in a few minutes")
193
- print("4. Use a different video URL for testing")
194
  raise Exception("YouTube access blocked (403 Forbidden). See solutions above.")
195
  else:
196
  logger.error(f"yt-dlp download error: {e}")
 
8
  from moviepy.editor import VideoFileClip
9
  from urllib.parse import urlparse
10
  from logger import get_logger
 
 
11
  import platform
12
 
13
  # Get logger for this module
 
19
  self.output_dir = Path(output_dir)
20
  self.output_dir.mkdir(exist_ok=True)
21
 
22
+ # yt-dlp options with automatic cookie support
 
 
 
23
  self.ydl_opts = {
24
  'outtmpl': str(self.output_dir / '%(title)s.%(ext)s'),
25
  'format': 'best',
26
  'noplaylist': True,
 
27
  'quiet': True,
28
  'no_warnings': True,
29
  'extract_flat': False,
30
  }
31
+
32
+ # Add automatic browser cookie support
33
+ self._configure_cookies()
34
 
35
+ def _configure_cookies(self):
36
+ """Configure automatic cookie extraction from browser"""
37
  try:
38
  # Try different browsers in order of preference
39
+ browsers_to_try = ['chrome', 'firefox', 'edge', 'safari', 'opera', 'brave']
40
+
41
+ for browser in browsers_to_try:
 
 
 
 
 
 
 
42
  try:
43
+ # Test if browser cookies are accessible
44
+ test_opts = self.ydl_opts.copy()
45
+ test_opts['cookies_from_browser'] = (browser, None, None, None)
46
+ test_opts['quiet'] = True
47
+ test_opts['no_warnings'] = True
48
+
49
+ # Try to extract info from a simple YouTube URL to test cookies
50
+ with yt_dlp.YoutubeDL(test_opts) as ydl:
51
+ # Just test if we can access cookies, don't actually download
52
+ logger.debug(f"Testing {browser} cookies...")
53
+
54
+ # If no exception, browser cookies are accessible
55
+ self.ydl_opts['cookies_from_browser'] = (browser, None, None, None)
56
+ logger.info(f"Successfully configured automatic cookies from {browser}")
57
+ return
58
+
59
  except Exception as e:
60
+ logger.debug(f"Failed to configure cookies from {browser}: {e}")
61
  continue
62
 
63
+ logger.warning("No accessible browser cookies found. Proceeding without cookies.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
 
65
  except Exception as e:
66
+ logger.error(f"Error configuring browser cookies: {e}")
67
+ logger.info("Proceeding without automatic cookies")
 
 
 
 
 
 
 
 
68
 
69
  def validate_url(self, url):
70
  # Handle empty or None URLs
 
149
  logger.error("HTTP 403 Forbidden error detected!")
150
  print("YOUTUBE ACCESS ERROR (403 Forbidden)")
151
  print("This error typically occurs due to:")
152
+ print("1. Rate limiting or IP blocking")
153
+ print("2. Need for updated cookies")
154
+ print("3. Outdated yt-dlp version")
155
  print("\n SOLUTIONS:")
156
+ print("1. Try again in a few minutes")
157
  print("2. Update yt-dlp: pip install --upgrade yt-dlp")
158
+ print("3. Use a different video URL for testing")
159
+ print("4. Make sure your browser is logged into YouTube")
160
  raise Exception("YouTube access blocked (403 Forbidden). See solutions above.")
161
  else:
162
  logger.error(f"yt-dlp download error: {e}")