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
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
module WebmockHelper
|
|
2
|
+
# Stub YouTube page request
|
|
3
|
+
def stub_youtube_page(video_id, video_data)
|
|
4
|
+
# Stub the main video page
|
|
5
|
+
url = "https://www.youtube.com/watch?v=#{video_id}"
|
|
6
|
+
|
|
7
|
+
# Also stub various YouTube URL formats
|
|
8
|
+
stub_youtube_url_variant("https://www.youtube.com/watch?v=#{video_id}", video_id, video_data)
|
|
9
|
+
stub_youtube_url_variant("https://youtu.be/#{video_id}", video_id, video_data)
|
|
10
|
+
stub_youtube_url_variant("https://www.youtube.com/embed/#{video_id}", video_id, video_data)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def stub_youtube_url_variant(url, video_id, video_data)
|
|
14
|
+
# Create a fake YouTube page HTML with player response
|
|
15
|
+
player_response = {
|
|
16
|
+
'videoDetails' => {
|
|
17
|
+
'videoId' => video_data['id'],
|
|
18
|
+
'title' => video_data['title'],
|
|
19
|
+
'shortDescription' => video_data['description'],
|
|
20
|
+
'lengthSeconds' => video_data['duration'].to_s,
|
|
21
|
+
'viewCount' => video_data['view_count'].to_s,
|
|
22
|
+
'author' => video_data['uploader'],
|
|
23
|
+
'channelId' => video_data['uploader_id']
|
|
24
|
+
},
|
|
25
|
+
'streamingData' => {
|
|
26
|
+
'formats' => video_data['formats'].map { |f| format_to_player_format(f) }
|
|
27
|
+
},
|
|
28
|
+
'captions' => {
|
|
29
|
+
'playerCaptionsTracklistRenderer' => {
|
|
30
|
+
'captionTracks' => subtitles_to_caption_tracks(video_data['subtitles'])
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
'microformat' => {
|
|
34
|
+
'playerMicroformatRenderer' => {
|
|
35
|
+
'title' => video_data['title'],
|
|
36
|
+
'description' => video_data['description'],
|
|
37
|
+
'uploadDate' => format_upload_date(video_data['upload_date']),
|
|
38
|
+
'ownerChannelName' => video_data['uploader'],
|
|
39
|
+
'externalChannelId' => video_data['uploader_id'],
|
|
40
|
+
'thumbnail' => {
|
|
41
|
+
'thumbnails' => [
|
|
42
|
+
{ 'url' => video_data['thumbnail'] }
|
|
43
|
+
]
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
html = <<~HTML
|
|
50
|
+
<!DOCTYPE html>
|
|
51
|
+
<html>
|
|
52
|
+
<head><title>#{video_data['title']}</title></head>
|
|
53
|
+
<body>
|
|
54
|
+
<script>
|
|
55
|
+
var ytInitialPlayerResponse = #{player_response.to_json};
|
|
56
|
+
</script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
HTML
|
|
60
|
+
|
|
61
|
+
stub_request(:get, url)
|
|
62
|
+
.to_return(status: 200, body: html, headers: { 'Content-Type' => 'text/html' })
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Stub video download
|
|
66
|
+
def stub_video_download(url, content = nil)
|
|
67
|
+
content ||= sample_video_binary
|
|
68
|
+
|
|
69
|
+
# Stub both with and without Range header
|
|
70
|
+
stub_request(:get, url)
|
|
71
|
+
.to_return(status: 200, body: content, headers: { 'Content-Type' => 'video/mp4' })
|
|
72
|
+
|
|
73
|
+
# Also stub all example.com video URLs
|
|
74
|
+
stub_request(:get, /example\.com\/video.*\.mp4/)
|
|
75
|
+
.to_return(status: 200, body: content, headers: { 'Content-Type' => 'video/mp4' })
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Stub subtitle download
|
|
79
|
+
def stub_subtitle_download(url, content = nil)
|
|
80
|
+
content ||= sample_subtitle_vtt
|
|
81
|
+
|
|
82
|
+
stub_request(:get, url)
|
|
83
|
+
.to_return(status: 200, body: content, headers: { 'Content-Type' => 'text/vtt' })
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Stub thumbnail download
|
|
87
|
+
def stub_thumbnail_download(url, content = nil)
|
|
88
|
+
content ||= "\xFF\xD8\xFF\xE0" # JPEG header
|
|
89
|
+
|
|
90
|
+
stub_request(:get, url)
|
|
91
|
+
.to_return(status: 200, body: content, headers: { 'Content-Type' => 'image/jpeg' })
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def format_to_player_format(format)
|
|
97
|
+
{
|
|
98
|
+
'itag' => format['format_id'].to_i,
|
|
99
|
+
'url' => format['url'],
|
|
100
|
+
'mimeType' => "video/#{format['ext']}; codecs=\"#{format['vcodec']}, #{format['acodec']}\"",
|
|
101
|
+
'width' => format['width'],
|
|
102
|
+
'height' => format['height'],
|
|
103
|
+
'quality' => format['quality'],
|
|
104
|
+
'qualityLabel' => "#{format['height']}p",
|
|
105
|
+
'bitrate' => format['tbr'] ? format['tbr'] * 1000 : nil
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def subtitles_to_caption_tracks(subtitles)
|
|
110
|
+
return [] unless subtitles
|
|
111
|
+
|
|
112
|
+
subtitles.flat_map do |lang, subs|
|
|
113
|
+
subs.map do |sub|
|
|
114
|
+
{
|
|
115
|
+
'languageCode' => lang,
|
|
116
|
+
'baseUrl' => sub['url'],
|
|
117
|
+
'name' => { 'simpleText' => sub['name'] }
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def format_upload_date(date_str)
|
|
124
|
+
return nil unless date_str
|
|
125
|
+
# Convert YYYYMMDD to YYYY-MM-DD
|
|
126
|
+
"#{date_str[0..3]}-#{date_str[4..5]}-#{date_str[6..7]}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
RSpec.configure do |config|
|
|
131
|
+
config.include WebmockHelper
|
|
132
|
+
end
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
RSpec.describe YoutubeRb do
|
|
2
|
+
it "has a version number" do
|
|
3
|
+
expect(YoutubeRb::VERSION).not_to be nil
|
|
4
|
+
end
|
|
5
|
+
|
|
6
|
+
describe ".new" do
|
|
7
|
+
it "creates a client instance" do
|
|
8
|
+
client = YoutubeRb.new
|
|
9
|
+
expect(client).to be_a(YoutubeRb::Client)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
it "accepts options" do
|
|
13
|
+
client = YoutubeRb.new(output_path: './test')
|
|
14
|
+
expect(client.options.output_path).to eq('./test')
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
describe YoutubeRb::Client do
|
|
19
|
+
let(:client) { YoutubeRb::Client.new(output_path: './test_downloads') }
|
|
20
|
+
|
|
21
|
+
describe "#initialize" do
|
|
22
|
+
it "creates options from hash" do
|
|
23
|
+
expect(client.options).to be_a(YoutubeRb::Options)
|
|
24
|
+
expect(client.options.output_path).to eq('./test_downloads')
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
describe "#check_dependencies" do
|
|
29
|
+
it "returns hash with dependency status" do
|
|
30
|
+
deps = client.check_dependencies
|
|
31
|
+
expect(deps).to be_a(Hash)
|
|
32
|
+
expect(deps).to have_key(:ffmpeg)
|
|
33
|
+
expect(deps).to have_key(:ytdlp)
|
|
34
|
+
expect(deps).to have_key(:ytdlp_version)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
it "checks ffmpeg availability" do
|
|
38
|
+
deps = client.check_dependencies
|
|
39
|
+
expect([true, false]).to include(deps[:ffmpeg])
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
it "checks yt-dlp availability" do
|
|
43
|
+
deps = client.check_dependencies
|
|
44
|
+
expect([true, false]).to include(deps[:ytdlp])
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "#version" do
|
|
49
|
+
it "returns version string" do
|
|
50
|
+
expect(client.version).to eq(YoutubeRb::VERSION)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
describe YoutubeRb::Options do
|
|
56
|
+
describe "#initialize" do
|
|
57
|
+
it "creates with default options" do
|
|
58
|
+
options = YoutubeRb::Options.new
|
|
59
|
+
expect(options.format).to eq('best')
|
|
60
|
+
expect(options.quality).to eq('best')
|
|
61
|
+
expect(options.output_path).to eq('./downloads')
|
|
62
|
+
expect(options.use_ytdlp).to eq(false)
|
|
63
|
+
expect(options.ytdlp_fallback).to eq(true)
|
|
64
|
+
expect(options.verbose).to eq(false)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
it "creates with custom options" do
|
|
68
|
+
options = YoutubeRb::Options.new(
|
|
69
|
+
format: '1080p',
|
|
70
|
+
output_path: '/tmp',
|
|
71
|
+
write_subtitles: true,
|
|
72
|
+
use_ytdlp: true,
|
|
73
|
+
verbose: true
|
|
74
|
+
)
|
|
75
|
+
expect(options.format).to eq('1080p')
|
|
76
|
+
expect(options.output_path).to eq('/tmp')
|
|
77
|
+
expect(options.write_subtitles).to eq(true)
|
|
78
|
+
expect(options.use_ytdlp).to eq(true)
|
|
79
|
+
expect(options.verbose).to eq(true)
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe "#to_h" do
|
|
84
|
+
it "returns hash representation" do
|
|
85
|
+
options = YoutubeRb::Options.new(format: 'best', use_ytdlp: true)
|
|
86
|
+
hash = options.to_h
|
|
87
|
+
expect(hash).to be_a(Hash)
|
|
88
|
+
expect(hash[:format]).to eq('best')
|
|
89
|
+
expect(hash[:use_ytdlp]).to eq(true)
|
|
90
|
+
expect(hash).to have_key(:ytdlp_fallback)
|
|
91
|
+
expect(hash).to have_key(:verbose)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
describe "#merge" do
|
|
96
|
+
it "merges options" do
|
|
97
|
+
options = YoutubeRb::Options.new(format: 'best')
|
|
98
|
+
options.merge(format: '720p', quality: 'high')
|
|
99
|
+
expect(options.format).to eq('720p')
|
|
100
|
+
expect(options.quality).to eq('high')
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
describe YoutubeRb::VideoInfo do
|
|
106
|
+
let(:video_data) do
|
|
107
|
+
{
|
|
108
|
+
'id' => 'test123',
|
|
109
|
+
'title' => 'Test Video',
|
|
110
|
+
'description' => 'Test description',
|
|
111
|
+
'duration' => 180,
|
|
112
|
+
'view_count' => 1000,
|
|
113
|
+
'formats' => [
|
|
114
|
+
{
|
|
115
|
+
'format_id' => '18',
|
|
116
|
+
'ext' => 'mp4',
|
|
117
|
+
'height' => 360,
|
|
118
|
+
'vcodec' => 'avc1',
|
|
119
|
+
'acodec' => 'mp4a'
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
'subtitles' => {
|
|
123
|
+
'en' => [{ 'ext' => 'srt', 'url' => 'https://example.com/sub.srt' }]
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
let(:video_info) { YoutubeRb::VideoInfo.new(video_data) }
|
|
129
|
+
|
|
130
|
+
describe "#initialize" do
|
|
131
|
+
it "parses video data" do
|
|
132
|
+
expect(video_info.id).to eq('test123')
|
|
133
|
+
expect(video_info.title).to eq('Test Video')
|
|
134
|
+
expect(video_info.duration).to eq(180)
|
|
135
|
+
expect(video_info.view_count).to eq(1000)
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
describe "#duration_formatted" do
|
|
140
|
+
it "formats duration" do
|
|
141
|
+
expect(video_info.duration_formatted).to eq('03:00')
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
it "formats duration with hours" do
|
|
145
|
+
info = YoutubeRb::VideoInfo.new('duration' => 3665)
|
|
146
|
+
expect(info.duration_formatted).to eq('01:01:05')
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
describe "#available_formats" do
|
|
151
|
+
it "returns format IDs" do
|
|
152
|
+
expect(video_info.available_formats).to eq(['18'])
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
describe "#available_subtitle_languages" do
|
|
157
|
+
it "returns subtitle languages" do
|
|
158
|
+
expect(video_info.available_subtitle_languages).to eq(['en'])
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
describe "#to_h" do
|
|
163
|
+
it "returns hash representation" do
|
|
164
|
+
hash = video_info.to_h
|
|
165
|
+
expect(hash).to be_a(Hash)
|
|
166
|
+
expect(hash[:id]).to eq('test123')
|
|
167
|
+
expect(hash[:title]).to eq('Test Video')
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
describe YoutubeRb::Extractor do
|
|
173
|
+
let(:extractor) { YoutubeRb::Extractor.new('https://www.youtube.com/watch?v=test') }
|
|
174
|
+
|
|
175
|
+
describe "#initialize" do
|
|
176
|
+
it "creates extractor with URL" do
|
|
177
|
+
expect(extractor.url).to eq('https://www.youtube.com/watch?v=test')
|
|
178
|
+
end
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
describe YoutubeRb::Downloader do
|
|
183
|
+
let(:url) { 'https://www.youtube.com/watch?v=test' }
|
|
184
|
+
let(:options) { YoutubeRb::Options.new(output_path: './test_downloads') }
|
|
185
|
+
let(:downloader) { YoutubeRb::Downloader.new(url, options) }
|
|
186
|
+
|
|
187
|
+
describe "#initialize" do
|
|
188
|
+
it "creates downloader with URL and options" do
|
|
189
|
+
expect(downloader.url).to eq(url)
|
|
190
|
+
expect(downloader.options).to be_a(YoutubeRb::Options)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
it "accepts hash options" do
|
|
194
|
+
dl = YoutubeRb::Downloader.new(url, { output_path: './test' })
|
|
195
|
+
expect(dl.options).to be_a(YoutubeRb::Options)
|
|
196
|
+
expect(dl.options.output_path).to eq('./test')
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
RSpec.describe YoutubeRb::YtdlpWrapper do
|
|
2
|
+
describe ".available?" do
|
|
3
|
+
it "returns boolean" do
|
|
4
|
+
result = described_class.available?
|
|
5
|
+
expect([true, false]).to include(result)
|
|
6
|
+
end
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
describe ".version" do
|
|
10
|
+
it "returns version string" do
|
|
11
|
+
version = described_class.version
|
|
12
|
+
expect(version).to be_a(String)
|
|
13
|
+
expect(version).not_to be_empty
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
it "returns 'not installed' when yt-dlp command fails" do
|
|
17
|
+
allow(Open3).to receive(:capture2).with('yt-dlp', '--version').and_raise(Errno::ENOENT)
|
|
18
|
+
version = described_class.version
|
|
19
|
+
expect(version).to eq('not installed')
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
context "when yt-dlp is available", if: YoutubeRb::YtdlpWrapper.available? do
|
|
24
|
+
let(:options) { YoutubeRb::Options.new }
|
|
25
|
+
let(:wrapper) { described_class.new(options) }
|
|
26
|
+
|
|
27
|
+
describe "#initialize" do
|
|
28
|
+
it "creates wrapper with options" do
|
|
29
|
+
expect(wrapper.options).to be_a(YoutubeRb::Options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
it "accepts hash options" do
|
|
33
|
+
wrapper = described_class.new(output_path: './test')
|
|
34
|
+
expect(wrapper.options).to be_a(YoutubeRb::Options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
describe "#extract_info" do
|
|
39
|
+
it "extracts video information", :slow do
|
|
40
|
+
# Note: This test requires internet and may need cookies for some videos
|
|
41
|
+
# Skip if not running integration tests
|
|
42
|
+
skip "Requires internet and may need cookies" unless ENV['RUN_INTEGRATION_TESTS']
|
|
43
|
+
|
|
44
|
+
url = 'https://www.youtube.com/watch?v=jNQXAC9IVRw'
|
|
45
|
+
info = wrapper.extract_info(url)
|
|
46
|
+
|
|
47
|
+
expect(info).to be_a(Hash)
|
|
48
|
+
expect(info).to have_key('id')
|
|
49
|
+
expect(info).to have_key('title')
|
|
50
|
+
expect(info).to have_key('duration')
|
|
51
|
+
expect(info['id']).to eq('jNQXAC9IVRw')
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
it "raises error for invalid URL" do
|
|
55
|
+
expect {
|
|
56
|
+
wrapper.extract_info('https://www.youtube.com/watch?v=invalidvideoid')
|
|
57
|
+
}.to raise_error(YoutubeRb::YtdlpWrapper::YtdlpError)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
describe "private methods" do
|
|
62
|
+
describe "#build_info_args" do
|
|
63
|
+
it "builds correct arguments for info extraction" do
|
|
64
|
+
url = 'https://www.youtube.com/watch?v=test'
|
|
65
|
+
args = wrapper.send(:build_info_args, url)
|
|
66
|
+
|
|
67
|
+
expect(args).to include('yt-dlp')
|
|
68
|
+
expect(args).to include('--dump-json')
|
|
69
|
+
expect(args).to include('--no-playlist')
|
|
70
|
+
expect(args).to include(url)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
it "includes cookies file if specified" do
|
|
74
|
+
options = YoutubeRb::Options.new(cookies_file: './test_cookies.txt')
|
|
75
|
+
wrapper = described_class.new(options)
|
|
76
|
+
|
|
77
|
+
# Create temporary cookies file
|
|
78
|
+
File.write('./test_cookies.txt', 'test')
|
|
79
|
+
|
|
80
|
+
url = 'https://www.youtube.com/watch?v=test'
|
|
81
|
+
args = wrapper.send(:build_info_args, url)
|
|
82
|
+
|
|
83
|
+
expect(args).to include('--cookies')
|
|
84
|
+
expect(args).to include('./test_cookies.txt')
|
|
85
|
+
|
|
86
|
+
File.delete('./test_cookies.txt')
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
describe "#build_download_args" do
|
|
91
|
+
it "builds correct arguments for download" do
|
|
92
|
+
url = 'https://www.youtube.com/watch?v=test'
|
|
93
|
+
args = wrapper.send(:build_download_args, url, nil)
|
|
94
|
+
|
|
95
|
+
expect(args).to include('yt-dlp')
|
|
96
|
+
expect(args).to include(url)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
it "includes output path" do
|
|
100
|
+
url = 'https://www.youtube.com/watch?v=test'
|
|
101
|
+
output = './test_output.mp4'
|
|
102
|
+
args = wrapper.send(:build_download_args, url, output)
|
|
103
|
+
|
|
104
|
+
expect(args).to include('-o')
|
|
105
|
+
expect(args).to include(output)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
it "includes audio extraction options" do
|
|
109
|
+
options = YoutubeRb::Options.new(
|
|
110
|
+
extract_audio: true,
|
|
111
|
+
audio_format: 'mp3',
|
|
112
|
+
audio_quality: '192'
|
|
113
|
+
)
|
|
114
|
+
wrapper = described_class.new(options)
|
|
115
|
+
|
|
116
|
+
url = 'https://www.youtube.com/watch?v=test'
|
|
117
|
+
args = wrapper.send(:build_download_args, url, nil)
|
|
118
|
+
|
|
119
|
+
expect(args).to include('-x')
|
|
120
|
+
expect(args).to include('--audio-format')
|
|
121
|
+
expect(args).to include('mp3')
|
|
122
|
+
expect(args).to include('--audio-quality')
|
|
123
|
+
expect(args).to include('192')
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
it "includes subtitle options" do
|
|
127
|
+
options = YoutubeRb::Options.new(
|
|
128
|
+
write_subtitles: true,
|
|
129
|
+
subtitle_langs: ['en', 'ru']
|
|
130
|
+
)
|
|
131
|
+
wrapper = described_class.new(options)
|
|
132
|
+
|
|
133
|
+
url = 'https://www.youtube.com/watch?v=test'
|
|
134
|
+
args = wrapper.send(:build_download_args, url, nil)
|
|
135
|
+
|
|
136
|
+
expect(args).to include('--write-subs')
|
|
137
|
+
expect(args).to include('--sub-langs')
|
|
138
|
+
expect(args).to include('en,ru')
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
describe "#build_format_string" do
|
|
143
|
+
it "returns best format by default" do
|
|
144
|
+
format = wrapper.send(:build_format_string)
|
|
145
|
+
expect(format).to eq('bestvideo+bestaudio/best')
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
it "handles quality specifications" do
|
|
149
|
+
options = YoutubeRb::Options.new(quality: '720p')
|
|
150
|
+
wrapper = described_class.new(options)
|
|
151
|
+
|
|
152
|
+
format = wrapper.send(:build_format_string)
|
|
153
|
+
expect(format).to include('720')
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
it "returns worst format when requested" do
|
|
157
|
+
options = YoutubeRb::Options.new(quality: 'worst')
|
|
158
|
+
wrapper = described_class.new(options)
|
|
159
|
+
|
|
160
|
+
format = wrapper.send(:build_format_string)
|
|
161
|
+
expect(format).to eq('worstvideo+worstaudio/worst')
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
context "when yt-dlp is not available", unless: YoutubeRb::YtdlpWrapper.available? do
|
|
168
|
+
describe "#initialize" do
|
|
169
|
+
it "raises YtdlpNotFoundError" do
|
|
170
|
+
options = YoutubeRb::Options.new
|
|
171
|
+
|
|
172
|
+
expect {
|
|
173
|
+
described_class.new(options)
|
|
174
|
+
}.to raise_error(YoutubeRb::YtdlpWrapper::YtdlpNotFoundError, /yt-dlp is not installed/)
|
|
175
|
+
end
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
data/youtube-rb.gemspec
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
require_relative "lib/youtube-rb/version"
|
|
2
|
+
|
|
3
|
+
Gem::Specification.new do |spec|
|
|
4
|
+
spec.name = "youtube-rb"
|
|
5
|
+
spec.version = YoutubeRb::VERSION
|
|
6
|
+
spec.authors = ["Maxim Veysgeym"]
|
|
7
|
+
spec.email = ["qew7777@gmail.com"]
|
|
8
|
+
|
|
9
|
+
spec.summary = "A Ruby library for downloading and extracting YouTube videos and subtitles"
|
|
10
|
+
spec.description = "A Ruby library inspired by youtube-dl for downloading videos, extracting video segments, and fetching subtitles from YouTube and other video platforms"
|
|
11
|
+
spec.homepage = "https://github.com/Qew7/youtube-rb"
|
|
12
|
+
spec.license = "MIT"
|
|
13
|
+
spec.required_ruby_version = ">= 3.4.0"
|
|
14
|
+
|
|
15
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
|
16
|
+
spec.metadata["source_code_uri"] = "https://github.com/Qew7/youtube-rb"
|
|
17
|
+
spec.metadata["changelog_uri"] = "https://github.com/Qew7/youtube-rb/blob/master/CHANGELOG.md"
|
|
18
|
+
|
|
19
|
+
# Specify which files should be added to the gem when it is released.
|
|
20
|
+
spec.files = Dir.glob("{lib,bin,spec}/**/*") + %w[README.md LICENSE Rakefile youtube-rb.gemspec]
|
|
21
|
+
spec.bindir = "bin"
|
|
22
|
+
spec.executables = spec.files.grep(%r{\Abin/}) { |f| File.basename(f) }
|
|
23
|
+
spec.require_paths = ["lib"]
|
|
24
|
+
|
|
25
|
+
# Runtime dependencies
|
|
26
|
+
spec.add_dependency "faraday", "~> 2.14"
|
|
27
|
+
spec.add_dependency "faraday-retry", "~> 2.4"
|
|
28
|
+
spec.add_dependency "nokogiri", "~> 1.19"
|
|
29
|
+
spec.add_dependency "streamio-ffmpeg", "~> 3.0"
|
|
30
|
+
spec.add_dependency "addressable", "~> 2.8"
|
|
31
|
+
spec.add_dependency "base64", "~> 0.2"
|
|
32
|
+
|
|
33
|
+
# Development dependencies
|
|
34
|
+
spec.add_development_dependency "bundler", ">= 2.7"
|
|
35
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
|
36
|
+
spec.add_development_dependency "rspec", "~> 3.13"
|
|
37
|
+
spec.add_development_dependency "webmock", "~> 3.26"
|
|
38
|
+
spec.add_development_dependency "vcr", "~> 6.4"
|
|
39
|
+
end
|