vidload 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/lib/vidload/cli.rb +45 -32
- data/lib/vidload/mp2t/api.rb +206 -187
- data/lib/vidload/mp2t.rb +6 -2
- data/lib/vidload/mp4/api.rb +130 -0
- data/lib/vidload/mp4.rb +7 -0
- data/lib/vidload/version.rb +3 -1
- data/lib/vidload.rb +6 -3
- metadata +32 -16
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d7f2ad6f46978b5cad7216f00d0c9a74bb2cd7ec537ddab8145d9f8bd0914cde
|
|
4
|
+
data.tar.gz: 0146cabc4cb8f7f39c2b3adaa76293eefa86acf4233890a5116f4988ef590fdd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c195fbe2098e82a3da3317f61cacf1c10f667a6195c0b4a0ebd9d38eee8cec222703cf00aeb1cba48ff14b9d0a98ffc6334ef5a14f4b74aad259683db471ff4c
|
|
7
|
+
data.tar.gz: dd4a308b63da84aa3dcfc8796556f8f48fc53eeabb73c92d39ed2689181bc72b1bb99c776e67cd352db415d405b759975d40c3c85ae877bf898c0644bd95dadf
|
data/lib/vidload/cli.rb
CHANGED
|
@@ -1,36 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'thor'
|
|
4
|
+
|
|
5
|
+
module Vidload
|
|
6
|
+
class Cli < Thor
|
|
7
|
+
desc 'mp2t VIDEO_URL', 'download a mp2t containerized video'
|
|
8
|
+
method_option :video_name, type: :string, required: false
|
|
9
|
+
method_option :author_name, type: :string, required: false
|
|
10
|
+
method_option :output_dir, type: :string, required: false
|
|
11
|
+
method_option :hls_url, type: :string, required: true
|
|
12
|
+
method_option :master_playlist_name, type: :string, required: true
|
|
13
|
+
method_option :playwright_cli_path, type: :string, required: true
|
|
14
|
+
method_option :video_referer, type: :string, required: true
|
|
15
|
+
method_option :ts_seg_pattern, type: :string, required: true
|
|
16
|
+
method_option :hls_index_pattern, type: :string, required: true
|
|
17
|
+
def mp2t(video_url)
|
|
18
|
+
params = {
|
|
19
|
+
video_url: video_url,
|
|
20
|
+
**options
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
process_mp2t(params)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
desc 'mp4 VIDEO_URL', 'download a mp4 video'
|
|
27
|
+
method_option :video_name, type: :string, required: false
|
|
28
|
+
method_option :video_hub_url, type: :string, required: true
|
|
29
|
+
method_option :playwright_cli_path, type: :string, required: true
|
|
30
|
+
def mp4(video_url)
|
|
31
|
+
params = {
|
|
32
|
+
video_url: video_url,
|
|
33
|
+
**options
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process_mp4(params)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
30
40
|
|
|
31
|
-
|
|
41
|
+
def process_mp2t(params)
|
|
42
|
+
raise NotImplementedError
|
|
43
|
+
end
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
def process_mp4(params)
|
|
46
|
+
raise NotImplementedError
|
|
47
|
+
end
|
|
35
48
|
end
|
|
36
49
|
end
|
data/lib/vidload/mp2t/api.rb
CHANGED
|
@@ -1,220 +1,239 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
require
|
|
4
|
-
require
|
|
5
|
-
require
|
|
6
|
-
require
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
video_referer:,
|
|
27
|
-
ts_seg_pattern:,
|
|
28
|
-
hls_index_pattern:,
|
|
29
|
-
output_dir:
|
|
30
|
-
)
|
|
31
|
-
raise ArgumentError, "video_url must be provided" unless video_url
|
|
32
|
-
raise ArgumentError, "hls_url must be provided" unless hls_url
|
|
33
|
-
raise ArgumentError, "master_playlist_name must be provided" unless master_playlist_name
|
|
34
|
-
raise ArgumentError, "playwright_cli_path must be provided" unless playwright_cli_path
|
|
35
|
-
raise ArgumentError, "video_referer must be provided" unless video_referer
|
|
36
|
-
raise ArgumentError, "ts_seg_pattern must be provided" unless ts_seg_pattern
|
|
37
|
-
raise ArgumentError, "hls_index_pattern must be provided" unless hls_index_pattern
|
|
38
|
-
|
|
39
|
-
@video_url = video_url
|
|
40
|
-
@hls_url = hls_url
|
|
41
|
-
@master_playlist_name = master_playlist_name
|
|
42
|
-
@playwright_cli_path = playwright_cli_path
|
|
43
|
-
@video_referer = video_referer
|
|
44
|
-
@ts_seg_pattern = ts_seg_pattern
|
|
45
|
-
@hls_index_pattern = hls_index_pattern
|
|
46
|
-
@max_lines = IO.console.winsize[0]
|
|
47
|
-
@author_name = author_name
|
|
48
|
-
@video_name = if @author_name then "#{@author_name}_#{video_name}" else nil end
|
|
49
|
-
@output_dir = output_dir || "./"
|
|
50
|
-
end
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'playwright'
|
|
4
|
+
require 'tty-spinner'
|
|
5
|
+
require 'open3'
|
|
6
|
+
require 'm3u8'
|
|
7
|
+
require 'io/console'
|
|
8
|
+
|
|
9
|
+
module Vidload
|
|
10
|
+
module Mp2t
|
|
11
|
+
module Api
|
|
12
|
+
DEMUXER_PATH = "#{__dir__}/remuxer.sh"
|
|
13
|
+
VIDEO_DOWNLOADED_EVENT_QUEUE = Queue.new
|
|
14
|
+
VIDEO_INDEX_EVENT_QUEUE = Queue.new
|
|
15
|
+
ANSI_BOLD_WHITE = "\033[1;97m"
|
|
16
|
+
ANSI_LIGHT_GREY = "\033[37m"
|
|
17
|
+
ANSI_RESET = "\033[0m"
|
|
18
|
+
|
|
19
|
+
class DownloaderBuilder
|
|
20
|
+
REQUIRED_ARGS = %i[video_url hls_url master_playlist_name playwright_cli_path video_referer
|
|
21
|
+
ts_seg_pattern hls_index_pattern].freeze
|
|
22
|
+
|
|
23
|
+
def initialize
|
|
24
|
+
@kwargs = {}
|
|
25
|
+
end
|
|
51
26
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
hls_url: ARGV[2],
|
|
57
|
-
master_playlist_name: ARGV[3],
|
|
58
|
-
playwright_cli_path: ARGV[4],
|
|
59
|
-
video_referer: ARGV[5],
|
|
60
|
-
ts_seg_pattern: ARGV[6],
|
|
61
|
-
hls_index_pattern: ARGV[7],
|
|
62
|
-
author_name: ARGV[8],
|
|
63
|
-
)
|
|
64
|
-
end
|
|
27
|
+
def with_kwargs(**kwargs)
|
|
28
|
+
@kwargs = kwargs
|
|
29
|
+
self
|
|
30
|
+
end
|
|
65
31
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
video_name: hash[:video_name],
|
|
71
|
-
hls_url: hash[:hls_url],
|
|
72
|
-
master_playlist_name: hash[:master_playlist_name],
|
|
73
|
-
playwright_cli_path: hash[:playwright_cli_path],
|
|
74
|
-
video_referer: hash[:video_referer],
|
|
75
|
-
ts_seg_pattern: hash[:ts_seg_pattern],
|
|
76
|
-
hls_index_pattern: hash[:hls_index_pattern],
|
|
77
|
-
)
|
|
78
|
-
end
|
|
32
|
+
def with_video_url(video_url)
|
|
33
|
+
@kwargs[:video_url] = video_url
|
|
34
|
+
self
|
|
35
|
+
end
|
|
79
36
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
page = browser.new_page
|
|
37
|
+
def with_video_name(video_name)
|
|
38
|
+
@kwargs[:video_name] = video_name
|
|
39
|
+
self
|
|
40
|
+
end
|
|
85
41
|
|
|
86
|
-
|
|
87
|
-
|
|
42
|
+
def with_author_name(author_name)
|
|
43
|
+
@kwargs[:author_name] = author_name
|
|
44
|
+
self
|
|
45
|
+
end
|
|
88
46
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
47
|
+
def with_hls_url(hls_url)
|
|
48
|
+
@kwargs[:hls_url] = hls_url
|
|
49
|
+
self
|
|
50
|
+
end
|
|
92
51
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
puts "\tvideo_url=#{@video_url}"
|
|
98
|
-
puts "\tvideo_name=#{@video_name}"
|
|
99
|
-
puts "\thls_url=#{@hls_url}"
|
|
100
|
-
puts "\tmaster_playlist_name=#{@master_playlist_name}"
|
|
101
|
-
puts "\tplaywright_cli_path=#{@playwright_cli_path}"
|
|
102
|
-
puts "\tvideo_referer=#{@video_referer}"
|
|
103
|
-
puts "\tts_seg_pattern=#{@ts_seg_pattern}"
|
|
104
|
-
puts "\thls_index_pattern=#{@hls_index_pattern}"
|
|
105
|
-
puts "\tauthor_name=#{@author_name}"
|
|
106
|
-
end
|
|
52
|
+
def with_master_playlist_name(master_playlist_name)
|
|
53
|
+
@kwargs[:master_playlist_name] = master_playlist_name
|
|
54
|
+
self
|
|
55
|
+
end
|
|
107
56
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
spinner.success("(done)")
|
|
113
|
-
end
|
|
57
|
+
def with_playwright_cli_path(playwright_cli_path)
|
|
58
|
+
@kwargs[:playwright_cli_path] = playwright_cli_path
|
|
59
|
+
self
|
|
60
|
+
end
|
|
114
61
|
|
|
115
|
-
|
|
62
|
+
def with_video_referer(video_referer)
|
|
63
|
+
@kwargs[:video_referer] = video_referer
|
|
64
|
+
self
|
|
65
|
+
end
|
|
116
66
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
@lines = [""] * @max_lines
|
|
121
|
-
page.on("response", -> (resp) { listen_to_video_starts(resp) })
|
|
122
|
-
navigate_to_url(@video_url, page)
|
|
123
|
-
video_starter_callbacks.each do |callback|
|
|
124
|
-
res = callback.call(page)
|
|
125
|
-
if !@video_name && res[:video_name]
|
|
126
|
-
@video_name = res[:video_name]
|
|
67
|
+
def with_ts_seg_pattern(ts_seg_pattern)
|
|
68
|
+
@kwargs[:ts_seg_pattern] = ts_seg_pattern
|
|
69
|
+
self
|
|
127
70
|
end
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
@
|
|
71
|
+
|
|
72
|
+
def with_hls_index_pattern(hls_index_pattern)
|
|
73
|
+
@kwargs[:hls_index_pattern] = hls_index_pattern
|
|
74
|
+
self
|
|
131
75
|
end
|
|
132
|
-
end
|
|
133
|
-
end
|
|
134
76
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
77
|
+
def with_output_dir(output_dir)
|
|
78
|
+
@kwargs[:output_dir] = output_dir
|
|
79
|
+
self
|
|
80
|
+
end
|
|
138
81
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
82
|
+
def build
|
|
83
|
+
REQUIRED_ARGS.each do |required_arg|
|
|
84
|
+
raise ArgumentError, "#{required_arg} must be provided" unless @kwargs[required_arg]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@kwargs[:output_dir] = './' unless @kwargs[:output_dir]
|
|
88
|
+
@kwargs[:video_name] = "#{@kwargs[:author_name]}_#{@kwargs[:video_name]}" if @kwargs[:author_name]
|
|
89
|
+
|
|
90
|
+
Downloader.new(**@kwargs)
|
|
146
91
|
end
|
|
147
92
|
end
|
|
148
|
-
print "\r\e[2K"
|
|
149
|
-
puts "✔ Video downloaded successfully! Available in #{@output_dir}#{@video_name}.mp4"
|
|
150
|
-
VIDEO_DOWNLOADED_EVENT_QUEUE << true
|
|
151
|
-
end
|
|
152
93
|
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
match = last_item.match(/#{@ts_seg_pattern}/i)
|
|
159
|
-
@seg_qty = match[:seg_nb].to_i
|
|
94
|
+
class Downloader
|
|
95
|
+
def initialize(**kwargs)
|
|
96
|
+
@max_lines = IO.console.winsize[0]
|
|
97
|
+
@kwargs = kwargs
|
|
98
|
+
end
|
|
160
99
|
|
|
161
|
-
|
|
162
|
-
|
|
100
|
+
def self.builder
|
|
101
|
+
DownloaderBuilder.new
|
|
163
102
|
end
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
else
|
|
168
|
-
@pending_hls_response = response
|
|
103
|
+
|
|
104
|
+
def self.from_hash(hash)
|
|
105
|
+
builder.with_kwargs(**hash).build
|
|
169
106
|
end
|
|
170
|
-
end
|
|
171
|
-
end
|
|
172
107
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
108
|
+
# main func to be called in your own scripts defined under web/
|
|
109
|
+
def download_video(video_starter_callbacks: [])
|
|
110
|
+
Playwright.create(playwright_cli_executable_path: @kwargs[:playwright_cli_path]) do |playwright|
|
|
111
|
+
browser = playwright.chromium.launch
|
|
112
|
+
page = browser.new_page
|
|
178
113
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
114
|
+
manage_video_download(page, *video_starter_callbacks)
|
|
115
|
+
wait_until_video_downloaded
|
|
116
|
+
|
|
117
|
+
browser.close
|
|
118
|
+
end
|
|
183
119
|
end
|
|
184
|
-
end
|
|
185
|
-
end
|
|
186
120
|
|
|
187
|
-
|
|
188
|
-
|
|
121
|
+
def display_calling_args
|
|
122
|
+
puts 'Constants:'
|
|
123
|
+
puts "\tDEMUXER_PATH=#{DEMUXER_PATH}"
|
|
124
|
+
puts 'Called with:'
|
|
125
|
+
@kwargs.each do |key, value|
|
|
126
|
+
puts "\t#{key}=#{value}"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
189
129
|
|
|
190
|
-
|
|
191
|
-
|
|
130
|
+
def self.display_with_spinner(loading_msg = 'Loading...')
|
|
131
|
+
spinner = TTY::Spinner.new("[:spinner] #{loading_msg}")
|
|
132
|
+
spinner.auto_spin
|
|
133
|
+
yield
|
|
134
|
+
spinner.success('(done)')
|
|
135
|
+
end
|
|
192
136
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def manage_video_download(page, *video_starter_callbacks)
|
|
140
|
+
@seg_qty = nil
|
|
141
|
+
@pending_hls_response = nil
|
|
142
|
+
@lines = [''] * @max_lines
|
|
143
|
+
page.on('response', ->(resp) { listen_to_video_starts(resp) })
|
|
144
|
+
navigate_to_url(@kwargs[:video_url], page)
|
|
145
|
+
video_starter_callbacks.each do |callback|
|
|
146
|
+
res = callback.call(page)
|
|
147
|
+
@kwargs[:video_name] = res[:video_name] if !@kwargs[:video_name] && res[:video_name]
|
|
148
|
+
if !@kwargs[:author_name] && res[:author_name]
|
|
149
|
+
@kwargs[:author_name] = res[:author_name]
|
|
150
|
+
@kwargs[:video_name] = "#{@kwargs[:author_name]}_#{@kwargs[:video_name]}"
|
|
151
|
+
end
|
|
152
|
+
end
|
|
199
153
|
end
|
|
200
|
-
end
|
|
201
|
-
end
|
|
202
154
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
redraw_lines()
|
|
207
|
-
end
|
|
155
|
+
def wait_until_video_downloaded
|
|
156
|
+
VIDEO_DOWNLOADED_EVENT_QUEUE.pop
|
|
157
|
+
end
|
|
208
158
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
159
|
+
def trigger_video_download(video_url, seg_qty)
|
|
160
|
+
puts 'Video starts. Starting download...'
|
|
161
|
+
run_cmd(DEMUXER_PATH, video_url, "#{@kwargs[:output_dir]}#{@kwargs[:video_name]}",
|
|
162
|
+
@kwargs[:video_referer]) do |line|
|
|
163
|
+
if (line.include?('hls @') || line.include?('https @')) && line.match?(/#{@kwargs[:ts_seg_pattern]}/i)
|
|
164
|
+
seg_nb = line.match(/#{@kwargs[:ts_seg_pattern]}/i)[:seg_nb]
|
|
165
|
+
add_line(line)
|
|
166
|
+
progress_bar(seg_nb, seg_qty)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
print "\r\e[2K"
|
|
170
|
+
puts "✔ Video downloaded successfully! Available in #{@kwargs[:output_dir]}#{@kwargs[:video_name]}.mp4"
|
|
171
|
+
VIDEO_DOWNLOADED_EVENT_QUEUE << true
|
|
172
|
+
end
|
|
213
173
|
|
|
214
|
-
|
|
215
|
-
|
|
174
|
+
def listen_to_video_starts(response)
|
|
175
|
+
if response.url.start_with?(@kwargs[:hls_url]) && response.url.match?(/#{@kwargs[:hls_index_pattern]}/i)
|
|
176
|
+
body = response.text
|
|
177
|
+
playlist = M3u8::Playlist.read(body)
|
|
178
|
+
last_item = playlist.items.last.segment
|
|
179
|
+
match = last_item.match(/#{@kwargs[:ts_seg_pattern]}/i)
|
|
180
|
+
@seg_qty = match[:seg_nb].to_i
|
|
181
|
+
|
|
182
|
+
trigger_video_download(@pending_hls_response.url, @seg_qty) if @pending_hls_response
|
|
183
|
+
elsif response.url.start_with?(@kwargs[:hls_url]) && response.url.include?(@kwargs[:master_playlist_name])
|
|
184
|
+
if @seg_qty
|
|
185
|
+
trigger_video_download(response.url, @seg_qty)
|
|
186
|
+
else
|
|
187
|
+
@pending_hls_response = response
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
216
191
|
|
|
217
|
-
|
|
192
|
+
def navigate_to_url(url, page)
|
|
193
|
+
Downloader.display_with_spinner("Page #{url} loading...") do
|
|
194
|
+
page.goto(url)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def run_cmd(*cmd, &block)
|
|
199
|
+
Open3.popen2e(*cmd) do |_stdin, stdout_and_stderr, _wait_thr|
|
|
200
|
+
stdout_and_stderr.each_line(&block)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def redraw_lines
|
|
205
|
+
return if @lines.empty?
|
|
206
|
+
|
|
207
|
+
printf "\e[H"
|
|
208
|
+
printf "\e[0J"
|
|
209
|
+
|
|
210
|
+
_rows, cols = IO.console.winsize
|
|
211
|
+
@lines.each do |line|
|
|
212
|
+
if line.length > cols
|
|
213
|
+
puts "#{line.slice(0, cols - 3)}..."
|
|
214
|
+
else
|
|
215
|
+
puts line
|
|
216
|
+
end
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def add_line(line)
|
|
221
|
+
@lines << line
|
|
222
|
+
@lines.shift if @lines.size > @max_lines
|
|
223
|
+
redraw_lines
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def progress_bar(current, total, width: 40)
|
|
227
|
+
ratio = current.to_f / total
|
|
228
|
+
filled = (ratio * width).round
|
|
229
|
+
empty = width - filled
|
|
230
|
+
|
|
231
|
+
bar = '█' * filled + '░' * empty
|
|
232
|
+
percent = (ratio * 100).round(1)
|
|
233
|
+
|
|
234
|
+
print "\r[#{bar}] #{percent}% (#{current}/#{total})"
|
|
235
|
+
end
|
|
236
|
+
end
|
|
218
237
|
end
|
|
219
238
|
end
|
|
220
239
|
end
|
data/lib/vidload/mp2t.rb
CHANGED
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'playwright'
|
|
4
|
+
require 'open3'
|
|
5
|
+
require 'io/console'
|
|
6
|
+
|
|
7
|
+
module Vidload
|
|
8
|
+
module Mp4
|
|
9
|
+
module Api
|
|
10
|
+
VIDEO_DOWNLOADED_EVENT_QUEUE = Queue.new
|
|
11
|
+
|
|
12
|
+
class DownloaderBuilder
|
|
13
|
+
REQUIRED_ARGS = %i[video_url video_hub_url playwright_cli_path].freeze
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@kwargs = {}
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_kwargs(**kwargs)
|
|
20
|
+
@kwargs = kwargs
|
|
21
|
+
self
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def with_video_name(video_name)
|
|
25
|
+
@kwargs[:video_name] = video_name
|
|
26
|
+
self
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def with_video_url(video_url)
|
|
30
|
+
@kwargs[:video_url] = video_url
|
|
31
|
+
self
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def with_video_hub_url(video_hub_url)
|
|
35
|
+
@kwargs[:video_hub_url] = video_hub_url
|
|
36
|
+
self
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def with_playwright_cli_path(playwright_cli_path)
|
|
40
|
+
@kwargs[:playwright_cli_path] = playwright_cli_path
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def build
|
|
45
|
+
REQUIRED_ARGS.each do |required_arg|
|
|
46
|
+
raise ArgumentError, "#{required_arg} must be provided" unless @kwargs[required_arg]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
@kwargs[:video_name] = @kwargs[:video_url].split('/').last unless @kwargs[:video_name]
|
|
50
|
+
|
|
51
|
+
Downloader.new(**@kwargs)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
class Downloader
|
|
56
|
+
def initialize(**kwargs)
|
|
57
|
+
@max_lines = IO.console.winsize[0]
|
|
58
|
+
@kwargs = kwargs
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.builder
|
|
62
|
+
DownloaderBuilder.new
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.from_hash(hash)
|
|
66
|
+
builder.with_kwargs(**hash).build
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# main func to be called in your own scripts defined under web/
|
|
70
|
+
def download_video
|
|
71
|
+
Playwright.create(playwright_cli_executable_path: @kwargs[:playwright_cli_path]) do |playwright|
|
|
72
|
+
browser = playwright.chromium.launch
|
|
73
|
+
page = browser.new_page
|
|
74
|
+
|
|
75
|
+
manage_video_download(page)
|
|
76
|
+
wait_until_video_downloaded
|
|
77
|
+
|
|
78
|
+
browser.close
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def display_calling_args
|
|
83
|
+
puts 'Called with:'
|
|
84
|
+
@kwargs.each do |key, value|
|
|
85
|
+
puts "\t#{key}=#{value}"
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.display_with_spinner(loading_msg = 'Loading...')
|
|
90
|
+
spinner = TTY::Spinner.new("[:spinner] #{loading_msg}")
|
|
91
|
+
spinner.auto_spin
|
|
92
|
+
yield
|
|
93
|
+
spinner.success('(done)')
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
private
|
|
97
|
+
|
|
98
|
+
def manage_video_download(page)
|
|
99
|
+
@lines = [''] * @max_lines
|
|
100
|
+
page.on('response', ->(resp) { listen_to_video_starts(page, resp) })
|
|
101
|
+
navigate_to_url(@kwargs[:video_url], page)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def wait_until_video_downloaded
|
|
105
|
+
Downloader.display_with_spinner('Downloading mp4 video...') do
|
|
106
|
+
VIDEO_DOWNLOADED_EVENT_QUEUE.pop
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def listen_to_video_starts(_page, response)
|
|
111
|
+
unless response.url.start_with?(@kwargs[:video_hub_url]) && response.headers['content-type']&.include?('video/mp4')
|
|
112
|
+
return
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
body = response.text
|
|
116
|
+
File.open("video-#{@kwargs[:video_name]}.mp4", 'wb') do |f|
|
|
117
|
+
f.write(body)
|
|
118
|
+
end
|
|
119
|
+
VIDEO_DOWNLOADED_EVENT_QUEUE << true
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def navigate_to_url(url, page)
|
|
123
|
+
Downloader.display_with_spinner("Page #{url} loading...") do
|
|
124
|
+
page.goto(url)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
data/lib/vidload/mp4.rb
ADDED
data/lib/vidload/version.rb
CHANGED
data/lib/vidload.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module Vidload
|
|
2
|
-
require_relative
|
|
3
|
-
require_relative
|
|
4
|
-
require_relative
|
|
4
|
+
require_relative 'vidload/version'
|
|
5
|
+
require_relative 'vidload/mp2t'
|
|
6
|
+
require_relative 'vidload/mp4'
|
|
7
|
+
require_relative 'vidload/cli'
|
|
5
8
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vidload
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Patacode <pata.codegineer@gmail.com>
|
|
@@ -10,6 +10,20 @@ bindir: bin
|
|
|
10
10
|
cert_chain: []
|
|
11
11
|
date: 2026-03-13 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: m3u8
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.8'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.8'
|
|
13
27
|
- !ruby/object:Gem::Dependency
|
|
14
28
|
name: playwright-ruby-client
|
|
15
29
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -25,61 +39,61 @@ dependencies:
|
|
|
25
39
|
- !ruby/object:Gem::Version
|
|
26
40
|
version: '1.58'
|
|
27
41
|
- !ruby/object:Gem::Dependency
|
|
28
|
-
name:
|
|
42
|
+
name: thor
|
|
29
43
|
requirement: !ruby/object:Gem::Requirement
|
|
30
44
|
requirements:
|
|
31
45
|
- - "~>"
|
|
32
46
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: '
|
|
47
|
+
version: '1.5'
|
|
34
48
|
type: :runtime
|
|
35
49
|
prerelease: false
|
|
36
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
51
|
requirements:
|
|
38
52
|
- - "~>"
|
|
39
53
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: '
|
|
54
|
+
version: '1.5'
|
|
41
55
|
- !ruby/object:Gem::Dependency
|
|
42
|
-
name:
|
|
56
|
+
name: tty-spinner
|
|
43
57
|
requirement: !ruby/object:Gem::Requirement
|
|
44
58
|
requirements:
|
|
45
59
|
- - "~>"
|
|
46
60
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: '
|
|
61
|
+
version: '0.9'
|
|
48
62
|
type: :runtime
|
|
49
63
|
prerelease: false
|
|
50
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
65
|
requirements:
|
|
52
66
|
- - "~>"
|
|
53
67
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: '
|
|
68
|
+
version: '0.9'
|
|
55
69
|
- !ruby/object:Gem::Dependency
|
|
56
|
-
name:
|
|
70
|
+
name: rake
|
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
|
58
72
|
requirements:
|
|
59
73
|
- - "~>"
|
|
60
74
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: '
|
|
62
|
-
type: :
|
|
75
|
+
version: '13.3'
|
|
76
|
+
type: :development
|
|
63
77
|
prerelease: false
|
|
64
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
79
|
requirements:
|
|
66
80
|
- - "~>"
|
|
67
81
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: '
|
|
82
|
+
version: '13.3'
|
|
69
83
|
- !ruby/object:Gem::Dependency
|
|
70
|
-
name:
|
|
84
|
+
name: rubocop
|
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
|
72
86
|
requirements:
|
|
73
|
-
- - "
|
|
87
|
+
- - "~>"
|
|
74
88
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: '
|
|
89
|
+
version: '1.85'
|
|
76
90
|
type: :development
|
|
77
91
|
prerelease: false
|
|
78
92
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
93
|
requirements:
|
|
80
|
-
- - "
|
|
94
|
+
- - "~>"
|
|
81
95
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: '
|
|
96
|
+
version: '1.85'
|
|
83
97
|
description:
|
|
84
98
|
email:
|
|
85
99
|
executables: []
|
|
@@ -91,6 +105,8 @@ files:
|
|
|
91
105
|
- lib/vidload/mp2t.rb
|
|
92
106
|
- lib/vidload/mp2t/api.rb
|
|
93
107
|
- lib/vidload/mp2t/remuxer.sh
|
|
108
|
+
- lib/vidload/mp4.rb
|
|
109
|
+
- lib/vidload/mp4/api.rb
|
|
94
110
|
- lib/vidload/version.rb
|
|
95
111
|
homepage:
|
|
96
112
|
licenses: []
|