viddl 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +13 -0
- data/README.md +100 -0
- data/bin/viddl +80 -0
- data/lib/viddl.rb +19 -0
- data/lib/viddl/system.rb +38 -0
- data/lib/viddl/video.rb +23 -0
- data/lib/viddl/video/clip.rb +129 -0
- data/lib/viddl/video/clip/audio.rb +43 -0
- data/lib/viddl/video/clip/crop.rb +59 -0
- data/lib/viddl/video/clip/cut.rb +83 -0
- data/lib/viddl/video/clip/resize.rb +66 -0
- data/lib/viddl/video/download.rb +49 -0
- data/lib/viddl/video/instance.rb +62 -0
- data/spec/helper.rb +8 -0
- data/spec/system_spec.rb +77 -0
- data/spec/video/clip/audio_spec.rb +81 -0
- data/spec/video/clip/crop_spec.rb +113 -0
- data/spec/video/clip/cut_spec.rb +246 -0
- data/spec/video/clip/resize_spec.rb +136 -0
- data/spec/video/clip_spec.rb +443 -0
- data/spec/video/download_spec.rb +42 -0
- data/spec/video/instance_spec.rb +121 -0
- data/spec/video_spec.rb +24 -0
- metadata +109 -0
@@ -0,0 +1,59 @@
|
|
1
|
+
module Viddl
|
2
|
+
|
3
|
+
module Video
|
4
|
+
|
5
|
+
class Clip
|
6
|
+
|
7
|
+
module Crop
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Crop options formatted for ffmpeg
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Hash] :crop The desired crop parameters (:x, :y, :width, :height)
|
14
|
+
# @return [Hash]
|
15
|
+
def options_formatted(options = {})
|
16
|
+
result = {}
|
17
|
+
unless options[:crop].nil?
|
18
|
+
crop = {}
|
19
|
+
[:x, :y, :width, :height].each do |property|
|
20
|
+
if options[:crop][property].nil?
|
21
|
+
raise "Crop is missing required #{property} property"
|
22
|
+
else
|
23
|
+
crop[property] = options[:crop][property].to_i
|
24
|
+
end
|
25
|
+
end
|
26
|
+
result[:crop] = crop
|
27
|
+
end
|
28
|
+
result
|
29
|
+
end
|
30
|
+
|
31
|
+
# Command line options for cropping
|
32
|
+
# @param [Hash] options
|
33
|
+
# @option options [Hash] :crop The desired crop parameters (:x, :y, :width, :height)
|
34
|
+
# @return [String]
|
35
|
+
def filter_args(options = {})
|
36
|
+
unless options[:crop].nil?
|
37
|
+
crop = options[:crop]
|
38
|
+
"crop=#{crop[:width]}:#{crop[:height]}:#{crop[:x]}:#{crop[:y]}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Token added to clip filename for crop args
|
43
|
+
# @param [Hash] options
|
44
|
+
# @option options [Hash] :crop The desired crop parameters (:x, :y, :width, :height)
|
45
|
+
# @return [String, nil]
|
46
|
+
def filename_token(options = {})
|
47
|
+
unless options[:crop].nil?
|
48
|
+
crop = options[:crop]
|
49
|
+
"cx#{crop[:x]}cy#{crop[:y]}cw#{crop[:width]}ch#{crop[:height]}"
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
|
59
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
module Viddl
|
2
|
+
|
3
|
+
module Video
|
4
|
+
|
5
|
+
class Clip
|
6
|
+
|
7
|
+
module Cut
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Cut options formatted for ffmpeg
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Numeric] :start Time in the source file where the clip starts
|
14
|
+
# @option options [Numeric] :duration Duration of the clip
|
15
|
+
# @option options [Numeric] :end Time in the source file where the clip ends
|
16
|
+
# @return [Hash]
|
17
|
+
def options_formatted(options = {})
|
18
|
+
result = {}
|
19
|
+
result[:start] = options[:start]
|
20
|
+
result[:duration] = duration(options)
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
# Command line options for the given cut constraints
|
25
|
+
# @param [Hash] options
|
26
|
+
# @option options [Numeric] :start Time in the source file where the clip starts
|
27
|
+
# @option options [Numeric] :duration Duration of the clip
|
28
|
+
# @return [String, nil]
|
29
|
+
def args(options = {})
|
30
|
+
args = []
|
31
|
+
unless options[:start].nil?
|
32
|
+
args << "-ss #{options[:start]}"
|
33
|
+
end
|
34
|
+
unless options[:duration].nil?
|
35
|
+
args << "-t #{options[:duration]}"
|
36
|
+
end
|
37
|
+
args.join(" ") unless args.empty?
|
38
|
+
end
|
39
|
+
|
40
|
+
# Token added to clip filename for cut args
|
41
|
+
# @param [Hash] options
|
42
|
+
# @option options [Numeric] :start Time in the source file where the clip starts
|
43
|
+
# @option options [Numeric] :duration Duration of the clip
|
44
|
+
# @return [String, nil]
|
45
|
+
def filename_token(options = {})
|
46
|
+
if !options[:start].nil? || !options[:duration].nil?
|
47
|
+
args = ""
|
48
|
+
if !options[:start].nil?
|
49
|
+
args += "s#{options[:start]}"
|
50
|
+
end
|
51
|
+
if !options[:duration].nil?
|
52
|
+
args += "d#{options[:duration]}"
|
53
|
+
end
|
54
|
+
args
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
# Numeric duration for the given options
|
61
|
+
# @param [Hash] options
|
62
|
+
# @option options [Numeric] :duration Duration of the clip
|
63
|
+
# @option options [Numeric] :end Time in the source file where the clip ends
|
64
|
+
# @return [Numeric]
|
65
|
+
def duration(options = {})
|
66
|
+
duration = nil
|
67
|
+
if !options[:duration].nil? && !options[:end].nil?
|
68
|
+
raise "Can not use both end time and duration"
|
69
|
+
elsif !options[:duration].nil? && options[:end].nil?
|
70
|
+
duration = options[:duration]
|
71
|
+
elsif options[:duration].nil? && !options[:end].nil?
|
72
|
+
duration = options[:end] - (options[:start] || 0)
|
73
|
+
end
|
74
|
+
duration
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
|
83
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module Viddl
|
2
|
+
|
3
|
+
module Video
|
4
|
+
|
5
|
+
class Clip
|
6
|
+
|
7
|
+
module Resize
|
8
|
+
|
9
|
+
extend self
|
10
|
+
|
11
|
+
# Resize options formatted for ffmpeg
|
12
|
+
# @param [Hash] options
|
13
|
+
# @option options [Integer, String] :width The desired width to resize to
|
14
|
+
# @option options [Integer, String] :height The desired height to resize to
|
15
|
+
# @return [Hash]
|
16
|
+
def options_formatted(options = {})
|
17
|
+
result = {}
|
18
|
+
[:width, :height].each do |property|
|
19
|
+
result[property] = options[property].to_i unless options[property].nil?
|
20
|
+
end
|
21
|
+
result
|
22
|
+
end
|
23
|
+
|
24
|
+
# Command line options for resize
|
25
|
+
# @param [Hash] options
|
26
|
+
# @option options [Integer] :width The desired width to resize to
|
27
|
+
# @option options [Integer] :height The desired height to resize to
|
28
|
+
# @return [String, nil]
|
29
|
+
def filter_args(options = {})
|
30
|
+
scale = if options[:width].nil? && !options[:height].nil?
|
31
|
+
"-1:#{options[:height]}"
|
32
|
+
elsif !options[:width].nil? && options[:height].nil?
|
33
|
+
"#{options[:width]}:-1"
|
34
|
+
elsif !options[:width].nil? && !options[:height].nil?
|
35
|
+
"#{options[:width]}:#{options[:height]}"
|
36
|
+
end
|
37
|
+
unless scale.nil?
|
38
|
+
"scale=#{scale}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# Token added to clip filename for resize args
|
43
|
+
# @param [Hash] options
|
44
|
+
# @option options [Integer] :width The desired width to resize to
|
45
|
+
# @option options [Integer] :height The desired height to resize to
|
46
|
+
# @return [String, nil]
|
47
|
+
def filename_token(options = {})
|
48
|
+
if !options[:width].nil? || !options[:height].nil?
|
49
|
+
args = ""
|
50
|
+
if !options[:width].nil?
|
51
|
+
args += "w#{options[:width]}"
|
52
|
+
end
|
53
|
+
if !options[:height].nil?
|
54
|
+
args += "h#{options[:height]}"
|
55
|
+
end
|
56
|
+
args
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
module Viddl
|
2
|
+
|
3
|
+
module Video
|
4
|
+
|
5
|
+
class Download
|
6
|
+
|
7
|
+
# download is stored to /tmp before processing
|
8
|
+
TEMPDIR = "/tmp"
|
9
|
+
# download format is forced to mp4 to optimize for quickness
|
10
|
+
FORMAT_ARG = "-f 'best[ext=mp4]'"
|
11
|
+
|
12
|
+
# Download the given video
|
13
|
+
# @param [Video::Instance] video
|
14
|
+
# @param [Hash] options
|
15
|
+
# @return [Download]
|
16
|
+
def self.process(video, options = {})
|
17
|
+
download = new(video)
|
18
|
+
download.process(options)
|
19
|
+
download
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param [Video::Instance] video
|
23
|
+
def initialize(video)
|
24
|
+
@video = video
|
25
|
+
@video.download = self if @video.download.nil?
|
26
|
+
end
|
27
|
+
|
28
|
+
# Download the video file
|
29
|
+
# @param [Hash] options
|
30
|
+
# @return [Boolean]
|
31
|
+
def process(options = {})
|
32
|
+
result = Kernel.system(command_line)
|
33
|
+
raise(result.to_s) unless result
|
34
|
+
true
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Command line to download the video file
|
40
|
+
# @return [String]
|
41
|
+
def command_line
|
42
|
+
"youtube-dl #{@video.source_url} #{FORMAT_ARG} -o '#{TEMPDIR}/#{@video.id}s.%(ext)s'"
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Viddl
|
2
|
+
|
3
|
+
module Video
|
4
|
+
|
5
|
+
class Instance
|
6
|
+
|
7
|
+
attr_accessor :download
|
8
|
+
attr_reader :id, :source_url
|
9
|
+
|
10
|
+
# @param [String] url The url of the video source
|
11
|
+
def initialize(url)
|
12
|
+
@source_url = url
|
13
|
+
|
14
|
+
populate_id
|
15
|
+
end
|
16
|
+
|
17
|
+
# Cut the video source using the given options
|
18
|
+
# @param [Hash] options
|
19
|
+
# @option options [Boolean] :audio Whether to include audio
|
20
|
+
# @option options [Numeric] :start Time in the source file where the clip starts
|
21
|
+
# @option options [Numeric] :duration Duration of the clip
|
22
|
+
# @option options [Numeric] :end Time in the source file where the clip ends
|
23
|
+
# @option options [Integer, String] :width The desired width to resize to
|
24
|
+
# @option options [Integer, String] :height The desired height to resize to
|
25
|
+
# @option options [Hash] :crop The desired crop parameters (:x, :y, :width, :height)
|
26
|
+
# @return [Array<Clip>]
|
27
|
+
def create_clip(options = {})
|
28
|
+
source_filenames.map do |filename|
|
29
|
+
Clip.process(filename, options)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Download the video source
|
34
|
+
# @param [Hash] options
|
35
|
+
# @return [Download]
|
36
|
+
def process_download(options = {})
|
37
|
+
@download = Download.process(self, options)
|
38
|
+
end
|
39
|
+
|
40
|
+
# The downloaded source filenames
|
41
|
+
# @return [Array<String>]
|
42
|
+
def source_filenames
|
43
|
+
if @download.nil?
|
44
|
+
raise "File must be downloaded"
|
45
|
+
else
|
46
|
+
@source_filenames = Dir["#{Download::TEMPDIR}/#{@id}*"]
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# The video instance id
|
53
|
+
# @return [String]
|
54
|
+
def populate_id
|
55
|
+
@id = @source_url.scan(/youtube.com\/watch\?v\=(\S*)&?/).flatten.first
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
data/spec/helper.rb
ADDED
data/spec/system_spec.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe Viddl::System do
|
4
|
+
|
5
|
+
context ".validate" do
|
6
|
+
|
7
|
+
context "passes" do
|
8
|
+
|
9
|
+
it "execs command line" do
|
10
|
+
expect(Kernel).to(receive(:system).twice.and_return(true))
|
11
|
+
@result = Viddl::System.send(:validate)
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
context "fails" do
|
17
|
+
|
18
|
+
it "raises" do
|
19
|
+
expect(Kernel).to(receive(:system).and_return(nil))
|
20
|
+
expect {
|
21
|
+
Viddl::System.send(:validate)
|
22
|
+
}.to(raise_error(RuntimeError))
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
context ".validate_ffmpeg" do
|
30
|
+
|
31
|
+
context "passes" do
|
32
|
+
|
33
|
+
it "execs command line" do
|
34
|
+
expect(Kernel).to(receive(:system).and_return(true))
|
35
|
+
@result = Viddl::System.send(:validate_ffmpeg)
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
context "fails" do
|
41
|
+
|
42
|
+
it "raises" do
|
43
|
+
expect(Kernel).to(receive(:system).and_return(nil))
|
44
|
+
expect {
|
45
|
+
Viddl::System.send(:validate_ffmpeg)
|
46
|
+
}.to(raise_error(RuntimeError))
|
47
|
+
end
|
48
|
+
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
context ".validate_youtube_dl" do
|
54
|
+
|
55
|
+
context "passes" do
|
56
|
+
|
57
|
+
it "execs command line" do
|
58
|
+
expect(Kernel).to(receive(:system).and_return(true))
|
59
|
+
@result = Viddl::System.send(:validate_youtube_dl)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
context "fails" do
|
65
|
+
|
66
|
+
it "raises" do
|
67
|
+
expect(Kernel).to(receive(:system).and_return(nil))
|
68
|
+
expect {
|
69
|
+
Viddl::System.send(:validate_youtube_dl)
|
70
|
+
}.to(raise_error(RuntimeError))
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
@@ -0,0 +1,81 @@
|
|
1
|
+
require "helper"
|
2
|
+
|
3
|
+
describe Viddl::Video::Clip::Audio do
|
4
|
+
|
5
|
+
context ".options_formatted" do
|
6
|
+
|
7
|
+
context "with no options" do
|
8
|
+
|
9
|
+
it "returns audio = true" do
|
10
|
+
options = {}
|
11
|
+
opts = Viddl::Video::Clip::Audio.send(:options_formatted, options)
|
12
|
+
expect(opts).to(include(audio: true))
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
16
|
+
|
17
|
+
context "with audio = false" do
|
18
|
+
|
19
|
+
it "returns audio = false" do
|
20
|
+
options = {
|
21
|
+
audio: false
|
22
|
+
}
|
23
|
+
opts = Viddl::Video::Clip::Audio.send(:options_formatted, options)
|
24
|
+
expect(opts).to(include(audio: false))
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
context "with audio = true" do
|
30
|
+
|
31
|
+
it "returns audio = true" do
|
32
|
+
options = {
|
33
|
+
audio: true
|
34
|
+
}
|
35
|
+
opts = Viddl::Video::Clip::Audio.send(:options_formatted, options)
|
36
|
+
expect(opts).to(include(audio: true))
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
|
43
|
+
context ".args" do
|
44
|
+
|
45
|
+
context "with no options" do
|
46
|
+
|
47
|
+
it "return blank string" do
|
48
|
+
options = {}
|
49
|
+
args = Viddl::Video::Clip::Audio.send(:args, options)
|
50
|
+
expect(args).to(be_nil)
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
54
|
+
|
55
|
+
context "with audio=false" do
|
56
|
+
|
57
|
+
it "turns off audio" do
|
58
|
+
options = {
|
59
|
+
audio: false
|
60
|
+
}
|
61
|
+
args = Viddl::Video::Clip::Audio.send(:args, options)
|
62
|
+
expect(args).to(eq("-an"))
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with audio=true" do
|
68
|
+
|
69
|
+
it "returns blank string" do
|
70
|
+
options = {
|
71
|
+
audio: true
|
72
|
+
}
|
73
|
+
args = Viddl::Video::Clip::Audio.send(:args, options)
|
74
|
+
expect(args).to(be_nil)
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|