viddl 0.0.2
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/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
|