youtube-rb 0.2.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +703 -0
- data/Rakefile +6 -0
- data/bin/console +10 -0
- data/bin/setup +8 -0
- data/lib/youtube-rb/client.rb +160 -0
- data/lib/youtube-rb/downloader.rb +632 -0
- data/lib/youtube-rb/extractor.rb +425 -0
- data/lib/youtube-rb/options.rb +186 -0
- data/lib/youtube-rb/version.rb +3 -0
- data/lib/youtube-rb/video_info.rb +179 -0
- data/lib/youtube-rb/ytdlp_wrapper.rb +269 -0
- data/lib/youtube-rb.rb +69 -0
- data/spec/client_spec.rb +514 -0
- data/spec/download_with_mocks_spec.rb +216 -0
- data/spec/downloader_spec.rb +774 -0
- data/spec/fixtures/first_video_info.json +19 -0
- data/spec/fixtures/rickroll_full_info.json +73 -0
- data/spec/fixtures/rickroll_info.json +73 -0
- data/spec/fixtures/rickroll_segment_info.json +9 -0
- data/spec/integration/ytdlp_integration_spec.rb +109 -0
- data/spec/real_download_spec.rb +175 -0
- data/spec/spec_helper.rb +31 -0
- data/spec/support/fixtures_helper.rb +109 -0
- data/spec/support/mocking_helper.rb +21 -0
- data/spec/support/webmock_helper.rb +132 -0
- data/spec/youtube_rb_spec.rb +200 -0
- data/spec/ytdlp_wrapper_spec.rb +178 -0
- data/youtube-rb.gemspec +39 -0
- metadata +229 -0
data/spec/client_spec.rb
ADDED
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
RSpec.describe YoutubeRb::Client do
|
|
2
|
+
let(:test_url) { 'https://www.youtube.com/watch?v=test123abc' }
|
|
3
|
+
let(:video_data) { sample_video_data }
|
|
4
|
+
let(:client) { described_class.new(output_path: @test_output_dir) }
|
|
5
|
+
|
|
6
|
+
before do
|
|
7
|
+
mock_extractor(video_data)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
describe '#initialize' do
|
|
11
|
+
it 'creates client with default options' do
|
|
12
|
+
client = described_class.new
|
|
13
|
+
expect(client).to be_a(described_class)
|
|
14
|
+
expect(client.options).to be_a(YoutubeRb::Options)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
it 'creates client with custom options' do
|
|
18
|
+
client = described_class.new(output_path: '/tmp/test', format: '720p')
|
|
19
|
+
expect(client.options.output_path).to eq('/tmp/test')
|
|
20
|
+
expect(client.options.format).to eq('720p')
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
it 'accepts multiple options' do
|
|
24
|
+
client = described_class.new(
|
|
25
|
+
output_path: './custom',
|
|
26
|
+
write_subtitles: true,
|
|
27
|
+
subtitle_langs: ['en', 'es'],
|
|
28
|
+
audio_format: 'aac'
|
|
29
|
+
)
|
|
30
|
+
expect(client.options.output_path).to eq('./custom')
|
|
31
|
+
expect(client.options.write_subtitles).to eq(true)
|
|
32
|
+
expect(client.options.subtitle_langs).to eq(['en', 'es'])
|
|
33
|
+
expect(client.options.audio_format).to eq('aac')
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
describe '#info' do
|
|
38
|
+
it 'returns video information' do
|
|
39
|
+
info = client.info(test_url)
|
|
40
|
+
|
|
41
|
+
expect(info).to be_a(YoutubeRb::VideoInfo)
|
|
42
|
+
expect(info.id).to eq('test123abc')
|
|
43
|
+
expect(info.title).to eq('Test Video Title')
|
|
44
|
+
expect(info.uploader).to eq('Test Channel')
|
|
45
|
+
expect(info.duration).to eq(180)
|
|
46
|
+
expect(info.view_count).to eq(1000)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
it 'returns formats' do
|
|
50
|
+
info = client.info(test_url)
|
|
51
|
+
|
|
52
|
+
expect(info.formats).to be_an(Array)
|
|
53
|
+
expect(info.formats.size).to eq(2)
|
|
54
|
+
expect(info.formats.first[:format_id]).to eq('18')
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
it 'returns subtitles' do
|
|
58
|
+
info = client.info(test_url)
|
|
59
|
+
|
|
60
|
+
expect(info.subtitles).to be_a(Hash)
|
|
61
|
+
expect(info.subtitles.keys).to include('en', 'es')
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
it 'raises error for invalid URL' do
|
|
65
|
+
mock_extractor_error
|
|
66
|
+
|
|
67
|
+
expect {
|
|
68
|
+
client.info('https://www.youtube.com/watch?v=invalid')
|
|
69
|
+
}.to raise_error(YoutubeRb::Extractor::ExtractionError)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe '#download' do
|
|
74
|
+
let(:video_url) { video_data['formats'].last['url'] } # Use 720p (highest quality)
|
|
75
|
+
|
|
76
|
+
before do
|
|
77
|
+
stub_video_download(video_url)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
it 'downloads video file' do
|
|
81
|
+
output_file = client.download(test_url)
|
|
82
|
+
|
|
83
|
+
expect(output_file).to be_a(String)
|
|
84
|
+
expect(File.exist?(output_file)).to be true
|
|
85
|
+
expect(File.size(output_file)).to be > 0
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
it 'accepts additional options' do
|
|
89
|
+
output_file = client.download(test_url, output_template: 'custom-%(id)s.%(ext)s')
|
|
90
|
+
|
|
91
|
+
expect(output_file).to include('custom-test123abc')
|
|
92
|
+
expect(File.exist?(output_file)).to be true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it 'creates output directory if needed' do
|
|
96
|
+
custom_dir = File.join(@test_output_dir, 'nested', 'path')
|
|
97
|
+
custom_client = described_class.new(output_path: custom_dir)
|
|
98
|
+
|
|
99
|
+
mock_extractor(video_data)
|
|
100
|
+
stub_video_download(video_url)
|
|
101
|
+
|
|
102
|
+
output_file = custom_client.download(test_url)
|
|
103
|
+
|
|
104
|
+
expect(Dir.exist?(custom_dir)).to be true
|
|
105
|
+
expect(File.exist?(output_file)).to be true
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
describe '#download_segment' do
|
|
110
|
+
before do
|
|
111
|
+
allow(YoutubeRb::YtdlpWrapper).to receive(:available?).and_return(true)
|
|
112
|
+
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:download_segment) do |_, url, start_time, end_time, output_file|
|
|
113
|
+
output_file || File.join(@test_output_dir, "segment-#{start_time}-#{end_time}.mp4")
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
it 'downloads video segment using yt-dlp' do
|
|
118
|
+
output_file = client.download_segment(test_url, 10, 30)
|
|
119
|
+
|
|
120
|
+
expect(output_file).to be_a(String)
|
|
121
|
+
expect(output_file).to include('segment')
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
it 'accepts custom output file' do
|
|
125
|
+
custom_file = File.join(@test_output_dir, 'my-segment.mp4')
|
|
126
|
+
output_file = client.download_segment(test_url, 10, 30, output_file: custom_file)
|
|
127
|
+
|
|
128
|
+
expect(output_file).to eq(custom_file)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
it 'validates segment duration (minimum 10 seconds)' do
|
|
132
|
+
expect {
|
|
133
|
+
client.download_segment(test_url, 0, 5)
|
|
134
|
+
}.to raise_error(ArgumentError, /between 10 and 60 seconds/)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
it 'validates segment duration (maximum 60 seconds)' do
|
|
138
|
+
expect {
|
|
139
|
+
client.download_segment(test_url, 0, 70)
|
|
140
|
+
}.to raise_error(ArgumentError, /between 10 and 60 seconds/)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
it 'validates start time less than end time' do
|
|
144
|
+
expect {
|
|
145
|
+
client.download_segment(test_url, 30, 10)
|
|
146
|
+
}.to raise_error(ArgumentError, /Start time must be less than end time/)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
it 'raises error if yt-dlp not available' do
|
|
150
|
+
allow(YoutubeRb::YtdlpWrapper).to receive(:available?).and_return(false)
|
|
151
|
+
|
|
152
|
+
expect {
|
|
153
|
+
client.download_segment(test_url, 10, 30)
|
|
154
|
+
}.to raise_error(YoutubeRb::Downloader::DownloadError, /yt-dlp is required/)
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
describe '#download_segments' do
|
|
159
|
+
before do
|
|
160
|
+
allow(YoutubeRb::YtdlpWrapper).to receive(:available?).and_return(true)
|
|
161
|
+
allow_any_instance_of(YoutubeRb::Downloader).to receive(:ffmpeg_available?).and_return(true)
|
|
162
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
163
|
+
allow_any_instance_of(YoutubeRb::YtdlpWrapper).to receive(:download) do |_, url, output_path|
|
|
164
|
+
FileUtils.touch(output_path)
|
|
165
|
+
output_path
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
it 'downloads multiple video segments using yt-dlp' do
|
|
170
|
+
segments = [
|
|
171
|
+
{ start: 10, end: 30 },
|
|
172
|
+
{ start: 60, end: 90 },
|
|
173
|
+
{ start: 120, end: 150 }
|
|
174
|
+
]
|
|
175
|
+
|
|
176
|
+
output_files = client.download_segments(test_url, segments)
|
|
177
|
+
|
|
178
|
+
expect(output_files).to be_an(Array)
|
|
179
|
+
expect(output_files.size).to eq(3)
|
|
180
|
+
output_files.each do |file|
|
|
181
|
+
expect(file).to be_a(String)
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
it 'accepts custom output files for each segment' do
|
|
186
|
+
segments = [
|
|
187
|
+
{ start: 10, end: 30, output_file: File.join(@test_output_dir, 'seg1.mp4') },
|
|
188
|
+
{ start: 60, end: 90, output_file: File.join(@test_output_dir, 'seg2.mp4') }
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
output_files = client.download_segments(test_url, segments)
|
|
192
|
+
|
|
193
|
+
expect(output_files[0]).to end_with('seg1.mp4')
|
|
194
|
+
expect(output_files[1]).to end_with('seg2.mp4')
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
it 'enables caching by default for batch downloads' do
|
|
198
|
+
segments = [
|
|
199
|
+
{ start: 10, end: 30 },
|
|
200
|
+
{ start: 60, end: 90 }
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
# Verify that cache_full_video is enabled by default for batch downloads
|
|
204
|
+
expect_any_instance_of(YoutubeRb::Downloader)
|
|
205
|
+
.to receive(:download_segments)
|
|
206
|
+
.and_call_original
|
|
207
|
+
|
|
208
|
+
client.download_segments(test_url, segments)
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
it 'accepts additional options' do
|
|
212
|
+
segments = [
|
|
213
|
+
{ start: 10, end: 30 }
|
|
214
|
+
]
|
|
215
|
+
|
|
216
|
+
output_files = client.download_segments(test_url, segments, cache_full_video: false)
|
|
217
|
+
|
|
218
|
+
expect(output_files).to be_an(Array)
|
|
219
|
+
expect(output_files.size).to eq(1)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
it 'validates segments array' do
|
|
223
|
+
expect {
|
|
224
|
+
client.download_segments(test_url, "not an array")
|
|
225
|
+
}.to raise_error(ArgumentError, /segments must be an Array/)
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
it 'validates segment durations' do
|
|
229
|
+
segments = [
|
|
230
|
+
{ start: 10, end: 30 }, # valid
|
|
231
|
+
{ start: 60, end: 65 } # invalid (5 seconds)
|
|
232
|
+
]
|
|
233
|
+
|
|
234
|
+
expect {
|
|
235
|
+
client.download_segments(test_url, segments)
|
|
236
|
+
}.to raise_error(ArgumentError, /between 10 and 60 seconds/)
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
it 'raises error if yt-dlp not available' do
|
|
240
|
+
allow(YoutubeRb::YtdlpWrapper).to receive(:available?).and_return(false)
|
|
241
|
+
|
|
242
|
+
segments = [{ start: 10, end: 30 }]
|
|
243
|
+
|
|
244
|
+
expect {
|
|
245
|
+
client.download_segments(test_url, segments)
|
|
246
|
+
}.to raise_error(YoutubeRb::Downloader::DownloadError, /yt-dlp is required/)
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
describe '#download_subtitles' do
|
|
251
|
+
before do
|
|
252
|
+
video_data['subtitles'].each do |lang, subs|
|
|
253
|
+
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
it 'downloads subtitles for default languages' do
|
|
258
|
+
client.download_subtitles(test_url)
|
|
259
|
+
|
|
260
|
+
# Should download at least one subtitle file
|
|
261
|
+
subtitle_files = Dir.glob(File.join(@test_output_dir, '*.srt'))
|
|
262
|
+
expect(subtitle_files).not_to be_empty
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
it 'downloads subtitles for specific languages' do
|
|
266
|
+
client.download_subtitles(test_url, langs: ['en'])
|
|
267
|
+
|
|
268
|
+
subtitle_files = Dir.glob(File.join(@test_output_dir, '*.en.*'))
|
|
269
|
+
expect(subtitle_files).not_to be_empty
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
it 'accepts additional options' do
|
|
273
|
+
custom_client = described_class.new(
|
|
274
|
+
output_path: @test_output_dir,
|
|
275
|
+
subtitle_format: 'vtt'
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
mock_extractor(video_data)
|
|
279
|
+
video_data['subtitles'].each do |lang, subs|
|
|
280
|
+
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
custom_client.download_subtitles(test_url, langs: ['en', 'es'])
|
|
284
|
+
|
|
285
|
+
# Files should exist
|
|
286
|
+
files = Dir.glob(File.join(@test_output_dir, '*.*'))
|
|
287
|
+
expect(files).not_to be_empty
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
describe '#download_with_metadata' do
|
|
292
|
+
let(:video_url) { video_data['formats'].first['url'] }
|
|
293
|
+
|
|
294
|
+
before do
|
|
295
|
+
stub_video_download(video_url)
|
|
296
|
+
stub_thumbnail_download(video_data['thumbnail'])
|
|
297
|
+
video_data['subtitles'].each do |lang, subs|
|
|
298
|
+
subs.each { |sub| stub_subtitle_download(sub['url']) }
|
|
299
|
+
end
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
it 'downloads video with metadata files' do
|
|
303
|
+
output_file = client.download_with_metadata(test_url)
|
|
304
|
+
|
|
305
|
+
expect(File.exist?(output_file)).to be true
|
|
306
|
+
|
|
307
|
+
# Check for metadata files
|
|
308
|
+
base_name = File.basename(output_file, '.*')
|
|
309
|
+
dir = File.dirname(output_file)
|
|
310
|
+
|
|
311
|
+
# Should create info.json
|
|
312
|
+
json_file = Dir.glob(File.join(dir, '*.info.json')).first
|
|
313
|
+
expect(json_file).not_to be_nil
|
|
314
|
+
|
|
315
|
+
# Should create thumbnail
|
|
316
|
+
thumbnail_files = Dir.glob(File.join(dir, '*{.jpg,.png,.webp}'))
|
|
317
|
+
expect(thumbnail_files).not_to be_empty
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it 'downloads subtitles along with video' do
|
|
321
|
+
output_file = client.download_with_metadata(test_url)
|
|
322
|
+
|
|
323
|
+
# Should create subtitle files
|
|
324
|
+
subtitle_files = Dir.glob(File.join(@test_output_dir, '*.{srt,vtt}'))
|
|
325
|
+
expect(subtitle_files).not_to be_empty
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
describe '#extract_audio' do
|
|
330
|
+
let(:video_url) { video_data['formats'].first['url'] }
|
|
331
|
+
|
|
332
|
+
before do
|
|
333
|
+
stub_video_download(video_url)
|
|
334
|
+
|
|
335
|
+
# Mock ffmpeg for audio extraction
|
|
336
|
+
allow_any_instance_of(YoutubeRb::Downloader).to receive(:ffmpeg_available?).and_return(true)
|
|
337
|
+
allow(Open3).to receive(:capture3).and_return(['', '', double(success?: true)])
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
it 'extracts audio in default format (mp3)' do
|
|
341
|
+
output_file = client.extract_audio(test_url)
|
|
342
|
+
|
|
343
|
+
expect(output_file).to be_a(String)
|
|
344
|
+
end
|
|
345
|
+
|
|
346
|
+
it 'extracts audio in specified format' do
|
|
347
|
+
output_file = client.extract_audio(test_url, format: 'aac')
|
|
348
|
+
|
|
349
|
+
expect(output_file).to be_a(String)
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
it 'extracts audio with specified quality' do
|
|
353
|
+
output_file = client.extract_audio(test_url, format: 'mp3', quality: '320')
|
|
354
|
+
|
|
355
|
+
expect(output_file).to be_a(String)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
it 'accepts additional options' do
|
|
359
|
+
output_file = client.extract_audio(
|
|
360
|
+
test_url,
|
|
361
|
+
format: 'opus',
|
|
362
|
+
quality: '128',
|
|
363
|
+
output_template: 'audio-%(id)s.%(ext)s'
|
|
364
|
+
)
|
|
365
|
+
|
|
366
|
+
expect(output_file).to be_a(String)
|
|
367
|
+
end
|
|
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
|
+
end
|
|
377
|
+
|
|
378
|
+
describe '#valid_url?' do
|
|
379
|
+
it 'returns true for valid YouTube URL' do
|
|
380
|
+
expect(client.valid_url?(test_url)).to be true
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
it 'returns false for invalid URL' do
|
|
384
|
+
mock_extractor_error
|
|
385
|
+
|
|
386
|
+
expect(client.valid_url?('https://www.youtube.com/watch?v=invalid')).to be false
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
it 'returns false for nil URL' do
|
|
390
|
+
expect(client.valid_url?(nil)).to be false
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
it 'returns false for empty URL' do
|
|
394
|
+
expect(client.valid_url?('')).to be false
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
it 'returns false for non-YouTube URL' do
|
|
398
|
+
mock_extractor_error(YoutubeRb::Extractor::ExtractionError, 'Not a YouTube URL')
|
|
399
|
+
|
|
400
|
+
expect(client.valid_url?('https://example.com')).to be false
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
describe '#formats' do
|
|
405
|
+
it 'returns available formats' do
|
|
406
|
+
formats = client.formats(test_url)
|
|
407
|
+
|
|
408
|
+
expect(formats).to be_an(Array)
|
|
409
|
+
expect(formats.size).to eq(2)
|
|
410
|
+
expect(formats.first).to be_a(Hash)
|
|
411
|
+
expect(formats.first[:format_id]).to eq('18')
|
|
412
|
+
expect(formats.first[:height]).to eq(360)
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
it 'returns empty array if no formats available' do
|
|
416
|
+
no_formats_data = video_data.dup
|
|
417
|
+
no_formats_data['formats'] = []
|
|
418
|
+
|
|
419
|
+
mock_extractor(no_formats_data)
|
|
420
|
+
|
|
421
|
+
formats = client.formats('https://www.youtube.com/watch?v=noformats')
|
|
422
|
+
expect(formats).to eq([])
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
describe '#subtitles' do
|
|
427
|
+
it 'returns available subtitles' do
|
|
428
|
+
subtitles = client.subtitles(test_url)
|
|
429
|
+
|
|
430
|
+
expect(subtitles).to be_a(Hash)
|
|
431
|
+
expect(subtitles.keys).to include('en', 'es')
|
|
432
|
+
expect(subtitles['en']).to be_an(Array)
|
|
433
|
+
expect(subtitles['en'].first).to have_key(:url)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
it 'returns empty hash if no subtitles available' do
|
|
437
|
+
no_subs_data = video_data.dup
|
|
438
|
+
no_subs_data['subtitles'] = {}
|
|
439
|
+
|
|
440
|
+
mock_extractor(no_subs_data)
|
|
441
|
+
|
|
442
|
+
subtitles = client.subtitles('https://www.youtube.com/watch?v=nosubs')
|
|
443
|
+
expect(subtitles).to eq({})
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
describe '#configure' do
|
|
448
|
+
it 'updates client options' do
|
|
449
|
+
client.configure(format: '1080p', quality: 'high')
|
|
450
|
+
|
|
451
|
+
expect(client.options.format).to eq('1080p')
|
|
452
|
+
expect(client.options.quality).to eq('high')
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
it 'returns self for chaining' do
|
|
456
|
+
result = client.configure(format: '720p')
|
|
457
|
+
|
|
458
|
+
expect(result).to eq(client)
|
|
459
|
+
end
|
|
460
|
+
|
|
461
|
+
it 'preserves existing options' do
|
|
462
|
+
original_path = client.options.output_path
|
|
463
|
+
client.configure(format: '720p')
|
|
464
|
+
|
|
465
|
+
expect(client.options.output_path).to eq(original_path)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
it 'can be chained' do
|
|
469
|
+
client
|
|
470
|
+
.configure(format: '1080p')
|
|
471
|
+
.configure(write_subtitles: true)
|
|
472
|
+
.configure(audio_quality: '320')
|
|
473
|
+
|
|
474
|
+
expect(client.options.format).to eq('1080p')
|
|
475
|
+
expect(client.options.write_subtitles).to eq(true)
|
|
476
|
+
expect(client.options.audio_quality).to eq('320')
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
describe '#check_dependencies' do
|
|
481
|
+
it 'returns hash with dependency status' do
|
|
482
|
+
deps = client.check_dependencies
|
|
483
|
+
|
|
484
|
+
expect(deps).to be_a(Hash)
|
|
485
|
+
expect(deps).to have_key(:ffmpeg)
|
|
486
|
+
expect([true, false]).to include(deps[:ffmpeg])
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
it 'checks ffmpeg availability' do
|
|
490
|
+
allow(client).to receive(:system).with('which ffmpeg > /dev/null 2>&1').and_return(true)
|
|
491
|
+
|
|
492
|
+
deps = client.check_dependencies
|
|
493
|
+
expect(deps[:ffmpeg]).to be true
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
it 'returns false when ffmpeg not available' do
|
|
497
|
+
allow(client).to receive(:system).with('which ffmpeg > /dev/null 2>&1').and_return(false)
|
|
498
|
+
|
|
499
|
+
deps = client.check_dependencies
|
|
500
|
+
expect(deps[:ffmpeg]).to be false
|
|
501
|
+
end
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
describe '#version' do
|
|
505
|
+
it 'returns version string' do
|
|
506
|
+
expect(client.version).to eq(YoutubeRb::VERSION)
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
it 'returns non-empty version' do
|
|
510
|
+
expect(client.version).not_to be_nil
|
|
511
|
+
expect(client.version).not_to be_empty
|
|
512
|
+
end
|
|
513
|
+
end
|
|
514
|
+
end
|