ffmprb 0.11.3 → 0.12.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/Dockerfile +18 -0
- data/Gemfile +8 -1
- data/Gemfile.lock +121 -0
- data/README.md +65 -21
- data/TODO.md +0 -0
- data/bin/dev +12 -0
- data/bin/test +13 -0
- data/coverage/assets/0.10.0/application.css +799 -0
- data/coverage/assets/0.10.0/application.js +1707 -0
- data/coverage/assets/0.10.0/colorbox/border.png +0 -0
- data/coverage/assets/0.10.0/colorbox/controls.png +0 -0
- data/coverage/assets/0.10.0/colorbox/loading.gif +0 -0
- data/coverage/assets/0.10.0/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.10.0/favicon_green.png +0 -0
- data/coverage/assets/0.10.0/favicon_red.png +0 -0
- data/coverage/assets/0.10.0/favicon_yellow.png +0 -0
- data/coverage/assets/0.10.0/loading.gif +0 -0
- data/coverage/assets/0.10.0/magnify.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.10.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.10.2/application.css +799 -0
- data/coverage/assets/0.10.2/application.js +1707 -0
- data/coverage/assets/0.10.2/colorbox/border.png +0 -0
- data/coverage/assets/0.10.2/colorbox/controls.png +0 -0
- data/coverage/assets/0.10.2/colorbox/loading.gif +0 -0
- data/coverage/assets/0.10.2/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.10.2/favicon_green.png +0 -0
- data/coverage/assets/0.10.2/favicon_red.png +0 -0
- data/coverage/assets/0.10.2/favicon_yellow.png +0 -0
- data/coverage/assets/0.10.2/loading.gif +0 -0
- data/coverage/assets/0.10.2/magnify.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.10.2/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_asc.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_both.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_desc.png +0 -0
- data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
- data/coverage/assets/0.12.2/application.css +1 -0
- data/coverage/assets/0.12.2/application.js +7 -0
- data/coverage/assets/0.12.2/colorbox/border.png +0 -0
- data/coverage/assets/0.12.2/colorbox/controls.png +0 -0
- data/coverage/assets/0.12.2/colorbox/loading.gif +0 -0
- data/coverage/assets/0.12.2/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.12.2/favicon_green.png +0 -0
- data/coverage/assets/0.12.2/favicon_red.png +0 -0
- data/coverage/assets/0.12.2/favicon_yellow.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.12.2/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.12.2/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.12.2/loading.gif +0 -0
- data/coverage/assets/0.12.2/magnify.png +0 -0
- data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc.png +0 -0
- data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
- data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_both.png +0 -0
- data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc.png +0 -0
- data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
- data/coverage/assets/0.12.3/application.css +1 -0
- data/coverage/assets/0.12.3/application.js +7 -0
- data/coverage/assets/0.12.3/colorbox/border.png +0 -0
- data/coverage/assets/0.12.3/colorbox/controls.png +0 -0
- data/coverage/assets/0.12.3/colorbox/loading.gif +0 -0
- data/coverage/assets/0.12.3/colorbox/loading_background.png +0 -0
- data/coverage/assets/0.12.3/favicon_green.png +0 -0
- data/coverage/assets/0.12.3/favicon_red.png +0 -0
- data/coverage/assets/0.12.3/favicon_yellow.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_glass_75_dadada_1x400.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
- data/coverage/assets/0.12.3/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
- data/coverage/assets/0.12.3/images/ui-icons_222222_256x240.png +0 -0
- data/coverage/assets/0.12.3/images/ui-icons_2e83ff_256x240.png +0 -0
- data/coverage/assets/0.12.3/images/ui-icons_454545_256x240.png +0 -0
- data/coverage/assets/0.12.3/images/ui-icons_888888_256x240.png +0 -0
- data/coverage/assets/0.12.3/images/ui-icons_cd0a0a_256x240.png +0 -0
- data/coverage/assets/0.12.3/loading.gif +0 -0
- data/coverage/assets/0.12.3/magnify.png +0 -0
- data/coverage/index.html +47324 -0
- data/exp/EXP +7 -0
- data/exp/av-cut-mp4you60.ffmprb +10 -0
- data/exp/docker-compose.yml +9 -0
- data/exp/gop-cut-cat-you60 +141 -0
- data/exp/present/Dockerfile +13 -0
- data/exp/present/Gemfile +3 -0
- data/exp/present/Gemfile.lock +22 -0
- data/exp/present/bin/up-deps +8 -0
- data/exp/present/docker-compose.yml +21 -0
- data/exp/present/exp/present +10 -0
- data/exp/present/exp/present.rb +37 -0
- data/exp/run +9 -0
- data/exp/stitch2.ffmprb +5 -0
- data/exp/unzip-mp4you60 +58 -0
- data/exp/youtubby/Dockerfile +13 -0
- data/exp/youtubby/Gemfile +7 -0
- data/exp/youtubby/Gemfile.lock +73 -0
- data/exp/youtubby/README.md +21 -0
- data/exp/youtubby/bin/up-deps +8 -0
- data/exp/youtubby/docker-compose.yml +20 -0
- data/exp/youtubby/exp/gop-raw-cut-you-HD60 +13 -0
- data/exp/youtubby/exp/gop-raw-cut-you-HD60.rb +230 -0
- data/exp/youtubby/exp/media-upload +13 -0
- data/exp/youtubby/exp/tmp/CURRENT +2 -0
- data/exp/youtubby/google_youtube.rb +39 -0
- data/exp/youtubby/old-ul.py +181 -0
- data/exp/youtubby/old-ul.rb +87 -0
- data/exp/youtubby/py-Dockerfile +11 -0
- data/exp/youtubby/py-docker-compose.yml +13 -0
- data/exp/youtubby/requirements.txt +19 -0
- data/exp/youtubby/upload.rb +38 -0
- data/exp/zip-cut-mp4you60 +42 -0
- data/exp/zip2mp4k60 +27 -0
- data/ffmprb.gemspec +34 -14
- data/lib/defaults.rb +6 -6
- data/lib/ffmprb/execution.rb +1 -1
- data/lib/ffmprb/file/sample.rb +2 -2
- data/lib/ffmprb/file/threaded_buffered.rb +4 -4
- data/lib/ffmprb/file.rb +20 -20
- data/lib/ffmprb/filter.rb +100 -61
- data/lib/ffmprb/find_silence.rb +5 -2
- data/lib/ffmprb/process/input/chain_base.rb +6 -4
- data/lib/ffmprb/process/input/channeled.rb +1 -1
- data/lib/ffmprb/process/input/cropped.rb +4 -1
- data/lib/ffmprb/process/input/cut.rb +2 -2
- data/lib/ffmprb/process/input/looping.rb +11 -16
- data/lib/ffmprb/process/input/loud.rb +1 -1
- data/lib/ffmprb/process/input/paced.rb +35 -0
- data/lib/ffmprb/process/input/postprocessed.rb +29 -0
- data/lib/ffmprb/process/input/reversed.rb +29 -0
- data/lib/ffmprb/process/input.rb +7 -3
- data/lib/ffmprb/process/output.rb +45 -29
- data/lib/ffmprb/process.rb +8 -9
- data/lib/ffmprb/util/proc_vis.rb +5 -4
- data/lib/ffmprb/util/thread.rb +13 -11
- data/lib/ffmprb/util/threaded_io_buffer.rb +25 -22
- data/lib/ffmprb/util.rb +38 -13
- data/lib/ffmprb/version.rb +2 -2
- data/lib/ffmprb.rb +8 -5
- data/tmp/exp/docker-compose.yml +9 -0
- data/tmp/exp/src/SAM_3132.MP4 +0 -0
- data/tmp/ffmprb-0.11.4.gem +0 -0
- data/tmp/output.rb +383 -0
- metadata +159 -139
- data/.gitignore +0 -10
- data/.rspec +0 -4
- data/.ruby-version +0 -1
- data/.travis.yml +0 -3
- data/circle.yml +0 -7
@@ -0,0 +1,230 @@
|
|
1
|
+
channel = ARGV.shift || 'default'
|
2
|
+
|
3
|
+
abort "USAGE: gop-raw-cut-you-HD60.rb [CHANNEL]" unless
|
4
|
+
ARGV.empty?
|
5
|
+
|
6
|
+
MEDIA_DIR = ENV['MEDIA_DIR'] or
|
7
|
+
abort "MEDIA_DIR needed"
|
8
|
+
|
9
|
+
time_s = Time.now.strftime('%y-%m-%d-%H-%M')
|
10
|
+
title = "Topublish uploaded at #{time_s}"
|
11
|
+
|
12
|
+
require 'cgi'
|
13
|
+
|
14
|
+
WE_URI = 'http://localhost'
|
15
|
+
|
16
|
+
require_relative '../google_youtube'
|
17
|
+
|
18
|
+
youtube = google_youtube(WE_URI, channel) do |auth_url|
|
19
|
+
puts "Open the following URL in your browser and authorize the application."
|
20
|
+
puts "(you'll have to copy the code= URL param when redrircted to #{WE_URI})"
|
21
|
+
puts auth_url
|
22
|
+
puts "Paste the authorization code."
|
23
|
+
$stdin.gets.chomp
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'fileutils'
|
27
|
+
|
28
|
+
require 'ffmprb'
|
29
|
+
# Ffmprb.debug = true # XXX
|
30
|
+
Ffmprb::Util::Thread.timeout = 150
|
31
|
+
|
32
|
+
int_video_opt = {resolution: Ffmprb::HD_4K, fps: 60}
|
33
|
+
fin_video_opt = {resolution: Ffmprb::HD_1080p, fps: 60, encoder: 'libx264 -crf 31'} # XXX -preset veryslow
|
34
|
+
YOU_VIDEO_OPT = {resolution: Ffmprb::HD_4K, fps: 60, encoder: 'libx264'}
|
35
|
+
|
36
|
+
GOP_MP4_RE = /\b(GX(\d\d)(\d\d\d\d)\.MP4)\b/i
|
37
|
+
GOP_ZIP_URL_RE = %r[/zip/]i
|
38
|
+
|
39
|
+
|
40
|
+
def dura_to_sec(dura_str)
|
41
|
+
dura_str.split(':').reverse.each_with_index.reduce(0) do |sec, (ns, i)|
|
42
|
+
sec + ns.to_i*(60**i)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def out_path(name)
|
47
|
+
File.join MEDIA_DIR, "#{name}.mp4"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
FileUtils.mkdir_p (tmp_dir = File.join(MEDIA_DIR, 'gop-raw-cut-you-tmp'))
|
52
|
+
begin
|
53
|
+
Dir.chdir tmp_dir do
|
54
|
+
warn "Working in #{tmp_dir} --"
|
55
|
+
system "df -h #{tmp_dir}"
|
56
|
+
warn "\nEnter lines containing GoP media D/L URLs and cut times:\n\n"
|
57
|
+
|
58
|
+
av_src_cuts = []
|
59
|
+
dl_q = Queue.new
|
60
|
+
|
61
|
+
shots = []
|
62
|
+
fetcher = Thread.new do
|
63
|
+
while (url, cuts = dl_q.deq)
|
64
|
+
srcs = []
|
65
|
+
while srcs.empty? # NOTE sometimes (zip) D/L silently fails, see below
|
66
|
+
name =
|
67
|
+
case CGI.unescape url
|
68
|
+
when GOP_ZIP_URL_RE
|
69
|
+
'tmp.zip'
|
70
|
+
when GOP_MP4_RE
|
71
|
+
$1
|
72
|
+
else
|
73
|
+
abort "ERROR invalid URL, cannot go on"
|
74
|
+
end
|
75
|
+
abort "ERROR downloading #{url}" unless
|
76
|
+
system "curl -so #{name} '#{url}'"
|
77
|
+
if name == 'tmp.zip'
|
78
|
+
zip_lines = `unzip -o #{name}`.lines
|
79
|
+
if $?.success? # NOTE if the D/L in fact has failed, it'll be retried
|
80
|
+
zip_lines.each do |line|
|
81
|
+
if line =~ GOP_MP4_RE
|
82
|
+
srcs << $1
|
83
|
+
shots << $3
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
File.delete name
|
88
|
+
else
|
89
|
+
srcs << name
|
90
|
+
shots << $3
|
91
|
+
end
|
92
|
+
end
|
93
|
+
av_src_cuts << [
|
94
|
+
srcs.sort do |a, b|
|
95
|
+
a_m = GOP_MP4_RE.match(a)
|
96
|
+
b_m = GOP_MP4_RE.match(b)
|
97
|
+
if (fst = a_m[3] <=> b_m[3]) != 0
|
98
|
+
fst
|
99
|
+
else
|
100
|
+
a_m[2] <=> b_m[2]
|
101
|
+
end
|
102
|
+
end,
|
103
|
+
cuts
|
104
|
+
]
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
while (url_cut = gets)
|
109
|
+
url, *cut = url_cut.chomp.split(' ')
|
110
|
+
next if url.split('#')[0].empty?
|
111
|
+
|
112
|
+
last_cut = -1
|
113
|
+
cuts = cut.map do |ns|
|
114
|
+
dura_to_sec(ns).tap do |curr_cut|
|
115
|
+
abort "ERROR cut times must be ascending (look it up)" unless
|
116
|
+
curr_cut > last_cut
|
117
|
+
end
|
118
|
+
end
|
119
|
+
dl_q.enq [url, cuts]
|
120
|
+
end
|
121
|
+
dl_q.enq nil
|
122
|
+
|
123
|
+
warn "\nFetching those files..."
|
124
|
+
fetcher.join
|
125
|
+
|
126
|
+
abort "ERROR no inputs given" if
|
127
|
+
av_src_cuts.empty?
|
128
|
+
|
129
|
+
out_name = "GX-#{shots.uniq.join '-'}-#{time_s}"
|
130
|
+
you_out_path = "_you_#{out_name}.mp4"
|
131
|
+
warn "\nCut-catting to out (#{out_name}) paths + you..."
|
132
|
+
|
133
|
+
pipe_cut_threads = av_src_cuts.map do |srcs, cuts|
|
134
|
+
[
|
135
|
+
(av_pipe = Ffmprb::File.temp_fifo('.flv')),
|
136
|
+
cuts.each_slice(2).map { |from, to| {from: from, to: to} },
|
137
|
+
Thread.new do
|
138
|
+
Ffmprb.process do
|
139
|
+
output av_pipe, video: int_video_opt do
|
140
|
+
srcs.each do |src|
|
141
|
+
roll input src
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
]
|
147
|
+
end
|
148
|
+
|
149
|
+
Ffmprb::File.temp_fifo('.flv') do |av_pipe|
|
150
|
+
thr = Thread.new do
|
151
|
+
Ffmprb.process do
|
152
|
+
inp_cut_opts =
|
153
|
+
pipe_cut_threads.map do |av_pipe, cut_opts, _|
|
154
|
+
(cut_opts.empty?? [{}] : cut_opts).map do |cut_opt|
|
155
|
+
[input(av_pipe), cut_opt]
|
156
|
+
end
|
157
|
+
end.reduce :+
|
158
|
+
output av_pipe, video: YOU_VIDEO_OPT do
|
159
|
+
inp_cut_opts.each do |inp, cut_opt|
|
160
|
+
roll inp.cut cut_opt
|
161
|
+
end
|
162
|
+
end
|
163
|
+
# [Rational(1)/8, Rational(1)/4, Rational(1)/2, 1, 2, 4, 8].each do |r| # XXX
|
164
|
+
# output out_path("#{out_name}x#{r.to_f}"), video: fin_video_opt do
|
165
|
+
# inp_cut_opts.each do |inp, cut_opt|
|
166
|
+
# roll inp.cut(cut_opt).pace r
|
167
|
+
# end
|
168
|
+
# end
|
169
|
+
# end
|
170
|
+
|
171
|
+
# XXX
|
172
|
+
# output out_path("#{out_name}x-8"), video: fin_video_opt do
|
173
|
+
# inp_cut_opts.each do |inp, cut_opt|
|
174
|
+
# roll inp.cut(cut_opt).reverse.pace 8
|
175
|
+
# end
|
176
|
+
# end
|
177
|
+
# output out_path("#{out_name}x8-"), video: fin_video_opt do
|
178
|
+
# inp_cut_opts.each do |inp, cut_opt|
|
179
|
+
# roll inp.cut(cut_opt).pace(8).reverse
|
180
|
+
# end
|
181
|
+
# end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
begin
|
185
|
+
Ffmprb.process do
|
186
|
+
in1 = input(av_pipe)
|
187
|
+
output you_out_path, video: YOU_VIDEO_OPT do
|
188
|
+
roll in1.pace(16).reverse
|
189
|
+
roll in1.pp
|
190
|
+
end
|
191
|
+
end
|
192
|
+
ensure
|
193
|
+
thr.join
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
|
198
|
+
# XXX this is flawed:
|
199
|
+
# the simple threads fail because of oom docker signals (9)
|
200
|
+
# (not because of broken pipes)
|
201
|
+
pipe_cut_threads.each do |av_pipe, _, thr|
|
202
|
+
begin
|
203
|
+
thr.join
|
204
|
+
rescue
|
205
|
+
warn "WARN errors-a-happening: #{$!}"
|
206
|
+
end
|
207
|
+
av_pipe.unlink
|
208
|
+
end
|
209
|
+
|
210
|
+
metadata = {
|
211
|
+
snippet: {
|
212
|
+
title: title
|
213
|
+
},
|
214
|
+
status: {
|
215
|
+
privacy_status: 'private'
|
216
|
+
}
|
217
|
+
}
|
218
|
+
|
219
|
+
video =
|
220
|
+
youtube.insert_video('snippet,status', metadata, upload_source: you_out_path, content_type: 'video/mp4')
|
221
|
+
|
222
|
+
if video.status.upload_status == 'uploaded'
|
223
|
+
warn "OK"
|
224
|
+
else
|
225
|
+
warn "FAIL"
|
226
|
+
end
|
227
|
+
end
|
228
|
+
ensure
|
229
|
+
FileUtils.rm_r tmp_dir
|
230
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
#!/bin/bash -ex
|
2
|
+
cd "$( dirname "${BASH_SOURCE[0]}" )"/..
|
3
|
+
test "$#" -ne 2 && echo "Usage: media-upload CHANNEL MP4-MEDIA-PATH" >&2 && exit 145
|
4
|
+
|
5
|
+
COMPOSE_FILE=docker-compose.yml
|
6
|
+
COMPOSE_PROJECT_NAME=youtubby_exp
|
7
|
+
|
8
|
+
docker-compose build --force-rm
|
9
|
+
|
10
|
+
export GOOGLE_CLIENT_ID="$(< exp/.google-client-id)"
|
11
|
+
export GOOGLE_CLIENT_SECRET="$(< exp/.google-client-secret)"
|
12
|
+
|
13
|
+
docker-compose run --rm youtubby upload.rb "/media/$2" "$1"
|
@@ -0,0 +1,2 @@
|
|
1
|
+
https://api.gopro.com/media/dMBvNMe4001ON/zip/source?access_token=eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.tQ6_Rry_yXM_7_nWWBaAGiXY4Wu3qo5N-O6QvYskLFefGx_mTM_cfoGIjS5BWJYaHQE7PMp-vIHDpBETfJLskJ3aKjC9c7J9jkhutM8rvnJ_NfOapMzKcZ9Pt3vCCi7gYHWFV1yAC9EtUKJH-zRPAmSZZg_sBLhmcDs6mkiL6aXqfov_GVmlkQcyc8iuwavkUaNsac8sUy_etHVfPHkBYorMB4Z00mvETjRo5A6lMafjjaqSGkWAbIhheuAGQ994DYjc3ViatioO9T2uZXJEQ48JgzgolwV0AfmEiGq7dIzpwBUdpBMrdjUIzeMxtPVvD4r9kdnGEjNCG59Bwl9olA.yjVMGuX1AqAGlrsI.62Bv9LD9AiFdFga6D-Xt4ZfWkLg3X2cBv1mq04WmD3ESmvL-rmFsG9iVzIS9_uIdNPt0J-UBpK_lZuRMUujExKle-jbaL4Hcpp8XvL3SW1iSWekftXAoXNUMc8m-Y7scOr1JsbN8RD6cIGalLe00i98exrvw5c5Zvlom-Jm5xWK6nWvoI8PK5_E8nOgu-SAIiGUjsTnTwTRv1pTA4WJlmvwX_tyebfVIzx8FBapbmJDFGGjwD6TThvIjpL7Gb49h8xBYULo6a3EupbOb8mgzUHaLeBUWyB39ZPE2MvuWzsh9HBhli2-qupNathE86otHN9shSCcPkjAXDF_uiBJPz5LYvRPPguAmkc-TbUKb4INKsmCfu9S-pqQZ51oVO72Z7KRMnslJePHpy0UcB1GKGfLz4fajSMLseEjapdwq7Sb8QNxdnwqRIKr6ZkQlmAf5K0F0-2Zr7dwrwlgG_j13eHaqjt4BgygJAGeWv6-Xdkh69-DU5N3dg7Mk8ahgKE57ohZQL13AI6VwmMXOdu8MQoDZDfo0ltL4H8JXlcV5p1ZwLTGBc9SQ0lN1WOOt8Yr6z20iBzbFloJz6iuMU7J2GLkhjQzsg6_aLQZ97dCh5P1p5g4ZWaEZQMMPmHwf6JOJW8W78ftYXbH7dTKXMLlAyEBu2iF0CMd2hf45g4pPnWj-VB0bZ4OCcrIkEeQx7R01cebqraRPVCePsKebY6KolUSgdQ0unWCawLqqQMDebRHLWMBODJmjI6ARd9M-H48vqs9P2EfkFfSVOXwARe6pxVBGQS8JWA6NYjX9npenE-KzQt1MeIN3uA.4LBtJMFQJEQH9h5GLY8lJw 3:27 1:36:15
|
2
|
+
https://api.gopro.com/media/wEDB73n07O4kv/zip/source?access_token=eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.tQ6_Rry_yXM_7_nWWBaAGiXY4Wu3qo5N-O6QvYskLFefGx_mTM_cfoGIjS5BWJYaHQE7PMp-vIHDpBETfJLskJ3aKjC9c7J9jkhutM8rvnJ_NfOapMzKcZ9Pt3vCCi7gYHWFV1yAC9EtUKJH-zRPAmSZZg_sBLhmcDs6mkiL6aXqfov_GVmlkQcyc8iuwavkUaNsac8sUy_etHVfPHkBYorMB4Z00mvETjRo5A6lMafjjaqSGkWAbIhheuAGQ994DYjc3ViatioO9T2uZXJEQ48JgzgolwV0AfmEiGq7dIzpwBUdpBMrdjUIzeMxtPVvD4r9kdnGEjNCG59Bwl9olA.yjVMGuX1AqAGlrsI.62Bv9LD9AiFdFga6D-Xt4ZfWkLg3X2cBv1mq04WmD3ESmvL-rmFsG9iVzIS9_uIdNPt0J-UBpK_lZuRMUujExKle-jbaL4Hcpp8XvL3SW1iSWekftXAoXNUMc8m-Y7scOr1JsbN8RD6cIGalLe00i98exrvw5c5Zvlom-Jm5xWK6nWvoI8PK5_E8nOgu-SAIiGUjsTnTwTRv1pTA4WJlmvwX_tyebfVIzx8FBapbmJDFGGjwD6TThvIjpL7Gb49h8xBYULo6a3EupbOb8mgzUHaLeBUWyB39ZPE2MvuWzsh9HBhli2-qupNathE86otHN9shSCcPkjAXDF_uiBJPz5LYvRPPguAmkc-TbUKb4INKsmCfu9S-pqQZ51oVO72Z7KRMnslJePHpy0UcB1GKGfLz4fajSMLseEjapdwq7Sb8QNxdnwqRIKr6ZkQlmAf5K0F0-2Zr7dwrwlgG_j13eHaqjt4BgygJAGeWv6-Xdkh69-DU5N3dg7Mk8ahgKE57ohZQL13AI6VwmMXOdu8MQoDZDfo0ltL4H8JXlcV5p1ZwLTGBc9SQ0lN1WOOt8Yr6z20iBzbFloJz6iuMU7J2GLkhjQzsg6_aLQZ97dCh5P1p5g4ZWaEZQMMPmHwf6JOJW8W78ftYXbH7dTKXMLlAyEBu2iF0CMd2hf45g4pPnWj-VB0bZ4OCcrIkEeQx7R01cebqraRPVCePsKebY6KolUSgdQ0unWCawLqqQMDebRHLWMBODJmjI6ARd9M-H48vqs9P2EfkFfSVOXwARe6pxVBGQS8JWA6NYjX9npenE-KzQt1MeIN3uA.4LBtJMFQJEQH9h5GLY8lJw 1:06 1:59:45
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'googleauth'
|
2
|
+
require 'googleauth/stores/file_token_store'
|
3
|
+
require 'fileutils'
|
4
|
+
|
5
|
+
require 'google/apis/youtube_v3'
|
6
|
+
|
7
|
+
YOUTUBE_TIMEOUT_SEC = 12000
|
8
|
+
GOOGLE_CREDENTIAL_STORE = ENV['GOOGLE_CREDENTIAL_STORE'] or
|
9
|
+
abort "GOOGLE_CREDENTIAL_STORE needed"
|
10
|
+
GOOGLE_CLIENT_ID = ENV['GOOGLE_CLIENT_ID'] or
|
11
|
+
abort "GOOGLE_CLIENT_ID needed"
|
12
|
+
GOOGLE_CLIENT_SECRET = ENV['GOOGLE_CLIENT_SECRET'] or
|
13
|
+
abort "GOOGLE_CLIENT_SECRET needed"
|
14
|
+
|
15
|
+
def google_youtube(cb_uri, user_id, &blk)
|
16
|
+
Google::Apis::YoutubeV3::YouTubeService.new.tap do |youtube|
|
17
|
+
|
18
|
+
FileUtils.mkdir_p File.dirname GOOGLE_CREDENTIAL_STORE
|
19
|
+
|
20
|
+
authorizer = Google::Auth::UserAuthorizer.new(
|
21
|
+
Google::Auth::ClientId.new(GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET),
|
22
|
+
Google::Apis::YoutubeV3::AUTH_YOUTUBE,
|
23
|
+
Google::Auth::Stores::FileTokenStore.new(file: GOOGLE_CREDENTIAL_STORE)
|
24
|
+
)
|
25
|
+
youtube.client_options.send_timeout_sec =
|
26
|
+
youtube.client_options.open_timeout_sec =
|
27
|
+
youtube.client_options.read_timeout_sec = YOUTUBE_TIMEOUT_SEC
|
28
|
+
youtube.authorization =
|
29
|
+
if (credentials = authorizer.get_credentials(user_id))
|
30
|
+
credentials
|
31
|
+
else
|
32
|
+
fail "Internal: Needs auth'n procedure block" unless
|
33
|
+
block_given?
|
34
|
+
code = yield(authorizer.get_authorization_url base_url: cb_uri)
|
35
|
+
authorizer.get_and_store_credentials_from_code(
|
36
|
+
user_id: user_id, code: code, base_url: cb_uri)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,181 @@
|
|
1
|
+
#!/usr/bin/python
|
2
|
+
|
3
|
+
import httplib
|
4
|
+
import httplib2
|
5
|
+
import os
|
6
|
+
import random
|
7
|
+
import sys
|
8
|
+
import time
|
9
|
+
|
10
|
+
from apiclient.discovery import build
|
11
|
+
from apiclient.errors import HttpError
|
12
|
+
from apiclient.http import MediaFileUpload
|
13
|
+
from oauth2client.client import flow_from_clientsecrets
|
14
|
+
from oauth2client.file import Storage
|
15
|
+
from oauth2client.tools import argparser, run_flow
|
16
|
+
|
17
|
+
|
18
|
+
# Explicitly tell the underlying HTTP transport library not to retry, since
|
19
|
+
# we are handling retry logic ourselves.
|
20
|
+
httplib2.RETRIES = 1
|
21
|
+
|
22
|
+
# Maximum number of times to retry before giving up.
|
23
|
+
MAX_RETRIES = 10
|
24
|
+
|
25
|
+
# Always retry when these exceptions are raised.
|
26
|
+
RETRIABLE_EXCEPTIONS = (httplib2.HttpLib2Error, IOError, httplib.NotConnected,
|
27
|
+
httplib.IncompleteRead, httplib.ImproperConnectionState,
|
28
|
+
httplib.CannotSendRequest, httplib.CannotSendHeader,
|
29
|
+
httplib.ResponseNotReady, httplib.BadStatusLine)
|
30
|
+
|
31
|
+
# Always retry when an apiclient.errors.HttpError with one of these status
|
32
|
+
# codes is raised.
|
33
|
+
RETRIABLE_STATUS_CODES = [500, 502, 503, 504]
|
34
|
+
|
35
|
+
# The CLIENT_SECRETS_FILE variable specifies the name of a file that contains
|
36
|
+
# the OAuth 2.0 information for this application, including its client_id and
|
37
|
+
# client_secret. You can acquire an OAuth 2.0 client ID and client secret from
|
38
|
+
# the Google API Console at
|
39
|
+
# https://console.developers.google.com/.
|
40
|
+
# Please ensure that you have enabled the YouTube Data API for your project.
|
41
|
+
# For more information about using OAuth2 to access the YouTube Data API, see:
|
42
|
+
# https://developers.google.com/youtube/v3/guides/authentication
|
43
|
+
# For more information about the client_secrets.json file format, see:
|
44
|
+
# https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
45
|
+
CLIENT_SECRETS_FILE = ".client-sec.json"
|
46
|
+
|
47
|
+
# This OAuth 2.0 access scope allows an application to upload files to the
|
48
|
+
# authenticated user's YouTube channel, but doesn't allow other types of access.
|
49
|
+
YOUTUBE_UPLOAD_SCOPE = "https://www.googleapis.com/auth/youtube.upload"
|
50
|
+
YOUTUBE_API_SERVICE_NAME = "youtube"
|
51
|
+
YOUTUBE_API_VERSION = "v3"
|
52
|
+
|
53
|
+
# This variable defines a message to display if the CLIENT_SECRETS_FILE is
|
54
|
+
# missing.
|
55
|
+
MISSING_CLIENT_SECRETS_MESSAGE = """
|
56
|
+
WARNING: Please configure OAuth 2.0
|
57
|
+
|
58
|
+
To make this sample run you will need to populate the client_secrets.json file
|
59
|
+
found at:
|
60
|
+
|
61
|
+
%s
|
62
|
+
|
63
|
+
with information from the API Console
|
64
|
+
https://console.developers.google.com/
|
65
|
+
|
66
|
+
For more information about the client_secrets.json file format, please visit:
|
67
|
+
https://developers.google.com/api-client-library/python/guide/aaa_client_secrets
|
68
|
+
""" % os.path.abspath(os.path.join(os.path.dirname(__file__),
|
69
|
+
CLIENT_SECRETS_FILE))
|
70
|
+
|
71
|
+
VALID_PRIVACY_STATUSES = ("public", "private", "unlisted")
|
72
|
+
|
73
|
+
|
74
|
+
def get_authenticated_service(args):
|
75
|
+
flow = flow_from_clientsecrets(CLIENT_SECRETS_FILE,
|
76
|
+
scope=YOUTUBE_UPLOAD_SCOPE,
|
77
|
+
message=MISSING_CLIENT_SECRETS_MESSAGE)
|
78
|
+
|
79
|
+
storage = Storage("%s-oauth2.json" % sys.argv[0])
|
80
|
+
credentials = storage.get()
|
81
|
+
|
82
|
+
if credentials is None or credentials.invalid:
|
83
|
+
credentials = run_flow(flow, storage, args)
|
84
|
+
|
85
|
+
return build(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION,
|
86
|
+
http=credentials.authorize(httplib2.Http()))
|
87
|
+
|
88
|
+
def initialize_upload(youtube, options):
|
89
|
+
tags = None
|
90
|
+
if options.keywords:
|
91
|
+
tags = options.keywords.split(",")
|
92
|
+
|
93
|
+
body=dict(
|
94
|
+
snippet=dict(
|
95
|
+
title=options.title,
|
96
|
+
description=options.description,
|
97
|
+
tags=tags,
|
98
|
+
categoryId=options.category
|
99
|
+
),
|
100
|
+
status=dict(
|
101
|
+
privacyStatus=options.privacyStatus
|
102
|
+
)
|
103
|
+
)
|
104
|
+
|
105
|
+
# Call the API's videos.insert method to create and upload the video.
|
106
|
+
insert_request = youtube.videos().insert(
|
107
|
+
part=",".join(body.keys()),
|
108
|
+
body=body,
|
109
|
+
# The chunksize parameter specifies the size of each chunk of data, in
|
110
|
+
# bytes, that will be uploaded at a time. Set a higher value for
|
111
|
+
# reliable connections as fewer chunks lead to faster uploads. Set a lower
|
112
|
+
# value for better recovery on less reliable connections.
|
113
|
+
#
|
114
|
+
# Setting "chunksize" equal to -1 in the code below means that the entire
|
115
|
+
# file will be uploaded in a single HTTP request. (If the upload fails,
|
116
|
+
# it will still be retried where it left off.) This is usually a best
|
117
|
+
# practice, but if you're using Python older than 2.6 or if you're
|
118
|
+
# running on App Engine, you should set the chunksize to something like
|
119
|
+
# 1024 * 1024 (1 megabyte).
|
120
|
+
media_body=MediaFileUpload(options.file, chunksize=-1, resumable=True)
|
121
|
+
)
|
122
|
+
|
123
|
+
resumable_upload(insert_request)
|
124
|
+
|
125
|
+
# This method implements an exponential backoff strategy to resume a
|
126
|
+
# failed upload.
|
127
|
+
def resumable_upload(insert_request):
|
128
|
+
response = None
|
129
|
+
error = None
|
130
|
+
retry = 0
|
131
|
+
while response is None:
|
132
|
+
try:
|
133
|
+
print "Uploading file..."
|
134
|
+
status, response = insert_request.next_chunk()
|
135
|
+
if response is not None:
|
136
|
+
if 'id' in response:
|
137
|
+
print "Video id '%s' was successfully uploaded." % response['id']
|
138
|
+
else:
|
139
|
+
exit("The upload failed with an unexpected response: %s" % response)
|
140
|
+
except HttpError, e:
|
141
|
+
if e.resp.status in RETRIABLE_STATUS_CODES:
|
142
|
+
error = "A retriable HTTP error %d occurred:\n%s" % (e.resp.status,
|
143
|
+
e.content)
|
144
|
+
else:
|
145
|
+
raise
|
146
|
+
except RETRIABLE_EXCEPTIONS, e:
|
147
|
+
error = "A retriable error occurred: %s" % e
|
148
|
+
|
149
|
+
if error is not None:
|
150
|
+
print error
|
151
|
+
retry += 1
|
152
|
+
if retry > MAX_RETRIES:
|
153
|
+
exit("No longer attempting to retry.")
|
154
|
+
|
155
|
+
max_sleep = 2 ** retry
|
156
|
+
sleep_seconds = random.random() * max_sleep
|
157
|
+
print "Sleeping %f seconds and then retrying..." % sleep_seconds
|
158
|
+
time.sleep(sleep_seconds)
|
159
|
+
|
160
|
+
if __name__ == '__main__':
|
161
|
+
argparser.add_argument("--file", required=True, help="Video file to upload")
|
162
|
+
argparser.add_argument("--title", help="Video title", default="Test Title")
|
163
|
+
argparser.add_argument("--description", help="Video description",
|
164
|
+
default="Test Description")
|
165
|
+
argparser.add_argument("--category", default="22",
|
166
|
+
help="Numeric video category. " +
|
167
|
+
"See https://developers.google.com/youtube/v3/docs/videoCategories/list")
|
168
|
+
argparser.add_argument("--keywords", help="Video keywords, comma separated",
|
169
|
+
default="")
|
170
|
+
argparser.add_argument("--privacyStatus", choices=VALID_PRIVACY_STATUSES,
|
171
|
+
default=VALID_PRIVACY_STATUSES[0], help="Video privacy status.")
|
172
|
+
args = argparser.parse_args()
|
173
|
+
|
174
|
+
if not os.path.exists(args.file):
|
175
|
+
exit("Please specify a valid file using the --file= parameter.")
|
176
|
+
|
177
|
+
youtube = get_authenticated_service(args)
|
178
|
+
try:
|
179
|
+
initialize_upload(youtube, args)
|
180
|
+
except HttpError, e:
|
181
|
+
print "An HTTP error %d occurred:\n%s" % (e.resp.status, e.content)
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'google/api_client'
|
2
|
+
require 'google/api_client/client_secrets'
|
3
|
+
require 'google/api_client/auth/file_storage'
|
4
|
+
require 'google/api_client/auth/installed_app'
|
5
|
+
require 'trollop'
|
6
|
+
|
7
|
+
# A limited OAuth 2 access scope that allows for uploading files, but not other
|
8
|
+
# types of account access.
|
9
|
+
YOUTUBE_UPLOAD_SCOPE = 'https://www.googleapis.com/auth/youtube.upload'
|
10
|
+
YOUTUBE_API_SERVICE_NAME = 'youtube'
|
11
|
+
YOUTUBE_API_VERSION = 'v3'
|
12
|
+
|
13
|
+
def get_authenticated_service
|
14
|
+
client = Google::APIClient.new(
|
15
|
+
:application_name => $PROGRAM_NAME,
|
16
|
+
:application_version => '1.0.0'
|
17
|
+
)
|
18
|
+
youtube = client.discovered_api(YOUTUBE_API_SERVICE_NAME, YOUTUBE_API_VERSION)
|
19
|
+
|
20
|
+
file_storage = Google::APIClient::FileStorage.new("#{$PROGRAM_NAME}-oauth2.json")
|
21
|
+
if file_storage.authorization.nil?
|
22
|
+
client_secrets = Google::APIClient::ClientSecrets.load
|
23
|
+
flow = Google::APIClient::InstalledAppFlow.new(
|
24
|
+
:client_id => client_secrets.client_id,
|
25
|
+
:client_secret => client_secrets.client_secret,
|
26
|
+
:scope => [YOUTUBE_UPLOAD_SCOPE]
|
27
|
+
)
|
28
|
+
client.authorization = flow.authorize(file_storage)
|
29
|
+
else
|
30
|
+
client.authorization = file_storage.authorization
|
31
|
+
end
|
32
|
+
|
33
|
+
return client, youtube
|
34
|
+
end
|
35
|
+
|
36
|
+
def main
|
37
|
+
opts = Trollop::options do
|
38
|
+
opt :file, 'Video file to upload', :type => String
|
39
|
+
opt :title, 'Video title', :default => 'Test Title', :type => String
|
40
|
+
opt :description, 'Video description',
|
41
|
+
:default => 'Test Description', :type => String
|
42
|
+
opt :category_id, 'Numeric video category. See https://developers.google.com/youtube/v3/docs/videoCategories/list',
|
43
|
+
:default => 22, :type => :int
|
44
|
+
opt :keywords, 'Video keywords, comma-separated',
|
45
|
+
:default => '', :type => String
|
46
|
+
opt :privacy_status, 'Video privacy status: public, private, or unlisted',
|
47
|
+
:default => 'public', :type => String
|
48
|
+
end
|
49
|
+
|
50
|
+
if opts[:file].nil? or not File.file?(opts[:file])
|
51
|
+
Trollop::die :file, 'does not exist'
|
52
|
+
end
|
53
|
+
|
54
|
+
client, youtube = get_authenticated_service
|
55
|
+
|
56
|
+
begin
|
57
|
+
body = {
|
58
|
+
:snippet => {
|
59
|
+
:title => opts[:title],
|
60
|
+
:description => opts[:description],
|
61
|
+
:tags => opts[:keywords].split(','),
|
62
|
+
:categoryId => opts[:category_id],
|
63
|
+
},
|
64
|
+
:status => {
|
65
|
+
:privacyStatus => opts[:privacy_status]
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
videos_insert_response = client.execute!(
|
70
|
+
:api_method => youtube.videos.insert,
|
71
|
+
:body_object => body,
|
72
|
+
:media => Google::APIClient::UploadIO.new(opts[:file], 'video/*'),
|
73
|
+
:parameters => {
|
74
|
+
:uploadType => 'resumable',
|
75
|
+
:part => body.keys.join(',')
|
76
|
+
}
|
77
|
+
)
|
78
|
+
|
79
|
+
videos_insert_response.resumable_upload.send_all(client)
|
80
|
+
|
81
|
+
puts "Video id '#{videos_insert_response.data.id}' was successfully uploaded."
|
82
|
+
rescue Google::APIClient::TransmissionError => e
|
83
|
+
puts e.result.body
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
main
|
@@ -0,0 +1,13 @@
|
|
1
|
+
version: '2.1'
|
2
|
+
services:
|
3
|
+
youtubby:
|
4
|
+
build: .
|
5
|
+
tmpfs:
|
6
|
+
- /youtubby/__pycache__
|
7
|
+
volumes:
|
8
|
+
- .:/youtubby
|
9
|
+
# XXX hardcoded
|
10
|
+
- /mnt/data/comp/drive/data/master/media/tmp/exp:/exp
|
11
|
+
environment:
|
12
|
+
- XXX=xxx
|
13
|
+
entrypoint: ["python", "upload_video.py"]
|
@@ -0,0 +1,19 @@
|
|
1
|
+
cachetools==5.2.0
|
2
|
+
certifi==2022.6.15
|
3
|
+
charset-normalizer==2.1.0
|
4
|
+
google-api-core==2.8.2
|
5
|
+
google-api-python-client==2.52.0
|
6
|
+
google-auth==2.9.0
|
7
|
+
google-auth-httplib2==0.1.0
|
8
|
+
googleapis-common-protos==1.56.3
|
9
|
+
httplib2==0.20.4
|
10
|
+
idna==3.3
|
11
|
+
protobuf==4.21.2
|
12
|
+
pyasn1==0.4.8
|
13
|
+
pyasn1-modules==0.2.8
|
14
|
+
pyparsing==3.0.9
|
15
|
+
requests==2.28.1
|
16
|
+
rsa==4.8
|
17
|
+
six==1.16.0
|
18
|
+
uritemplate==4.1.1
|
19
|
+
urllib3==1.26.9
|
@@ -0,0 +1,38 @@
|
|
1
|
+
file_path = ARGV.shift
|
2
|
+
channel = ARGV.shift || 'default'
|
3
|
+
title = ARGV.shift || "Topublish uploaded at #{Time.now.strftime '%y-%m-%d-%H-%M'}"
|
4
|
+
|
5
|
+
abort "USAGE: upload.rb MP4-PATH [CHANNEL [TITLE]]" unless
|
6
|
+
file_path && ARGV.empty?
|
7
|
+
|
8
|
+
warn "Youtubbing '#{file_path}' with #{title}"
|
9
|
+
|
10
|
+
WE_URI = 'http://localhost'
|
11
|
+
|
12
|
+
require_relative 'google_youtube'
|
13
|
+
|
14
|
+
youtube = google_youtube(WE_URI, channel) do |auth_url|
|
15
|
+
puts "Open the following URL in your browser and authorize the application."
|
16
|
+
puts "(you'll have to copy the code= URL param when redrircted to #{WE_URI})"
|
17
|
+
puts auth_url
|
18
|
+
puts "Paste the authorization code."
|
19
|
+
$stdin.gets.chomp
|
20
|
+
end
|
21
|
+
|
22
|
+
metadata = {
|
23
|
+
snippet: {
|
24
|
+
title: title
|
25
|
+
},
|
26
|
+
status: {
|
27
|
+
privacy_status: 'private'
|
28
|
+
}
|
29
|
+
}
|
30
|
+
|
31
|
+
video =
|
32
|
+
youtube.insert_video('snippet,status', metadata, upload_source: file_path, content_type: 'video/mp4')
|
33
|
+
|
34
|
+
if video.status.upload_status == 'uploaded'
|
35
|
+
warn "OK"
|
36
|
+
else
|
37
|
+
warn "FAIL"
|
38
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
gem 'ffmprb'
|
4
|
+
require 'ffmprb'
|
5
|
+
|
6
|
+
int_video_opt = {resolution: '3840x2160', fps: 60}
|
7
|
+
you_video_opt = {resolution: '1920x1080', fps: 60}
|
8
|
+
|
9
|
+
if ARGV.length < 1 || ARGV.length > 3
|
10
|
+
warn "Usage: zip2mp4k60 <zip-file>"
|
11
|
+
exit 1
|
12
|
+
end
|
13
|
+
|
14
|
+
inp, from, to = ARGV
|
15
|
+
zip_path = File.expand_path(inp)
|
16
|
+
out_path = File.join(File.dirname(zip_path), "#{File.basename zip_path, '.*'}-you.mp4")
|
17
|
+
cut_opt = {}
|
18
|
+
cut_opt[:from] = from.to_i if from
|
19
|
+
cut_opt[:to] = to.to_i if to
|
20
|
+
|
21
|
+
Ffmprb::File.temp_fifo('.flv') do |av_pipe|
|
22
|
+
Thread.new do
|
23
|
+
Dir.mktmpdir do |tmp_dir|
|
24
|
+
Dir.chdir tmp_dir do
|
25
|
+
system "unzip '#{zip_path}'"
|
26
|
+
Ffmprb.process do
|
27
|
+
output av_pipe, video: int_video_opt do
|
28
|
+
Dir['*'].sort.each do |in_path|
|
29
|
+
roll input in_path
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
Ffmprb.process do
|
38
|
+
output out_path, video: you_video_opt do
|
39
|
+
roll input(av_pipe).cut cut_opt
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|