audio_to_youtube 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/lib/audio_to_youtube.rb +326 -0
- metadata +44 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 222b2488455a1f11c03853d3772db05f473aab73
|
4
|
+
data.tar.gz: 3d62868b920f7e694913b8cec93eff7b6e8a9b83
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 80e4dad589ee2596966310100151262afbe3f486c767159581ebbbcb1032b76ad803851c27ecbab33f8ad7718ba6955c16ff65b602722eb2a273d2a694d3ecc0
|
7
|
+
data.tar.gz: 96eac8616a78146fb75764e391fb6e8f5045a14d07ca8d156cf16c2d64d37ac07e04e03ac3269f529c41f7b0622cc1d5e2c738e64b23d5fff65ebde65be8db9b
|
@@ -0,0 +1,326 @@
|
|
1
|
+
class AudioToYoutube
|
2
|
+
@outputFile = 'out.mp4'
|
3
|
+
@tempFolder = 'temp'
|
4
|
+
@tempBg = 'bg.jpg'
|
5
|
+
@backgroundBase = 'media/bg-720.png'
|
6
|
+
@ffmpegDir = 'setme'
|
7
|
+
@imagemagickDir = 'setme'
|
8
|
+
private
|
9
|
+
def self.exitWithError(message)
|
10
|
+
puts ('\nError: ' + message)
|
11
|
+
end
|
12
|
+
|
13
|
+
################################################################################
|
14
|
+
# Generates the background art for the video
|
15
|
+
def self.generateBackground(audio, backgroundBase, output, blur, text)
|
16
|
+
|
17
|
+
blur ||= false
|
18
|
+
text ||= ""
|
19
|
+
|
20
|
+
# extract the album art
|
21
|
+
tempArt = "#{Dir.pwd}/art.png"
|
22
|
+
cmd = ["ffmpeg"]
|
23
|
+
cmd += ['-y']
|
24
|
+
cmd += ["-i #{audio}"]
|
25
|
+
cmd += ['-acodec none']
|
26
|
+
cmd += [tempArt]
|
27
|
+
puts 'Extracting album art...'
|
28
|
+
begin
|
29
|
+
unless Dir.exist?(@tempFolder) ||
|
30
|
+
Dir.mkdir(@tempFolder)
|
31
|
+
puts p cmd.join(' ')
|
32
|
+
end
|
33
|
+
rescue
|
34
|
+
exitWithError('Couldn\'t find album art in the audio file. ' +
|
35
|
+
'Use the -background option to select an image file.')
|
36
|
+
end
|
37
|
+
|
38
|
+
# now, build the background image
|
39
|
+
cmd = ["imagick"]
|
40
|
+
cmd += [backgroundBase]
|
41
|
+
|
42
|
+
# draw the blurred album art background
|
43
|
+
if blur
|
44
|
+
cmd += ['-gravity', 'center']
|
45
|
+
cmd += ['(']
|
46
|
+
cmd += [tempArt]
|
47
|
+
cmd += ['-resize', '1500x']
|
48
|
+
cmd += ['-modulate', '90']
|
49
|
+
cmd += ['-blur', '0x20']
|
50
|
+
cmd += [')']
|
51
|
+
cmd += ['-composite']
|
52
|
+
end
|
53
|
+
|
54
|
+
# set offsets depending on whether text is drawn
|
55
|
+
gravity = 'center'
|
56
|
+
offset = []
|
57
|
+
shadowOffset = []
|
58
|
+
unless text.empty?
|
59
|
+
gravity = 'west'
|
60
|
+
offset = ['-geometry', '+60+0']
|
61
|
+
shadowOffset = ['-geometry', '+30+0']
|
62
|
+
end
|
63
|
+
|
64
|
+
# set some settings used twice
|
65
|
+
mainArtFront = ['(']
|
66
|
+
mainArtFront += [tempArt]
|
67
|
+
mainArtFront += ['-resize', 'x720']
|
68
|
+
|
69
|
+
mainArtBack = [')']
|
70
|
+
mainArtBack += ['-gravity', gravity]
|
71
|
+
mainArtBack += ['-composite']
|
72
|
+
|
73
|
+
# draw shadow of the main album art
|
74
|
+
cmd += mainArtFront
|
75
|
+
cmd += ['-background', 'black']
|
76
|
+
cmd += ['-shadow', '50x15+0+0']
|
77
|
+
cmd += shadowOffset
|
78
|
+
cmd += mainArtBack
|
79
|
+
|
80
|
+
# draw the main album art
|
81
|
+
cmd += mainArtFront
|
82
|
+
cmd += offset
|
83
|
+
cmd += mainArtBack
|
84
|
+
|
85
|
+
# add in the track metadata text
|
86
|
+
unless text.empty?
|
87
|
+
labelText = text[0]
|
88
|
+
for i in 1..text.length
|
89
|
+
labelText += '\\n\\n' + text[i]
|
90
|
+
end
|
91
|
+
|
92
|
+
# set some settings used twice
|
93
|
+
labelFront = ['(']
|
94
|
+
labelFront += ['-size', '440x400']
|
95
|
+
labelFront += ['-background', 'none']
|
96
|
+
labelFront += ['-gravity', 'center']
|
97
|
+
labelFront += ['-font', 'Segoe-UI-Semibold']
|
98
|
+
|
99
|
+
labelMid = ['caption:' + labelText]
|
100
|
+
|
101
|
+
labelBack = ['-geometry', '+25+0']
|
102
|
+
labelBack += [')']
|
103
|
+
labelBack += ['-gravity', 'east']
|
104
|
+
labelBack += ['-composite']
|
105
|
+
|
106
|
+
# draw text shadow
|
107
|
+
cmd += labelFront.join(' ')
|
108
|
+
cmd += ['-fill', '#00000090']
|
109
|
+
cmd += labelMid.join(' ')
|
110
|
+
cmd += ['-blur', '0x6']
|
111
|
+
cmd += labelBack.join(' ')
|
112
|
+
|
113
|
+
# draw text itself
|
114
|
+
cmd += labelFront.join(' ')
|
115
|
+
cmd += ['-fill', 'white']
|
116
|
+
cmd += labelMid.join(' ')
|
117
|
+
cmd += labelBack.join(' ')
|
118
|
+
|
119
|
+
end
|
120
|
+
|
121
|
+
# put it all together
|
122
|
+
cmd += [output]
|
123
|
+
puts 'Generating video background...'
|
124
|
+
begin
|
125
|
+
puts Open3.capture3(cmd.join(' '))
|
126
|
+
rescue
|
127
|
+
exitWithError('Something went wrong when generating the background.')
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
################################################################################
|
132
|
+
# Splits text over multiple lines
|
133
|
+
def self.splitText(text, maximumLength)
|
134
|
+
|
135
|
+
maximumLength ||= 20
|
136
|
+
words = text.split(' ')
|
137
|
+
output = ''
|
138
|
+
currentLine = ''
|
139
|
+
words.each do |word|
|
140
|
+
if currentLine == ''
|
141
|
+
currentLine = word
|
142
|
+
else
|
143
|
+
temp = currentLine + ' ' + word
|
144
|
+
# with next word, still under maximum length - add normally
|
145
|
+
if len(temp) < maximumLength
|
146
|
+
currentLine = temp
|
147
|
+
# next word is longer than the maximum length - it gets its own line
|
148
|
+
elsif len(word) > maximumLength
|
149
|
+
if output != ''
|
150
|
+
output += r'\n'
|
151
|
+
end
|
152
|
+
output += currentLine
|
153
|
+
currentLine = ''
|
154
|
+
if output != ''
|
155
|
+
output += r'\n'
|
156
|
+
end
|
157
|
+
output += word
|
158
|
+
# we've overrun the maximum length - start a new line with the word
|
159
|
+
else
|
160
|
+
if output != ''
|
161
|
+
output += 'r\n'
|
162
|
+
end
|
163
|
+
output += currentLine
|
164
|
+
currentLine = word
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
if output != ''
|
169
|
+
output += r'\n'
|
170
|
+
end
|
171
|
+
output += currentLine
|
172
|
+
#print(output)
|
173
|
+
return output
|
174
|
+
end
|
175
|
+
|
176
|
+
################################################################################
|
177
|
+
# Retrieves the title and artist from the file
|
178
|
+
def self.getMetadata(file)
|
179
|
+
cmd = ["ffprobe"]
|
180
|
+
cmd += ['-i', file]
|
181
|
+
print('Extracting metadata...')
|
182
|
+
lines = ""
|
183
|
+
begin
|
184
|
+
output = p cmd.join(' ')
|
185
|
+
lines = output.split('\n')
|
186
|
+
rescue
|
187
|
+
exitWithError('Couldn\'t extract metadata from the file.')
|
188
|
+
end
|
189
|
+
|
190
|
+
title = ""
|
191
|
+
artist = ""
|
192
|
+
|
193
|
+
lines.each do |line|
|
194
|
+
match = line.scan(/^\s*(.*?)\s*: (.*?)\s*@/)
|
195
|
+
if match.length > 0
|
196
|
+
pair = match[0]
|
197
|
+
|
198
|
+
if len(pair) == 2
|
199
|
+
if pair[0] == 'title' and pair[1].length > 0
|
200
|
+
if title == "" # or len(title) < len(pair[1]):
|
201
|
+
title = pair[1]
|
202
|
+
print("Found title: " + title)
|
203
|
+
end
|
204
|
+
elsif pair[0] == 'artist' and pair[1].length > 0
|
205
|
+
if artist == "" # or len(artist) < len(pair[1]):
|
206
|
+
artist = pair[1]
|
207
|
+
print("Found artist: " + artist)
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
return artist, title
|
214
|
+
end
|
215
|
+
################################################################################
|
216
|
+
# Creates the final video suitable for uploading
|
217
|
+
def self.encode(audio, background, output)
|
218
|
+
cmd = ["ffmpeg"]
|
219
|
+
cmd += ['-y']
|
220
|
+
cmd += ['-loop', '1']
|
221
|
+
cmd += ['-r', '1']
|
222
|
+
cmd += ['-i', background]
|
223
|
+
cmd += ['-i', audio]
|
224
|
+
cmd += ['-c:v', 'libx264']
|
225
|
+
cmd += ['-preset', 'veryfast']
|
226
|
+
cmd += ['-tune', 'stillimage']
|
227
|
+
cmd += ['-crf', '15']
|
228
|
+
cmd += ['-pix_fmt', 'yuv420p']
|
229
|
+
cmd += ['-strict', 'experimental']
|
230
|
+
cmd += ['-c:a', 'aac']
|
231
|
+
cmd += ['-b:a', '256k']
|
232
|
+
cmd += ['-shortest']
|
233
|
+
cmd += ['-threads', '0']
|
234
|
+
cmd += [output]
|
235
|
+
print('Encoding video...')
|
236
|
+
begin
|
237
|
+
system(cmd.join(' '))
|
238
|
+
rescue
|
239
|
+
exitWithError('Something went wrong when encoding the video.')
|
240
|
+
end
|
241
|
+
print('\nDone. Saved output as "%s".' % @outputFile)
|
242
|
+
end
|
243
|
+
|
244
|
+
################################################################################
|
245
|
+
# Set library locations
|
246
|
+
def self.setLibraryLocations()
|
247
|
+
|
248
|
+
# set FFmpeg location
|
249
|
+
if @ffmpegDir == 'setme' or @ffmpegDir == ''
|
250
|
+
@ffmpegDir = '/usr/local/bin'
|
251
|
+
end
|
252
|
+
|
253
|
+
# set ImageMagick location
|
254
|
+
if @imagemagickDir == 'setme' or @imagemagickDir == ''
|
255
|
+
@imagemagickDir = '/usr/local/bin'
|
256
|
+
end
|
257
|
+
|
258
|
+
# set full paths
|
259
|
+
ffprobe = Dir.pwd + " " + @ffmpegDir + ' ffprobe'
|
260
|
+
ffmpeg = Dir.pwd + " " + @ffmpegDir + ' ffmpeg'
|
261
|
+
imagick = Dir.pwd + " " + @imagemagickDir + ' convert'
|
262
|
+
|
263
|
+
# ensure libraries are actually installed
|
264
|
+
unless (os.path.exists(ffprobe) and os.path.exists(ffmpeg))
|
265
|
+
exitWithError("FFmpeg couldn\'t be found at \"#{@ffmpegDir}\". " +
|
266
|
+
'Please check your configuration.')
|
267
|
+
end
|
268
|
+
unless os.path.exists(imagick)
|
269
|
+
exitWithError("ImageMagick couldn\'t be found at \"#{@imagemagickDir}\". " +
|
270
|
+
'Please check your configuration.')
|
271
|
+
end
|
272
|
+
end
|
273
|
+
public
|
274
|
+
################################################################################
|
275
|
+
# Program entrypoint.
|
276
|
+
def self.generate *args
|
277
|
+
audio = nil
|
278
|
+
bg = nil
|
279
|
+
out = nil
|
280
|
+
if args.size < 1 || args.size > 3
|
281
|
+
puts 'This method takes either 1 to 3 arguments'
|
282
|
+
else
|
283
|
+
audio = args[0]
|
284
|
+
if args.size == 2
|
285
|
+
bg = args[1]
|
286
|
+
elsif args.size == 3
|
287
|
+
bg = args[1]
|
288
|
+
out = args[2]
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
bg ||= @tempBg
|
293
|
+
out ||= @outputFile
|
294
|
+
unless File.exist?(audio)
|
295
|
+
exitWithError("Input file \"#{audio}\" doesn\'t exist or can\'t be read.")
|
296
|
+
end
|
297
|
+
|
298
|
+
# determine arguments
|
299
|
+
custom = bg
|
300
|
+
doBlur = true
|
301
|
+
doText = false
|
302
|
+
|
303
|
+
background = Dir.pwd + " " + @tempFolder + " " + bg
|
304
|
+
|
305
|
+
# grab metadata if necessary
|
306
|
+
text = ""
|
307
|
+
if doText and custom == ""
|
308
|
+
metadata = getMetadata(audio)
|
309
|
+
metadata.each do |item|
|
310
|
+
if item != ""
|
311
|
+
#split = splitText(item)
|
312
|
+
text.append(item)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
# generate the background unless a premade one is specified
|
318
|
+
if custom == ""
|
319
|
+
generateBackground(audio, backgroundBase, background, blur=doBlur, text=text)
|
320
|
+
else
|
321
|
+
background = custom
|
322
|
+
end
|
323
|
+
encode(audio, background, out)
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
metadata
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: audio_to_youtube
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brian Doan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-29 00:00:00.000000000 Z
|
12
|
+
dependencies: []
|
13
|
+
description: Convert audio with background image to mp4 for uploading to youtube
|
14
|
+
email: quangkhoidoan@gmail.com
|
15
|
+
executables: []
|
16
|
+
extensions: []
|
17
|
+
extra_rdoc_files: []
|
18
|
+
files:
|
19
|
+
- lib/audio_to_youtube.rb
|
20
|
+
homepage: http://rubygems.org/gems/audio_to_youtube
|
21
|
+
licenses:
|
22
|
+
- MIT
|
23
|
+
metadata: {}
|
24
|
+
post_install_message:
|
25
|
+
rdoc_options: []
|
26
|
+
require_paths:
|
27
|
+
- lib
|
28
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
34
|
+
requirements:
|
35
|
+
- - ">="
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
requirements: []
|
39
|
+
rubyforge_project:
|
40
|
+
rubygems_version: 2.2.2
|
41
|
+
signing_key:
|
42
|
+
specification_version: 4
|
43
|
+
summary: Convert audio with background image to mp4 for uploading to youtube
|
44
|
+
test_files: []
|