ffmprb 0.11.3 → 0.12.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (184) hide show
  1. checksums.yaml +5 -5
  2. data/Dockerfile +18 -0
  3. data/Gemfile +8 -1
  4. data/Gemfile.lock +121 -0
  5. data/README.md +65 -21
  6. data/TODO.md +0 -0
  7. data/bin/dev +12 -0
  8. data/bin/test +13 -0
  9. data/coverage/assets/0.10.0/application.css +799 -0
  10. data/coverage/assets/0.10.0/application.js +1707 -0
  11. data/coverage/assets/0.10.0/colorbox/border.png +0 -0
  12. data/coverage/assets/0.10.0/colorbox/controls.png +0 -0
  13. data/coverage/assets/0.10.0/colorbox/loading.gif +0 -0
  14. data/coverage/assets/0.10.0/colorbox/loading_background.png +0 -0
  15. data/coverage/assets/0.10.0/favicon_green.png +0 -0
  16. data/coverage/assets/0.10.0/favicon_red.png +0 -0
  17. data/coverage/assets/0.10.0/favicon_yellow.png +0 -0
  18. data/coverage/assets/0.10.0/loading.gif +0 -0
  19. data/coverage/assets/0.10.0/magnify.png +0 -0
  20. data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  21. data/coverage/assets/0.10.0/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  22. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  23. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  24. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  25. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  26. data/coverage/assets/0.10.0/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  27. data/coverage/assets/0.10.0/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  28. data/coverage/assets/0.10.0/smoothness/images/ui-icons_222222_256x240.png +0 -0
  29. data/coverage/assets/0.10.0/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  30. data/coverage/assets/0.10.0/smoothness/images/ui-icons_454545_256x240.png +0 -0
  31. data/coverage/assets/0.10.0/smoothness/images/ui-icons_888888_256x240.png +0 -0
  32. data/coverage/assets/0.10.0/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  33. data/coverage/assets/0.10.2/application.css +799 -0
  34. data/coverage/assets/0.10.2/application.js +1707 -0
  35. data/coverage/assets/0.10.2/colorbox/border.png +0 -0
  36. data/coverage/assets/0.10.2/colorbox/controls.png +0 -0
  37. data/coverage/assets/0.10.2/colorbox/loading.gif +0 -0
  38. data/coverage/assets/0.10.2/colorbox/loading_background.png +0 -0
  39. data/coverage/assets/0.10.2/favicon_green.png +0 -0
  40. data/coverage/assets/0.10.2/favicon_red.png +0 -0
  41. data/coverage/assets/0.10.2/favicon_yellow.png +0 -0
  42. data/coverage/assets/0.10.2/loading.gif +0 -0
  43. data/coverage/assets/0.10.2/magnify.png +0 -0
  44. data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  45. data/coverage/assets/0.10.2/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  46. data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  47. data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  48. data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  49. data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  50. data/coverage/assets/0.10.2/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  51. data/coverage/assets/0.10.2/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  52. data/coverage/assets/0.10.2/smoothness/images/ui-icons_222222_256x240.png +0 -0
  53. data/coverage/assets/0.10.2/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  54. data/coverage/assets/0.10.2/smoothness/images/ui-icons_454545_256x240.png +0 -0
  55. data/coverage/assets/0.10.2/smoothness/images/ui-icons_888888_256x240.png +0 -0
  56. data/coverage/assets/0.10.2/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  57. data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_asc.png +0 -0
  58. data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
  59. data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_both.png +0 -0
  60. data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_desc.png +0 -0
  61. data/coverage/assets/0.12.2/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
  62. data/coverage/assets/0.12.2/application.css +1 -0
  63. data/coverage/assets/0.12.2/application.js +7 -0
  64. data/coverage/assets/0.12.2/colorbox/border.png +0 -0
  65. data/coverage/assets/0.12.2/colorbox/controls.png +0 -0
  66. data/coverage/assets/0.12.2/colorbox/loading.gif +0 -0
  67. data/coverage/assets/0.12.2/colorbox/loading_background.png +0 -0
  68. data/coverage/assets/0.12.2/favicon_green.png +0 -0
  69. data/coverage/assets/0.12.2/favicon_red.png +0 -0
  70. data/coverage/assets/0.12.2/favicon_yellow.png +0 -0
  71. data/coverage/assets/0.12.2/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  72. data/coverage/assets/0.12.2/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  73. data/coverage/assets/0.12.2/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  74. data/coverage/assets/0.12.2/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  75. data/coverage/assets/0.12.2/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  76. data/coverage/assets/0.12.2/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  77. data/coverage/assets/0.12.2/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  78. data/coverage/assets/0.12.2/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  79. data/coverage/assets/0.12.2/images/ui-icons_222222_256x240.png +0 -0
  80. data/coverage/assets/0.12.2/images/ui-icons_2e83ff_256x240.png +0 -0
  81. data/coverage/assets/0.12.2/images/ui-icons_454545_256x240.png +0 -0
  82. data/coverage/assets/0.12.2/images/ui-icons_888888_256x240.png +0 -0
  83. data/coverage/assets/0.12.2/images/ui-icons_cd0a0a_256x240.png +0 -0
  84. data/coverage/assets/0.12.2/loading.gif +0 -0
  85. data/coverage/assets/0.12.2/magnify.png +0 -0
  86. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc.png +0 -0
  87. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_asc_disabled.png +0 -0
  88. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_both.png +0 -0
  89. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc.png +0 -0
  90. data/coverage/assets/0.12.3/DataTables-1.10.20/images/sort_desc_disabled.png +0 -0
  91. data/coverage/assets/0.12.3/application.css +1 -0
  92. data/coverage/assets/0.12.3/application.js +7 -0
  93. data/coverage/assets/0.12.3/colorbox/border.png +0 -0
  94. data/coverage/assets/0.12.3/colorbox/controls.png +0 -0
  95. data/coverage/assets/0.12.3/colorbox/loading.gif +0 -0
  96. data/coverage/assets/0.12.3/colorbox/loading_background.png +0 -0
  97. data/coverage/assets/0.12.3/favicon_green.png +0 -0
  98. data/coverage/assets/0.12.3/favicon_red.png +0 -0
  99. data/coverage/assets/0.12.3/favicon_yellow.png +0 -0
  100. data/coverage/assets/0.12.3/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  101. data/coverage/assets/0.12.3/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  102. data/coverage/assets/0.12.3/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  103. data/coverage/assets/0.12.3/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  104. data/coverage/assets/0.12.3/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  105. data/coverage/assets/0.12.3/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  106. data/coverage/assets/0.12.3/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  107. data/coverage/assets/0.12.3/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  108. data/coverage/assets/0.12.3/images/ui-icons_222222_256x240.png +0 -0
  109. data/coverage/assets/0.12.3/images/ui-icons_2e83ff_256x240.png +0 -0
  110. data/coverage/assets/0.12.3/images/ui-icons_454545_256x240.png +0 -0
  111. data/coverage/assets/0.12.3/images/ui-icons_888888_256x240.png +0 -0
  112. data/coverage/assets/0.12.3/images/ui-icons_cd0a0a_256x240.png +0 -0
  113. data/coverage/assets/0.12.3/loading.gif +0 -0
  114. data/coverage/assets/0.12.3/magnify.png +0 -0
  115. data/coverage/index.html +47324 -0
  116. data/exp/EXP +7 -0
  117. data/exp/av-cut-mp4you60.ffmprb +10 -0
  118. data/exp/docker-compose.yml +9 -0
  119. data/exp/gop-cut-cat-you60 +141 -0
  120. data/exp/present/Dockerfile +13 -0
  121. data/exp/present/Gemfile +3 -0
  122. data/exp/present/Gemfile.lock +22 -0
  123. data/exp/present/bin/up-deps +8 -0
  124. data/exp/present/docker-compose.yml +21 -0
  125. data/exp/present/exp/present +10 -0
  126. data/exp/present/exp/present.rb +37 -0
  127. data/exp/run +9 -0
  128. data/exp/stitch2.ffmprb +5 -0
  129. data/exp/unzip-mp4you60 +58 -0
  130. data/exp/youtubby/Dockerfile +13 -0
  131. data/exp/youtubby/Gemfile +7 -0
  132. data/exp/youtubby/Gemfile.lock +73 -0
  133. data/exp/youtubby/README.md +21 -0
  134. data/exp/youtubby/bin/up-deps +8 -0
  135. data/exp/youtubby/docker-compose.yml +20 -0
  136. data/exp/youtubby/exp/gop-raw-cut-you-HD60 +13 -0
  137. data/exp/youtubby/exp/gop-raw-cut-you-HD60.rb +230 -0
  138. data/exp/youtubby/exp/media-upload +13 -0
  139. data/exp/youtubby/exp/tmp/CURRENT +2 -0
  140. data/exp/youtubby/google_youtube.rb +39 -0
  141. data/exp/youtubby/old-ul.py +181 -0
  142. data/exp/youtubby/old-ul.rb +87 -0
  143. data/exp/youtubby/py-Dockerfile +11 -0
  144. data/exp/youtubby/py-docker-compose.yml +13 -0
  145. data/exp/youtubby/requirements.txt +19 -0
  146. data/exp/youtubby/upload.rb +38 -0
  147. data/exp/zip-cut-mp4you60 +42 -0
  148. data/exp/zip2mp4k60 +27 -0
  149. data/ffmprb.gemspec +34 -14
  150. data/lib/defaults.rb +6 -6
  151. data/lib/ffmprb/execution.rb +1 -1
  152. data/lib/ffmprb/file/sample.rb +2 -2
  153. data/lib/ffmprb/file/threaded_buffered.rb +4 -4
  154. data/lib/ffmprb/file.rb +20 -20
  155. data/lib/ffmprb/filter.rb +100 -61
  156. data/lib/ffmprb/find_silence.rb +5 -2
  157. data/lib/ffmprb/process/input/chain_base.rb +6 -4
  158. data/lib/ffmprb/process/input/channeled.rb +1 -1
  159. data/lib/ffmprb/process/input/cropped.rb +4 -1
  160. data/lib/ffmprb/process/input/cut.rb +2 -2
  161. data/lib/ffmprb/process/input/looping.rb +11 -16
  162. data/lib/ffmprb/process/input/loud.rb +1 -1
  163. data/lib/ffmprb/process/input/paced.rb +35 -0
  164. data/lib/ffmprb/process/input/postprocessed.rb +29 -0
  165. data/lib/ffmprb/process/input/reversed.rb +29 -0
  166. data/lib/ffmprb/process/input.rb +7 -3
  167. data/lib/ffmprb/process/output.rb +45 -29
  168. data/lib/ffmprb/process.rb +8 -9
  169. data/lib/ffmprb/util/proc_vis.rb +5 -4
  170. data/lib/ffmprb/util/thread.rb +13 -11
  171. data/lib/ffmprb/util/threaded_io_buffer.rb +25 -22
  172. data/lib/ffmprb/util.rb +38 -13
  173. data/lib/ffmprb/version.rb +2 -2
  174. data/lib/ffmprb.rb +8 -5
  175. data/tmp/exp/docker-compose.yml +9 -0
  176. data/tmp/exp/src/SAM_3132.MP4 +0 -0
  177. data/tmp/ffmprb-0.11.4.gem +0 -0
  178. data/tmp/output.rb +383 -0
  179. metadata +159 -139
  180. data/.gitignore +0 -10
  181. data/.rspec +0 -4
  182. data/.ruby-version +0 -1
  183. data/.travis.yml +0 -3
  184. 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,11 @@
1
+ FROM python:3.9
2
+
3
+ WORKDIR /youtubby
4
+
5
+ COPY requirements.txt .
6
+ RUN pip install -r requirements.txt
7
+
8
+ COPY . .
9
+
10
+ # XXX
11
+ CMD python upload.py
@@ -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