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,179 @@
|
|
|
1
|
+
module YoutubeRb
|
|
2
|
+
class VideoInfo
|
|
3
|
+
attr_reader :id, :title, :description, :uploader, :uploader_id,
|
|
4
|
+
:duration, :view_count, :like_count, :upload_date,
|
|
5
|
+
:thumbnail, :formats, :subtitles, :url, :ext,
|
|
6
|
+
:webpage_url, :fulltitle
|
|
7
|
+
|
|
8
|
+
def initialize(data)
|
|
9
|
+
@id = data['id'] || data[:id]
|
|
10
|
+
@title = data['title'] || data[:title]
|
|
11
|
+
@fulltitle = data['fulltitle'] || data[:fulltitle] || @title
|
|
12
|
+
@description = data['description'] || data[:description]
|
|
13
|
+
@uploader = data['uploader'] || data[:uploader]
|
|
14
|
+
@uploader_id = data['uploader_id'] || data[:uploader_id]
|
|
15
|
+
@duration = parse_duration(data['duration'] || data[:duration])
|
|
16
|
+
@view_count = data['view_count'] || data[:view_count]
|
|
17
|
+
@like_count = data['like_count'] || data[:like_count]
|
|
18
|
+
@upload_date = data['upload_date'] || data[:upload_date]
|
|
19
|
+
@thumbnail = data['thumbnail'] || data[:thumbnail]
|
|
20
|
+
@formats = parse_formats(data['formats'] || data[:formats] || [])
|
|
21
|
+
@subtitles = parse_subtitles(data['subtitles'] || data[:subtitles] || {})
|
|
22
|
+
@url = data['url'] || data[:url]
|
|
23
|
+
@ext = data['ext'] || data[:ext]
|
|
24
|
+
@webpage_url = data['webpage_url'] || data[:webpage_url]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def available_formats
|
|
28
|
+
@formats.map { |f| f[:format_id] }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def available_qualities
|
|
32
|
+
@formats.map { |f| f[:quality] }.uniq.compact
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def available_subtitle_languages
|
|
36
|
+
@subtitles.keys
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def best_format
|
|
40
|
+
@formats.max_by { |f| format_priority(f) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def worst_format
|
|
44
|
+
@formats.min_by { |f| format_priority(f) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def best_video_format
|
|
48
|
+
video_formats.max_by { |f| format_priority(f) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def best_audio_format
|
|
52
|
+
audio_formats.max_by { |f| format_priority(f) }
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def video_formats
|
|
56
|
+
@formats.select { |f| f[:vcodec] && f[:vcodec] != 'none' }
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def audio_formats
|
|
60
|
+
@formats.select { |f| f[:acodec] && f[:acodec] != 'none' }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def get_format(format_id)
|
|
64
|
+
@formats.find { |f| f[:format_id] == format_id }
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def get_subtitle(lang)
|
|
68
|
+
@subtitles[lang]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def duration_in_seconds
|
|
72
|
+
@duration
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def duration_formatted
|
|
76
|
+
return nil unless @duration
|
|
77
|
+
|
|
78
|
+
hours = @duration / 3600
|
|
79
|
+
minutes = (@duration % 3600) / 60
|
|
80
|
+
seconds = @duration % 60
|
|
81
|
+
|
|
82
|
+
if hours > 0
|
|
83
|
+
format("%02d:%02d:%02d", hours, minutes, seconds)
|
|
84
|
+
else
|
|
85
|
+
format("%02d:%02d", minutes, seconds)
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def to_h
|
|
90
|
+
{
|
|
91
|
+
id: @id,
|
|
92
|
+
title: @title,
|
|
93
|
+
fulltitle: @fulltitle,
|
|
94
|
+
description: @description,
|
|
95
|
+
uploader: @uploader,
|
|
96
|
+
uploader_id: @uploader_id,
|
|
97
|
+
duration: @duration,
|
|
98
|
+
duration_formatted: duration_formatted,
|
|
99
|
+
view_count: @view_count,
|
|
100
|
+
like_count: @like_count,
|
|
101
|
+
upload_date: @upload_date,
|
|
102
|
+
thumbnail: @thumbnail,
|
|
103
|
+
formats: @formats,
|
|
104
|
+
subtitles: @subtitles,
|
|
105
|
+
url: @url,
|
|
106
|
+
ext: @ext,
|
|
107
|
+
webpage_url: @webpage_url
|
|
108
|
+
}
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
private
|
|
112
|
+
|
|
113
|
+
def parse_duration(duration)
|
|
114
|
+
return nil unless duration
|
|
115
|
+
duration.is_a?(Numeric) ? duration.to_i : duration.to_s.to_i
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_formats(formats_data)
|
|
119
|
+
return [] unless formats_data.is_a?(Array)
|
|
120
|
+
|
|
121
|
+
formats_data.map do |format|
|
|
122
|
+
{
|
|
123
|
+
format_id: format['format_id'] || format[:format_id],
|
|
124
|
+
format_note: format['format_note'] || format[:format_note],
|
|
125
|
+
ext: format['ext'] || format[:ext],
|
|
126
|
+
url: format['url'] || format[:url],
|
|
127
|
+
width: format['width'] || format[:width],
|
|
128
|
+
height: format['height'] || format[:height],
|
|
129
|
+
fps: format['fps'] || format[:fps],
|
|
130
|
+
vcodec: format['vcodec'] || format[:vcodec],
|
|
131
|
+
acodec: format['acodec'] || format[:acodec],
|
|
132
|
+
tbr: format['tbr'] || format[:tbr],
|
|
133
|
+
abr: format['abr'] || format[:abr],
|
|
134
|
+
vbr: format['vbr'] || format[:vbr],
|
|
135
|
+
filesize: format['filesize'] || format[:filesize],
|
|
136
|
+
quality: format['quality'] || format[:quality],
|
|
137
|
+
protocol: format['protocol'] || format[:protocol]
|
|
138
|
+
}
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def parse_subtitles(subtitles_data)
|
|
143
|
+
return {} unless subtitles_data.is_a?(Hash)
|
|
144
|
+
|
|
145
|
+
subtitles_data.transform_values do |subtitle_formats|
|
|
146
|
+
subtitle_formats.map do |sub|
|
|
147
|
+
{
|
|
148
|
+
ext: sub['ext'] || sub[:ext],
|
|
149
|
+
url: sub['url'] || sub[:url],
|
|
150
|
+
name: sub['name'] || sub[:name]
|
|
151
|
+
}
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def format_priority(format)
|
|
157
|
+
# Priority calculation based on multiple factors
|
|
158
|
+
priority = 0
|
|
159
|
+
|
|
160
|
+
# Video quality
|
|
161
|
+
if format[:height]
|
|
162
|
+
priority += format[:height] * 100
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Bitrate
|
|
166
|
+
if format[:tbr]
|
|
167
|
+
priority += format[:tbr]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Prefer certain codecs
|
|
171
|
+
if format[:vcodec]
|
|
172
|
+
priority += 10 if format[:vcodec].include?('avc')
|
|
173
|
+
priority += 5 if format[:vcodec].include?('vp9')
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
priority
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
require 'open3'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'shellwords'
|
|
4
|
+
|
|
5
|
+
module YoutubeRb
|
|
6
|
+
class YtdlpWrapper
|
|
7
|
+
class YtdlpNotFoundError < StandardError; end
|
|
8
|
+
class YtdlpError < StandardError; end
|
|
9
|
+
|
|
10
|
+
attr_reader :options
|
|
11
|
+
|
|
12
|
+
def initialize(options = {})
|
|
13
|
+
@options = options.is_a?(Options) ? options : Options.new(**options)
|
|
14
|
+
check_ytdlp_installation!
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Extract video information using yt-dlp
|
|
18
|
+
# @param url [String] Video URL
|
|
19
|
+
# @return [Hash] Video information
|
|
20
|
+
def extract_info(url)
|
|
21
|
+
args = build_info_args(url)
|
|
22
|
+
output, status = execute_ytdlp(args)
|
|
23
|
+
|
|
24
|
+
raise YtdlpError, "Failed to extract info: #{output}" unless status.success?
|
|
25
|
+
|
|
26
|
+
JSON.parse(output)
|
|
27
|
+
rescue JSON::ParserError => e
|
|
28
|
+
raise YtdlpError, "Failed to parse yt-dlp output: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Download video using yt-dlp
|
|
32
|
+
# @param url [String] Video URL
|
|
33
|
+
# @param output_path [String] Output file path
|
|
34
|
+
# @return [String] Path to downloaded file
|
|
35
|
+
def download(url, output_path = nil)
|
|
36
|
+
args = build_download_args(url, output_path)
|
|
37
|
+
output, status = execute_ytdlp(args, show_progress: true)
|
|
38
|
+
|
|
39
|
+
unless status.success?
|
|
40
|
+
raise YtdlpError, "Download failed: #{output}"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Return the actual output path
|
|
44
|
+
output_path || detect_output_file(output)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Download video segment using yt-dlp
|
|
48
|
+
# @param url [String] Video URL
|
|
49
|
+
# @param start_time [Integer] Start time in seconds
|
|
50
|
+
# @param end_time [Integer] End time in seconds
|
|
51
|
+
# @param output_path [String, nil] Output file path
|
|
52
|
+
# @return [String] Path to downloaded segment
|
|
53
|
+
def download_segment(url, start_time, end_time, output_path = nil)
|
|
54
|
+
args = build_segment_args(url, start_time, end_time, output_path)
|
|
55
|
+
output, status = execute_ytdlp(args, show_progress: true)
|
|
56
|
+
|
|
57
|
+
unless status.success?
|
|
58
|
+
raise YtdlpError, "Segment download failed: #{output}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
output_path || detect_output_file(output)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Check if yt-dlp is installed
|
|
65
|
+
# @return [Boolean]
|
|
66
|
+
def self.available?
|
|
67
|
+
system('which yt-dlp > /dev/null 2>&1') ||
|
|
68
|
+
system('which youtube-dl > /dev/null 2>&1')
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Get yt-dlp version
|
|
72
|
+
# @return [String] Version string
|
|
73
|
+
def self.version
|
|
74
|
+
output, status = Open3.capture2('yt-dlp', '--version')
|
|
75
|
+
status.success? ? output.strip : 'unknown'
|
|
76
|
+
rescue
|
|
77
|
+
'not installed'
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def check_ytdlp_installation!
|
|
83
|
+
unless self.class.available?
|
|
84
|
+
raise YtdlpNotFoundError, <<~MSG
|
|
85
|
+
yt-dlp is not installed. Please install it:
|
|
86
|
+
|
|
87
|
+
# Using pip:
|
|
88
|
+
pip install -U yt-dlp
|
|
89
|
+
|
|
90
|
+
# Or using pipx:
|
|
91
|
+
pipx install yt-dlp
|
|
92
|
+
|
|
93
|
+
# Or using homebrew (macOS):
|
|
94
|
+
brew install yt-dlp
|
|
95
|
+
|
|
96
|
+
# Or download binary from:
|
|
97
|
+
https://github.com/yt-dlp/yt-dlp/releases
|
|
98
|
+
MSG
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def build_info_args(url)
|
|
103
|
+
args = ['yt-dlp', '--dump-json', '--no-playlist']
|
|
104
|
+
|
|
105
|
+
# Add cookies file if specified
|
|
106
|
+
if @options.cookies_file && File.exist?(@options.cookies_file)
|
|
107
|
+
args += ['--cookies', @options.cookies_file]
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Add user agent
|
|
111
|
+
if @options.user_agent
|
|
112
|
+
args += ['--user-agent', @options.user_agent]
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Add referer
|
|
116
|
+
if @options.referer
|
|
117
|
+
args += ['--referer', @options.referer]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Add authentication
|
|
121
|
+
if @options.username && @options.password
|
|
122
|
+
args += ['--username', @options.username, '--password', @options.password]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Add retries
|
|
126
|
+
args += ['--retries', @options.retries.to_s]
|
|
127
|
+
|
|
128
|
+
args << url
|
|
129
|
+
args
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def build_download_args(url, output_path)
|
|
133
|
+
args = ['yt-dlp']
|
|
134
|
+
|
|
135
|
+
# Output template
|
|
136
|
+
if output_path
|
|
137
|
+
args += ['-o', output_path]
|
|
138
|
+
else
|
|
139
|
+
template = @options.output_template
|
|
140
|
+
output_dir = @options.output_path
|
|
141
|
+
full_template = File.join(output_dir, template)
|
|
142
|
+
args += ['-o', full_template]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Format selection
|
|
146
|
+
if @options.extract_audio
|
|
147
|
+
args += ['-x', '--audio-format', @options.audio_format]
|
|
148
|
+
args += ['--audio-quality', @options.audio_quality] if @options.audio_quality
|
|
149
|
+
else
|
|
150
|
+
format = build_format_string
|
|
151
|
+
args += ['-f', format] if format
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
# Subtitles
|
|
155
|
+
if @options.write_subtitles
|
|
156
|
+
args << '--write-subs'
|
|
157
|
+
args += ['--sub-langs', @options.subtitle_langs.join(',')]
|
|
158
|
+
args += ['--sub-format', @options.subtitle_format]
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if @options.write_auto_sub
|
|
162
|
+
args << '--write-auto-subs'
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Metadata
|
|
166
|
+
args << '--write-info-json' if @options.write_info_json
|
|
167
|
+
args << '--write-thumbnail' if @options.write_thumbnail
|
|
168
|
+
args << '--write-description' if @options.write_description
|
|
169
|
+
|
|
170
|
+
# Cookies and authentication
|
|
171
|
+
if @options.cookies_file && File.exist?(@options.cookies_file)
|
|
172
|
+
args += ['--cookies', @options.cookies_file]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
if @options.username && @options.password
|
|
176
|
+
args += ['--username', @options.username, '--password', @options.password]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# Network options
|
|
180
|
+
args += ['--user-agent', @options.user_agent] if @options.user_agent
|
|
181
|
+
args += ['--referer', @options.referer] if @options.referer
|
|
182
|
+
args += ['--retries', @options.retries.to_s]
|
|
183
|
+
args += ['--rate-limit', @options.rate_limit] if @options.rate_limit
|
|
184
|
+
|
|
185
|
+
# Filesystem options
|
|
186
|
+
args << '--no-overwrites' if @options.no_overwrites
|
|
187
|
+
args << '--continue' if @options.continue_download
|
|
188
|
+
args << '--no-part' if @options.no_part
|
|
189
|
+
|
|
190
|
+
# Playlist handling
|
|
191
|
+
args << '--no-playlist' if @options.no_playlist
|
|
192
|
+
args << '--yes-playlist' if @options.yes_playlist
|
|
193
|
+
|
|
194
|
+
args << url
|
|
195
|
+
args
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def build_segment_args(url, start_time, end_time, output_path)
|
|
199
|
+
args = build_download_args(url, output_path)
|
|
200
|
+
|
|
201
|
+
# Add download sections for segment
|
|
202
|
+
# yt-dlp format: *start_time-end_time
|
|
203
|
+
section = "*#{start_time}-#{end_time}"
|
|
204
|
+
args.insert(-2, '--download-sections', section)
|
|
205
|
+
|
|
206
|
+
# Segment cutting mode:
|
|
207
|
+
# :fast (default) - Stream copy mode, cuts at keyframes (10x faster, ±2-5s accuracy)
|
|
208
|
+
# :precise - Re-encoding mode, exact timestamps (slow, frame-perfect)
|
|
209
|
+
#
|
|
210
|
+
# Fast mode: Uses stream copy (ffmpeg -c copy) which is very fast but cuts only at
|
|
211
|
+
# keyframe positions. This means segments may be a few seconds longer.
|
|
212
|
+
# Precise mode: Uses --force-keyframes-at-cuts which forces re-encoding to get
|
|
213
|
+
# frame-accurate cuts. This is VERY slow (especially for 4K video).
|
|
214
|
+
if @options.segment_mode == :precise
|
|
215
|
+
args.insert(-2, '--force-keyframes-at-cuts')
|
|
216
|
+
end
|
|
217
|
+
# For :fast mode, we don't add any flags - yt-dlp uses stream copy by default
|
|
218
|
+
|
|
219
|
+
args
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def build_format_string
|
|
223
|
+
case @options.quality
|
|
224
|
+
when 'best'
|
|
225
|
+
'bestvideo+bestaudio/best'
|
|
226
|
+
when 'worst'
|
|
227
|
+
'worstvideo+worstaudio/worst'
|
|
228
|
+
when /^\d+p$/
|
|
229
|
+
# e.g., "720p", "1080p"
|
|
230
|
+
height = @options.quality.to_i
|
|
231
|
+
"bestvideo[height<=#{height}]+bestaudio/best[height<=#{height}]"
|
|
232
|
+
else
|
|
233
|
+
@options.format || 'best'
|
|
234
|
+
end
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def execute_ytdlp(args, show_progress: false)
|
|
238
|
+
# Escape arguments for shell
|
|
239
|
+
escaped_args = args.map { |arg| Shellwords.escape(arg) }
|
|
240
|
+
|
|
241
|
+
if show_progress && @options.respond_to?(:verbose) && @options.verbose
|
|
242
|
+
# Show yt-dlp output in real-time
|
|
243
|
+
puts "Executing: #{args.join(' ')}" if ENV['DEBUG']
|
|
244
|
+
system(*args)
|
|
245
|
+
status = $?
|
|
246
|
+
['', status]
|
|
247
|
+
else
|
|
248
|
+
# Capture output
|
|
249
|
+
stdout, stderr, status = Open3.capture3(*args)
|
|
250
|
+
output = stdout.empty? ? stderr : stdout
|
|
251
|
+
[output, status]
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
def detect_output_file(output)
|
|
256
|
+
# Try to detect the output file from yt-dlp output
|
|
257
|
+
# yt-dlp outputs: [download] Destination: filename.ext
|
|
258
|
+
if match = output.match(/\[download\] Destination: (.+)/)
|
|
259
|
+
return match[1].strip
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
# Fallback: try to find the file in output directory
|
|
263
|
+
# This is less reliable but better than nothing
|
|
264
|
+
output_dir = @options.output_path
|
|
265
|
+
files = Dir.glob(File.join(output_dir, '*')).sort_by { |f| File.mtime(f) }
|
|
266
|
+
files.last
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
end
|
data/lib/youtube-rb.rb
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
require_relative "youtube-rb/version"
|
|
2
|
+
require_relative "youtube-rb/options"
|
|
3
|
+
require_relative "youtube-rb/video_info"
|
|
4
|
+
require_relative "youtube-rb/extractor"
|
|
5
|
+
require_relative "youtube-rb/ytdlp_wrapper"
|
|
6
|
+
require_relative "youtube-rb/downloader"
|
|
7
|
+
require_relative "youtube-rb/client"
|
|
8
|
+
|
|
9
|
+
module YoutubeRb
|
|
10
|
+
class Error < StandardError; end
|
|
11
|
+
class ExtractionError < Error; end
|
|
12
|
+
class DownloadError < Error; end
|
|
13
|
+
class ValidationError < Error; end
|
|
14
|
+
|
|
15
|
+
# Convenience method to create a new client
|
|
16
|
+
# @param options [Hash] Client options
|
|
17
|
+
# @return [Client] New client instance
|
|
18
|
+
def self.new(**options)
|
|
19
|
+
Client.new(**options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Quick download method
|
|
23
|
+
# @param url [String] Video URL
|
|
24
|
+
# @param options [Hash] Download options
|
|
25
|
+
# @return [String] Path to downloaded file
|
|
26
|
+
def self.download(url, **options)
|
|
27
|
+
client = Client.new(**options)
|
|
28
|
+
client.download(url)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Quick info extraction method
|
|
32
|
+
# @param url [String] Video URL
|
|
33
|
+
# @return [VideoInfo] Video information
|
|
34
|
+
def self.info(url)
|
|
35
|
+
client = Client.new
|
|
36
|
+
client.info(url)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Quick segment download method
|
|
40
|
+
# @param url [String] Video URL
|
|
41
|
+
# @param start_time [Integer] Start time in seconds
|
|
42
|
+
# @param end_time [Integer] End time in seconds
|
|
43
|
+
# @param options [Hash] Download options
|
|
44
|
+
# @return [String] Path to downloaded segment
|
|
45
|
+
def self.download_segment(url, start_time, end_time, **options)
|
|
46
|
+
client = Client.new(**options)
|
|
47
|
+
client.download_segment(url, start_time, end_time, **options)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Quick batch segment download method
|
|
51
|
+
# @param url [String] Video URL
|
|
52
|
+
# @param segments [Array<Hash>] Array of segment definitions
|
|
53
|
+
# @param options [Hash] Download options
|
|
54
|
+
# @return [Array<String>] Paths to downloaded segments
|
|
55
|
+
def self.download_segments(url, segments, **options)
|
|
56
|
+
client = Client.new(**options)
|
|
57
|
+
client.download_segments(url, segments, **options)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Quick subtitles download method
|
|
61
|
+
# @param url [String] Video URL
|
|
62
|
+
# @param langs [Array<String>, nil] Languages to download
|
|
63
|
+
# @param options [Hash] Download options
|
|
64
|
+
# @return [void]
|
|
65
|
+
def self.download_subtitles(url, langs: nil, **options)
|
|
66
|
+
client = Client.new(**options)
|
|
67
|
+
client.download_subtitles(url, langs: langs, **options)
|
|
68
|
+
end
|
|
69
|
+
end
|