youtube-rb 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +149 -429
- data/lib/youtube-rb/client.rb +5 -2
- data/lib/youtube-rb/downloader.rb +64 -456
- data/lib/youtube-rb/options.rb +3 -7
- data/lib/youtube-rb/version.rb +1 -1
- data/lib/youtube-rb.rb +0 -2
- data/spec/client_spec.rb +9 -16
- data/spec/download_with_mocks_spec.rb +8 -18
- data/spec/downloader_spec.rb +21 -114
- data/spec/fixtures/rickroll_full_info.json +785 -23
- data/spec/integration/ytdlp_integration_spec.rb +1 -12
- data/spec/real_download_spec.rb +1 -30
- data/spec/support/mocking_helper.rb +103 -8
- data/spec/youtube_rb_spec.rb +2 -18
- data/youtube-rb.gemspec +2 -2
- metadata +4 -5
- data/lib/youtube-rb/extractor.rb +0 -425
data/lib/youtube-rb.rb
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
require_relative "youtube-rb/version"
|
|
2
2
|
require_relative "youtube-rb/options"
|
|
3
3
|
require_relative "youtube-rb/video_info"
|
|
4
|
-
require_relative "youtube-rb/extractor"
|
|
5
4
|
require_relative "youtube-rb/ytdlp_wrapper"
|
|
6
5
|
require_relative "youtube-rb/downloader"
|
|
7
6
|
require_relative "youtube-rb/client"
|
|
8
7
|
|
|
9
8
|
module YoutubeRb
|
|
10
9
|
class Error < StandardError; end
|
|
11
|
-
class ExtractionError < Error; end
|
|
12
10
|
class DownloadError < Error; end
|
|
13
11
|
class ValidationError < Error; end
|
|
14
12
|
|
data/spec/client_spec.rb
CHANGED
|
@@ -4,7 +4,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
4
4
|
let(:client) { described_class.new(output_path: @test_output_dir) }
|
|
5
5
|
|
|
6
6
|
before do
|
|
7
|
-
|
|
7
|
+
mock_ytdlp(video_data)
|
|
8
8
|
end
|
|
9
9
|
|
|
10
10
|
describe '#initialize' do
|
|
@@ -62,11 +62,11 @@ RSpec.describe YoutubeRb::Client do
|
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
it 'raises error for invalid URL' do
|
|
65
|
-
|
|
65
|
+
mock_ytdlp_error
|
|
66
66
|
|
|
67
67
|
expect {
|
|
68
68
|
client.info('https://www.youtube.com/watch?v=invalid')
|
|
69
|
-
}.to raise_error(
|
|
69
|
+
}.to raise_error(StandardError)
|
|
70
70
|
end
|
|
71
71
|
end
|
|
72
72
|
|
|
@@ -96,7 +96,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
96
96
|
custom_dir = File.join(@test_output_dir, 'nested', 'path')
|
|
97
97
|
custom_client = described_class.new(output_path: custom_dir)
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
mock_ytdlp(video_data)
|
|
100
100
|
stub_video_download(video_url)
|
|
101
101
|
|
|
102
102
|
output_file = custom_client.download(test_url)
|
|
@@ -275,7 +275,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
275
275
|
subtitle_format: 'vtt'
|
|
276
276
|
)
|
|
277
277
|
|
|
278
|
-
|
|
278
|
+
mock_ytdlp(video_data)
|
|
279
279
|
video_data['subtitles'].each do |lang, subs|
|
|
280
280
|
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
281
281
|
end
|
|
@@ -366,13 +366,6 @@ RSpec.describe YoutubeRb::Client do
|
|
|
366
366
|
expect(output_file).to be_a(String)
|
|
367
367
|
end
|
|
368
368
|
|
|
369
|
-
it 'raises error if ffmpeg not available' do
|
|
370
|
-
allow_any_instance_of(YoutubeRb::Downloader).to receive(:ffmpeg_available?).and_return(false)
|
|
371
|
-
|
|
372
|
-
expect {
|
|
373
|
-
client.extract_audio(test_url)
|
|
374
|
-
}.to raise_error(YoutubeRb::Downloader::DownloadError, /FFmpeg is required/)
|
|
375
|
-
end
|
|
376
369
|
end
|
|
377
370
|
|
|
378
371
|
describe '#valid_url?' do
|
|
@@ -381,7 +374,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
381
374
|
end
|
|
382
375
|
|
|
383
376
|
it 'returns false for invalid URL' do
|
|
384
|
-
|
|
377
|
+
mock_ytdlp_error
|
|
385
378
|
|
|
386
379
|
expect(client.valid_url?('https://www.youtube.com/watch?v=invalid')).to be false
|
|
387
380
|
end
|
|
@@ -395,7 +388,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
395
388
|
end
|
|
396
389
|
|
|
397
390
|
it 'returns false for non-YouTube URL' do
|
|
398
|
-
|
|
391
|
+
mock_ytdlp_error('Not a YouTube URL')
|
|
399
392
|
|
|
400
393
|
expect(client.valid_url?('https://example.com')).to be false
|
|
401
394
|
end
|
|
@@ -416,7 +409,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
416
409
|
no_formats_data = video_data.dup
|
|
417
410
|
no_formats_data['formats'] = []
|
|
418
411
|
|
|
419
|
-
|
|
412
|
+
mock_ytdlp(no_formats_data)
|
|
420
413
|
|
|
421
414
|
formats = client.formats('https://www.youtube.com/watch?v=noformats')
|
|
422
415
|
expect(formats).to eq([])
|
|
@@ -437,7 +430,7 @@ RSpec.describe YoutubeRb::Client do
|
|
|
437
430
|
no_subs_data = video_data.dup
|
|
438
431
|
no_subs_data['subtitles'] = {}
|
|
439
432
|
|
|
440
|
-
|
|
433
|
+
mock_ytdlp(no_subs_data)
|
|
441
434
|
|
|
442
435
|
subtitles = client.subtitles('https://www.youtube.com/watch?v=nosubs')
|
|
443
436
|
expect(subtitles).to eq({})
|
|
@@ -13,10 +13,9 @@ RSpec.describe "Download with mocks" do
|
|
|
13
13
|
before(:each) do
|
|
14
14
|
FileUtils.mkdir_p(output_dir)
|
|
15
15
|
|
|
16
|
-
# Mock
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
end
|
|
16
|
+
# Mock YtdlpWrapper to return real data
|
|
17
|
+
allow(YoutubeRb::YtdlpWrapper).to receive(:available?).and_return(true)
|
|
18
|
+
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:extract_info).and_return(rickroll_info)
|
|
20
19
|
end
|
|
21
20
|
|
|
22
21
|
after(:each) do
|
|
@@ -70,8 +69,7 @@ RSpec.describe "Download with mocks" do
|
|
|
70
69
|
end
|
|
71
70
|
|
|
72
71
|
client = YoutubeRb::Client.new(
|
|
73
|
-
output_path: output_dir
|
|
74
|
-
use_ytdlp: true
|
|
72
|
+
output_path: output_dir
|
|
75
73
|
)
|
|
76
74
|
|
|
77
75
|
output_file = client.download(rickroll_url)
|
|
@@ -101,8 +99,7 @@ RSpec.describe "Download with mocks" do
|
|
|
101
99
|
end
|
|
102
100
|
|
|
103
101
|
client = YoutubeRb::Client.new(
|
|
104
|
-
output_path: output_dir
|
|
105
|
-
use_ytdlp: true
|
|
102
|
+
output_path: output_dir
|
|
106
103
|
)
|
|
107
104
|
|
|
108
105
|
output_file = client.download_segment(rickroll_url, start_time, end_time)
|
|
@@ -146,7 +143,7 @@ RSpec.describe "Download with mocks" do
|
|
|
146
143
|
# It's better tested in real_download_spec.rb with actual downloads
|
|
147
144
|
end
|
|
148
145
|
|
|
149
|
-
it "
|
|
146
|
+
it "always uses yt-dlp for downloads" do
|
|
150
147
|
tried_ytdlp = false
|
|
151
148
|
|
|
152
149
|
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:download) do
|
|
@@ -157,8 +154,7 @@ RSpec.describe "Download with mocks" do
|
|
|
157
154
|
end
|
|
158
155
|
|
|
159
156
|
client = YoutubeRb::Client.new(
|
|
160
|
-
output_path: output_dir
|
|
161
|
-
use_ytdlp: true
|
|
157
|
+
output_path: output_dir
|
|
162
158
|
)
|
|
163
159
|
|
|
164
160
|
client.download(rickroll_url)
|
|
@@ -173,14 +169,8 @@ RSpec.describe "Download with mocks" do
|
|
|
173
169
|
raise YoutubeRb::YtdlpWrapper::YtdlpError, "Video unavailable"
|
|
174
170
|
end
|
|
175
171
|
|
|
176
|
-
allow_any_instance_of(YoutubeRb::Extractor).to receive(:extract_info) do
|
|
177
|
-
raise YoutubeRb::Extractor::ExtractionError, "Failed to extract"
|
|
178
|
-
end
|
|
179
|
-
|
|
180
172
|
client = YoutubeRb::Client.new(
|
|
181
|
-
output_path: output_dir
|
|
182
|
-
use_ytdlp: true,
|
|
183
|
-
ytdlp_fallback: false
|
|
173
|
+
output_path: output_dir
|
|
184
174
|
)
|
|
185
175
|
|
|
186
176
|
expect {
|
data/spec/downloader_spec.rb
CHANGED
|
@@ -5,7 +5,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
5
5
|
let(:downloader) { described_class.new(test_url, options) }
|
|
6
6
|
|
|
7
7
|
before do
|
|
8
|
-
|
|
8
|
+
mock_ytdlp(video_data)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
11
|
describe '#initialize' do
|
|
@@ -49,11 +49,9 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
49
49
|
expect(info1).to equal(info2)
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
it 'calls
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
expect_any_instance_of(YoutubeRb::Extractor)
|
|
56
|
-
.to receive(:extract_info).once.and_return(video_info)
|
|
52
|
+
it 'calls ytdlp only once' do
|
|
53
|
+
expect_any_instance_of(YoutubeRb::YtdlpWrapper)
|
|
54
|
+
.to receive(:extract_info).once.and_return(video_data)
|
|
57
55
|
|
|
58
56
|
downloader.info
|
|
59
57
|
downloader.info
|
|
@@ -91,7 +89,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
91
89
|
)
|
|
92
90
|
dl = described_class.new(test_url, custom_options)
|
|
93
91
|
|
|
94
|
-
|
|
92
|
+
mock_ytdlp(video_data)
|
|
95
93
|
stub_video_download(video_url)
|
|
96
94
|
|
|
97
95
|
output_file = dl.download
|
|
@@ -103,7 +101,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
103
101
|
bad_title_data = video_data.dup
|
|
104
102
|
bad_title_data['title'] = 'Test/Video:With*Bad?Chars'
|
|
105
103
|
|
|
106
|
-
|
|
104
|
+
mock_ytdlp(bad_title_data)
|
|
107
105
|
stub_video_download(video_url)
|
|
108
106
|
|
|
109
107
|
dl = described_class.new(
|
|
@@ -134,7 +132,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
134
132
|
)
|
|
135
133
|
dl = described_class.new(test_url, subtitle_options)
|
|
136
134
|
|
|
137
|
-
|
|
135
|
+
mock_ytdlp(video_data)
|
|
138
136
|
stub_video_download(video_url)
|
|
139
137
|
video_data['subtitles'].each do |lang, subs|
|
|
140
138
|
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
@@ -149,7 +147,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
149
147
|
it 'skips subtitles when disabled' do
|
|
150
148
|
dl = described_class.new(test_url, options)
|
|
151
149
|
|
|
152
|
-
|
|
150
|
+
mock_ytdlp(video_data)
|
|
153
151
|
stub_video_download(video_url)
|
|
154
152
|
|
|
155
153
|
dl.download
|
|
@@ -171,7 +169,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
171
169
|
)
|
|
172
170
|
dl = described_class.new(test_url, metadata_options)
|
|
173
171
|
|
|
174
|
-
|
|
172
|
+
mock_ytdlp(video_data)
|
|
175
173
|
stub_video_download(video_url)
|
|
176
174
|
stub_thumbnail_download(video_data['thumbnail'])
|
|
177
175
|
|
|
@@ -191,7 +189,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
191
189
|
)
|
|
192
190
|
dl = described_class.new(test_url, thumbnail_options)
|
|
193
191
|
|
|
194
|
-
|
|
192
|
+
mock_ytdlp(video_data)
|
|
195
193
|
stub_video_download(video_url)
|
|
196
194
|
stub_thumbnail_download(video_data['thumbnail'])
|
|
197
195
|
|
|
@@ -208,7 +206,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
208
206
|
)
|
|
209
207
|
dl = described_class.new(test_url, desc_options)
|
|
210
208
|
|
|
211
|
-
|
|
209
|
+
mock_ytdlp(video_data)
|
|
212
210
|
stub_video_download(video_url)
|
|
213
211
|
|
|
214
212
|
dl.download
|
|
@@ -236,7 +234,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
236
234
|
)
|
|
237
235
|
dl = described_class.new(test_url, audio_options)
|
|
238
236
|
|
|
239
|
-
|
|
237
|
+
mock_ytdlp(video_data)
|
|
240
238
|
stub_video_download(video_url)
|
|
241
239
|
|
|
242
240
|
output_file = dl.download
|
|
@@ -244,81 +242,16 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
244
242
|
expect(output_file).to be_a(String)
|
|
245
243
|
end
|
|
246
244
|
|
|
247
|
-
it 'raises error if ffmpeg not available' do
|
|
248
|
-
audio_options = YoutubeRb::Options.new(
|
|
249
|
-
output_path: @test_output_dir,
|
|
250
|
-
extract_audio: true
|
|
251
|
-
)
|
|
252
|
-
dl = described_class.new(test_url, audio_options)
|
|
253
|
-
|
|
254
|
-
mock_extractor(video_data)
|
|
255
|
-
stub_video_download(video_url)
|
|
256
|
-
|
|
257
|
-
allow_any_instance_of(described_class).to receive(:ffmpeg_available?).and_return(false)
|
|
258
|
-
|
|
259
|
-
expect {
|
|
260
|
-
dl.download
|
|
261
|
-
}.to raise_error(YoutubeRb::Downloader::DownloadError, /FFmpeg is required/)
|
|
262
|
-
end
|
|
263
245
|
end
|
|
264
246
|
|
|
265
247
|
context 'error handling' do
|
|
266
|
-
it '
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
mock_extractor(no_format_data)
|
|
271
|
-
|
|
272
|
-
dl = described_class.new(
|
|
273
|
-
'https://www.youtube.com/watch?v=noformat',
|
|
274
|
-
options
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
expect {
|
|
278
|
-
dl.download
|
|
279
|
-
}.to raise_error(YoutubeRb::Downloader::DownloadError, /No suitable format/)
|
|
280
|
-
end
|
|
281
|
-
|
|
282
|
-
it 'raises error when format has no URL' do
|
|
283
|
-
bad_format_data = video_data.dup
|
|
284
|
-
bad_format_data['formats'] = [{ 'format_id' => '18', 'ext' => 'mp4' }]
|
|
285
|
-
|
|
286
|
-
mock_extractor(bad_format_data)
|
|
287
|
-
|
|
288
|
-
dl = described_class.new(
|
|
289
|
-
'https://www.youtube.com/watch?v=badformat',
|
|
290
|
-
options
|
|
291
|
-
)
|
|
292
|
-
|
|
293
|
-
expect {
|
|
294
|
-
dl.download
|
|
295
|
-
}.to raise_error(YoutubeRb::Downloader::DownloadError, /No URL found/)
|
|
296
|
-
end
|
|
297
|
-
|
|
298
|
-
it 'handles HTTP download errors' do
|
|
299
|
-
# Disable yt-dlp fallback for this test
|
|
300
|
-
dl = YoutubeRb::Downloader.new(
|
|
301
|
-
test_url,
|
|
302
|
-
output_path: @test_output_dir,
|
|
303
|
-
use_ytdlp: false,
|
|
304
|
-
ytdlp_fallback: false
|
|
305
|
-
)
|
|
306
|
-
|
|
307
|
-
mock_extractor(video_data)
|
|
308
|
-
stub_request(:get, video_url).to_return(status: 403, body: 'Forbidden')
|
|
309
|
-
|
|
310
|
-
expect {
|
|
311
|
-
dl.download
|
|
312
|
-
}.to raise_error(YoutubeRb::Downloader::DownloadError, /HTTP download failed/)
|
|
313
|
-
end
|
|
314
|
-
|
|
315
|
-
it 'handles network errors' do
|
|
316
|
-
mock_extractor(video_data)
|
|
317
|
-
stub_request(:get, video_url).to_raise(Faraday::ConnectionFailed)
|
|
248
|
+
it 'handles download errors' do
|
|
249
|
+
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:download)
|
|
250
|
+
.and_raise(YoutubeRb::YtdlpWrapper::YtdlpError, 'Download failed')
|
|
318
251
|
|
|
319
252
|
expect {
|
|
320
253
|
downloader.download
|
|
321
|
-
}.to raise_error(YoutubeRb::Downloader::DownloadError, /
|
|
254
|
+
}.to raise_error(YoutubeRb::Downloader::DownloadError, /Download failed/)
|
|
322
255
|
end
|
|
323
256
|
end
|
|
324
257
|
end
|
|
@@ -556,7 +489,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
556
489
|
)
|
|
557
490
|
dl = described_class.new(test_url, cache_options)
|
|
558
491
|
|
|
559
|
-
|
|
492
|
+
mock_ytdlp(video_data)
|
|
560
493
|
|
|
561
494
|
# Mock yt-dlp download
|
|
562
495
|
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:download) do |_, url, output_path|
|
|
@@ -583,7 +516,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
583
516
|
)
|
|
584
517
|
dl = described_class.new(test_url, no_cache_options)
|
|
585
518
|
|
|
586
|
-
|
|
519
|
+
mock_ytdlp(video_data)
|
|
587
520
|
|
|
588
521
|
# Mock yt-dlp download
|
|
589
522
|
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:download) do |_, url, output_path|
|
|
@@ -633,7 +566,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
633
566
|
)
|
|
634
567
|
dl = described_class.new(test_url, subtitle_options)
|
|
635
568
|
|
|
636
|
-
|
|
569
|
+
mock_ytdlp(video_data)
|
|
637
570
|
video_data['subtitles'].each do |lang, subs|
|
|
638
571
|
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
639
572
|
end
|
|
@@ -649,7 +582,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
649
582
|
subtitle_options = YoutubeRb::Options.new(output_path: custom_dir)
|
|
650
583
|
dl = described_class.new(test_url, subtitle_options)
|
|
651
584
|
|
|
652
|
-
|
|
585
|
+
mock_ytdlp(video_data)
|
|
653
586
|
video_data['subtitles'].each do |lang, subs|
|
|
654
587
|
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
655
588
|
end
|
|
@@ -701,32 +634,6 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
701
634
|
end
|
|
702
635
|
end
|
|
703
636
|
|
|
704
|
-
describe '#audio_codec_for_format' do
|
|
705
|
-
it 'returns correct codec for mp3' do
|
|
706
|
-
dl = downloader
|
|
707
|
-
expect(dl.send(:audio_codec_for_format, 'mp3')).to eq('libmp3lame')
|
|
708
|
-
end
|
|
709
|
-
|
|
710
|
-
it 'returns correct codec for aac' do
|
|
711
|
-
dl = downloader
|
|
712
|
-
expect(dl.send(:audio_codec_for_format, 'aac')).to eq('aac')
|
|
713
|
-
end
|
|
714
|
-
|
|
715
|
-
it 'returns correct codec for opus' do
|
|
716
|
-
dl = downloader
|
|
717
|
-
expect(dl.send(:audio_codec_for_format, 'opus')).to eq('libopus')
|
|
718
|
-
end
|
|
719
|
-
|
|
720
|
-
it 'returns correct codec for flac' do
|
|
721
|
-
dl = downloader
|
|
722
|
-
expect(dl.send(:audio_codec_for_format, 'flac')).to eq('flac')
|
|
723
|
-
end
|
|
724
|
-
|
|
725
|
-
it 'returns copy for unknown format' do
|
|
726
|
-
dl = downloader
|
|
727
|
-
expect(dl.send(:audio_codec_for_format, 'unknown')).to eq('copy')
|
|
728
|
-
end
|
|
729
|
-
end
|
|
730
637
|
|
|
731
638
|
describe '#valid_segment_duration?' do
|
|
732
639
|
it 'returns true for 10 seconds (default minimum)' do
|
|
@@ -762,7 +669,7 @@ RSpec.describe YoutubeRb::Downloader do
|
|
|
762
669
|
)
|
|
763
670
|
dl = described_class.new(test_url, custom_options)
|
|
764
671
|
|
|
765
|
-
|
|
672
|
+
mock_ytdlp(video_data)
|
|
766
673
|
|
|
767
674
|
expect(dl.send(:valid_segment_duration?, 5)).to be true # custom minimum
|
|
768
675
|
expect(dl.send(:valid_segment_duration?, 120)).to be true # custom maximum
|