wgif 0.2.0 → 0.3.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 +4 -4
- data/.travis.yml +1 -0
- data/README.md +27 -10
- data/Rakefile +8 -3
- data/lib/wgif.rb +10 -10
- data/lib/wgif/argument_parser.rb +94 -0
- data/lib/wgif/cli.rb +31 -87
- data/lib/wgif/download_bar.rb +0 -1
- data/lib/wgif/downloader.rb +23 -27
- data/lib/wgif/exceptions.rb +8 -25
- data/lib/wgif/gif_maker.rb +3 -1
- data/lib/wgif/installer.rb +2 -3
- data/lib/wgif/uploader.rb +7 -4
- data/lib/wgif/version.rb +1 -1
- data/lib/wgif/video.rb +17 -16
- data/lib/wgif/video_cache.rb +1 -4
- data/spec/integration/empty_image_list_spec.rb +18 -0
- data/spec/spec_helper.rb +3 -0
- data/spec/unit/wgif/argument_parser_spec.rb +153 -0
- data/spec/unit/wgif/cli_spec.rb +98 -0
- data/spec/{wgif → unit/wgif}/download_bar_spec.rb +0 -0
- data/spec/{wgif → unit/wgif}/downloader_spec.rb +14 -9
- data/spec/{wgif → unit/wgif}/gif_maker_spec.rb +0 -0
- data/spec/{wgif → unit/wgif}/installer_spec.rb +45 -35
- data/spec/unit/wgif/uploader_spec.rb +48 -0
- data/spec/{wgif → unit/wgif}/video_cache_spec.rb +0 -0
- data/spec/{wgif → unit/wgif}/video_spec.rb +27 -22
- data/wgif.gemspec +22 -22
- metadata +37 -18
- data/spec/wgif/cli_spec.rb +0 -207
- data/spec/wgif/uploader_spec.rb +0 -44
data/lib/wgif/exceptions.rb
CHANGED
@@ -1,27 +1,10 @@
|
|
1
1
|
module WGif
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
end
|
11
|
-
|
12
|
-
class InvalidFramesException < Exception
|
13
|
-
end
|
14
|
-
|
15
|
-
class MissingOutputFileException < Exception
|
16
|
-
end
|
17
|
-
|
18
|
-
class VideoNotFoundException < Exception
|
19
|
-
end
|
20
|
-
|
21
|
-
class ClipEncodingException < Exception
|
22
|
-
end
|
23
|
-
|
24
|
-
class ImgurException < Exception
|
25
|
-
end
|
26
|
-
|
2
|
+
InvalidUrlException = Class.new(Exception)
|
3
|
+
InvalidTimestampException = Class.new(Exception)
|
4
|
+
InvalidDurationException = Class.new(Exception)
|
5
|
+
InvalidFramesException = Class.new(Exception)
|
6
|
+
MissingOutputFileException = Class.new(Exception)
|
7
|
+
VideoNotFoundException = Class.new(Exception)
|
8
|
+
ClipEncodingException = Class.new(Exception)
|
9
|
+
ImgurException = Class.new(Exception)
|
27
10
|
end
|
data/lib/wgif/gif_maker.rb
CHANGED
@@ -12,7 +12,9 @@ module WGif
|
|
12
12
|
|
13
13
|
def resize(image, dimensions)
|
14
14
|
image.each do |frame|
|
15
|
-
frame.change_geometry(dimensions)
|
15
|
+
frame.change_geometry(dimensions) do |cols, rows, img|
|
16
|
+
img.resize!(cols, rows)
|
17
|
+
end
|
16
18
|
end
|
17
19
|
end
|
18
20
|
end
|
data/lib/wgif/installer.rb
CHANGED
@@ -6,7 +6,7 @@ module WGif
|
|
6
6
|
|
7
7
|
def run
|
8
8
|
if dependencies_installed?
|
9
|
-
puts
|
9
|
+
puts 'All dependencies are installed. Go make a gif.'
|
10
10
|
Kernel.exit 0
|
11
11
|
end
|
12
12
|
if homebrew_installed?
|
@@ -21,7 +21,7 @@ module WGif
|
|
21
21
|
end
|
22
22
|
|
23
23
|
def dependencies_installed?
|
24
|
-
DEPENDENCIES.map {|_, binary| installed?(binary)}.inject(:&)
|
24
|
+
DEPENDENCIES.map { |_, binary| installed?(binary) }.inject(:&)
|
25
25
|
end
|
26
26
|
|
27
27
|
def homebrew_installed?
|
@@ -39,6 +39,5 @@ module WGif
|
|
39
39
|
def installed?(binary)
|
40
40
|
Kernel.system "which #{binary} > /dev/null"
|
41
41
|
end
|
42
|
-
|
43
42
|
end
|
44
43
|
end
|
data/lib/wgif/uploader.rb
CHANGED
@@ -14,10 +14,13 @@ module WGif
|
|
14
14
|
def upload(filename)
|
15
15
|
File.open(filename, 'r') do |file|
|
16
16
|
response = Typhoeus.post(UPLOAD_ENDPOINT,
|
17
|
-
body: {image: file},
|
17
|
+
body: { image: file },
|
18
18
|
headers: auth_header)
|
19
|
-
|
20
|
-
|
19
|
+
if response.success?
|
20
|
+
image_url(response)
|
21
|
+
else
|
22
|
+
fail WGif::ImgurException, error_message(response)
|
23
|
+
end
|
21
24
|
end
|
22
25
|
end
|
23
26
|
|
@@ -32,7 +35,7 @@ module WGif
|
|
32
35
|
end
|
33
36
|
|
34
37
|
def auth_header
|
35
|
-
{Authorization: "Client-ID #{@client_id}"}
|
38
|
+
{ Authorization: "Client-ID #{@client_id}" }
|
36
39
|
end
|
37
40
|
end
|
38
41
|
end
|
data/lib/wgif/version.rb
CHANGED
data/lib/wgif/video.rb
CHANGED
@@ -5,25 +5,25 @@ module WGif
|
|
5
5
|
class Video
|
6
6
|
attr_accessor :name, :clip, :logger
|
7
7
|
|
8
|
-
def initialize
|
8
|
+
def initialize(name, filepath)
|
9
9
|
@name = name
|
10
10
|
@clip = FFMPEG::Movie.new(filepath)
|
11
|
-
FileUtils.mkdir_p
|
11
|
+
FileUtils.mkdir_p '/tmp/wgif/'
|
12
12
|
@logger = Logger.new("/tmp/wgif/#{name}.log")
|
13
13
|
FFMPEG.logger = @logger
|
14
14
|
end
|
15
15
|
|
16
|
-
def trim
|
16
|
+
def trim(start_timestamp, duration)
|
17
17
|
options = {
|
18
|
-
audio_codec:
|
19
|
-
video_codec:
|
18
|
+
audio_codec: 'copy',
|
19
|
+
video_codec: 'copy',
|
20
20
|
custom: "-ss #{start_timestamp} -t 00:00:#{'%06.3f' % duration}"
|
21
21
|
}
|
22
|
-
|
22
|
+
transcode(@clip, "/tmp/wgif/#{@name}-clip.mov", options)
|
23
23
|
WGif::Video.new "#{@name}-clip", "/tmp/wgif/#{@name}-clip.mov"
|
24
24
|
end
|
25
25
|
|
26
|
-
def to_frames(options={})
|
26
|
+
def to_frames(options = {})
|
27
27
|
make_frame_dir
|
28
28
|
if options[:frames]
|
29
29
|
framerate = options[:frames] / @clip.duration
|
@@ -37,22 +37,23 @@ module WGif
|
|
37
37
|
private
|
38
38
|
|
39
39
|
def make_frame_dir
|
40
|
-
FileUtils.rm Dir.glob(
|
41
|
-
FileUtils.mkdir_p
|
40
|
+
FileUtils.rm Dir.glob('/tmp/wgif/frames/*.png')
|
41
|
+
FileUtils.mkdir_p '/tmp/wgif/frames'
|
42
42
|
end
|
43
43
|
|
44
44
|
def open_frame_dir
|
45
|
-
Dir.glob(
|
45
|
+
Dir.glob('/tmp/wgif/frames/*.png')
|
46
46
|
end
|
47
47
|
|
48
48
|
def transcode(clip, file, options)
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
raise WGif::ClipEncodingException
|
53
|
-
|
49
|
+
clip.transcode(file, options)
|
50
|
+
rescue FFMPEG::Error => error
|
51
|
+
unless error.message.include? 'no output file created'
|
52
|
+
raise WGif::ClipEncodingException
|
53
|
+
end
|
54
|
+
if error.message.include? 'Invalid data found when processing input'
|
55
|
+
raise WGif::ClipEncodingException
|
54
56
|
end
|
55
57
|
end
|
56
|
-
|
57
58
|
end
|
58
59
|
end
|
data/lib/wgif/video_cache.rb
CHANGED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'wgif/cli'
|
3
|
+
|
4
|
+
describe 'empty image list bug', integration: true do
|
5
|
+
it 'throws an empty image list error' do
|
6
|
+
args = ['https://www.youtube.com/watch?v=deFDlB8RiNg',
|
7
|
+
'fish_grease.gif',
|
8
|
+
'--start',
|
9
|
+
'00:00:13',
|
10
|
+
'-d',
|
11
|
+
'3',
|
12
|
+
'-f',
|
13
|
+
'20',
|
14
|
+
'--width',
|
15
|
+
'350']
|
16
|
+
WGif::CLI.new.make_gif(args)
|
17
|
+
end
|
18
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'wgif/argument_parser'
|
3
|
+
|
4
|
+
describe WGif::ArgumentParser do
|
5
|
+
let(:parser) { described_class.new }
|
6
|
+
|
7
|
+
it 'parses a URL from command line args' do
|
8
|
+
args = parser.parse_args ['http://example.com']
|
9
|
+
args[:url].should eq('http://example.com')
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'starts at 0s by default' do
|
13
|
+
args = parser.parse_args ['http://example.com']
|
14
|
+
args[:trim_from].should eq('00:00:00')
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'trims parserps to 1s by default' do
|
18
|
+
args = parser.parse_args ['http://example.com']
|
19
|
+
args[:duration].should eq(1)
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'parses the short frame count option' do
|
23
|
+
options = parser.parse_options ['-f', '40']
|
24
|
+
options[:frames].should eq(40)
|
25
|
+
end
|
26
|
+
|
27
|
+
it 'parses the long frame count option' do
|
28
|
+
options = parser.parse_options ['--frames', '40']
|
29
|
+
options[:frames].should eq(40)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'parses the short start time option' do
|
33
|
+
options = parser.parse_options ['-s', '00:00:05']
|
34
|
+
options[:trim_from].should eq('00:00:05')
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'parses the long start time option' do
|
38
|
+
options = parser.parse_options ['--start', '00:00:05']
|
39
|
+
options[:trim_from].should eq('00:00:05')
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'parses the short duration option' do
|
43
|
+
options = parser.parse_options ['-d', '1.43']
|
44
|
+
options[:duration].should eq(1.43)
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'parses the long duration option' do
|
48
|
+
options = parser.parse_options ['--duration', '5.3']
|
49
|
+
options[:duration].should eq(5.3)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'parses the short dimensions option' do
|
53
|
+
options = parser.parse_options ['-w', '400']
|
54
|
+
expect(options[:dimensions]).to eq('400')
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'parses the long dimensions option' do
|
58
|
+
options = parser.parse_options ['--width', '300']
|
59
|
+
expect(options[:dimensions]).to eq('300')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'parses the short upload option' do
|
63
|
+
options = parser.parse_options ['-u']
|
64
|
+
expect(options[:upload]).to eq(true)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'parses the long upload option' do
|
68
|
+
options = parser.parse_options ['--upload']
|
69
|
+
expect(options[:upload]).to eq(true)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'parses the short preview option' do
|
73
|
+
options = parser.parse_options ['-p']
|
74
|
+
expect(options[:preview]).to eq(true)
|
75
|
+
end
|
76
|
+
|
77
|
+
it 'parses the long preview option' do
|
78
|
+
options = parser.parse_options ['--preview']
|
79
|
+
expect(options[:preview]).to eq(true)
|
80
|
+
end
|
81
|
+
|
82
|
+
it 'handles args in wacky order' do
|
83
|
+
args = parser.parse_args([
|
84
|
+
'-d',
|
85
|
+
'1.5',
|
86
|
+
'--upload',
|
87
|
+
'http://example.com',
|
88
|
+
'--frames',
|
89
|
+
'60',
|
90
|
+
'-p',
|
91
|
+
'my-great-gif.gif',
|
92
|
+
'-s',
|
93
|
+
'00:00:05'
|
94
|
+
])
|
95
|
+
|
96
|
+
expect(args).to eq(url: 'http://example.com',
|
97
|
+
trim_from: '00:00:05',
|
98
|
+
duration: 1.5,
|
99
|
+
frames: 60,
|
100
|
+
output: 'my-great-gif.gif',
|
101
|
+
dimensions: '480',
|
102
|
+
upload: true,
|
103
|
+
preview: true)
|
104
|
+
end
|
105
|
+
|
106
|
+
context 'validating args' do
|
107
|
+
|
108
|
+
it 'checks for a missing output file' do
|
109
|
+
args = parser.parse_args(['http://example.com'])
|
110
|
+
expect { parser.validate_args args }
|
111
|
+
.to raise_error(WGif::MissingOutputFileException)
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'checks for an invalid URL' do
|
115
|
+
args = parser.parse_args(['crazy nonsense', 'output.gif'])
|
116
|
+
expect { parser.validate_args args }
|
117
|
+
.to raise_error(WGif::InvalidUrlException)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'checks for an invalid timestamp' do
|
121
|
+
args = parser.parse_args([
|
122
|
+
'http://lol.wut',
|
123
|
+
'output.gif',
|
124
|
+
'-s',
|
125
|
+
'rofl'
|
126
|
+
])
|
127
|
+
expect { parser.validate_args args }
|
128
|
+
.to raise_error(WGif::InvalidTimestampException)
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'returns true when args are OK' do
|
132
|
+
args = parser.parse_args([
|
133
|
+
'https://crazynonsense.info',
|
134
|
+
'output.gif'
|
135
|
+
])
|
136
|
+
expect { parser.validate_args args }.not_to raise_error
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'parses and validates' do
|
141
|
+
expect { parser.parse(['http://lol.wut']) }
|
142
|
+
.to raise_error(WGif::MissingOutputFileException)
|
143
|
+
end
|
144
|
+
|
145
|
+
it 'returns parsed arguments' do
|
146
|
+
args = parser.parse(['http://lol.wut', 'out.gif'])
|
147
|
+
expect(args).to eq({dimensions: '480',
|
148
|
+
duration: 1.0,
|
149
|
+
output: 'out.gif',
|
150
|
+
trim_from: '00:00:00',
|
151
|
+
url: 'http://lol.wut'})
|
152
|
+
end
|
153
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'wgif/cli'
|
3
|
+
|
4
|
+
describe WGif::CLI do
|
5
|
+
let(:cli) { described_class.new }
|
6
|
+
|
7
|
+
context 'error handling' do
|
8
|
+
|
9
|
+
before do
|
10
|
+
@mock_stdout = StringIO.new
|
11
|
+
@real_stdout, $stdout = $stdout, @mock_stdout
|
12
|
+
end
|
13
|
+
|
14
|
+
after do
|
15
|
+
$stdout = @real_stdout
|
16
|
+
end
|
17
|
+
|
18
|
+
def expect_help_with_message(out, message)
|
19
|
+
expect(out).to include(message)
|
20
|
+
expect(out)
|
21
|
+
.to include('Usage: wgif [YouTube URL] [output file] [options]')
|
22
|
+
cli.argument_parser.argument_summary.each do |help_info|
|
23
|
+
expect(out).to include(help_info)
|
24
|
+
end
|
25
|
+
expect(out).to include('Example:')
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'catches invalid URLs' do
|
29
|
+
WGif::ArgumentParser.any_instance.stub(:parse)
|
30
|
+
.and_raise(WGif::InvalidUrlException)
|
31
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
32
|
+
message = 'That looks like an invalid URL. Check the syntax.'
|
33
|
+
expect_help_with_message(@mock_stdout.string, message)
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'catches invalid timestamps' do
|
37
|
+
WGif::ArgumentParser.any_instance.stub(:parse)
|
38
|
+
.and_raise(WGif::InvalidTimestampException)
|
39
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
40
|
+
message = 'That looks like an invalid timestamp. Check the syntax.'
|
41
|
+
expect_help_with_message(@mock_stdout.string, message)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'catches missing output args' do
|
45
|
+
WGif::ArgumentParser.any_instance.stub(:parse)
|
46
|
+
.and_raise(WGif::MissingOutputFileException)
|
47
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
48
|
+
message = 'Please specify an output file.'
|
49
|
+
expect_help_with_message(@mock_stdout.string, message)
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'catches missing videos' do
|
53
|
+
WGif::ArgumentParser.any_instance.stub(:parse)
|
54
|
+
.and_raise(WGif::VideoNotFoundException)
|
55
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
56
|
+
message = "WGif can't find a valid YouTube video at that URL."
|
57
|
+
expect_help_with_message(@mock_stdout.string, message)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'catches encoding exceptions' do
|
61
|
+
WGif::ArgumentParser.any_instance.stub(:parse)
|
62
|
+
.and_raise(WGif::ClipEncodingException)
|
63
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
64
|
+
message = 'WGif encountered an error transcoding the video.'
|
65
|
+
expect_help_with_message(@mock_stdout.string, message)
|
66
|
+
end
|
67
|
+
|
68
|
+
it 'catches upload errors' do
|
69
|
+
WGif::ArgumentParser.any_instance.stub(:parse)
|
70
|
+
.and_raise(WGif::ImgurException, 'Imgur error')
|
71
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
72
|
+
expect_help_with_message(@mock_stdout.string, 'Imgur error')
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'raises SystemExit when thrown' do
|
76
|
+
WGif::ArgumentParser.any_instance.stub(:parse).and_raise(SystemExit)
|
77
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'Prints the backtrace for all other exceptions' do
|
81
|
+
exception = StandardError.new 'crazy error'
|
82
|
+
WGif::ArgumentParser.any_instance.stub(:parse).and_raise(exception)
|
83
|
+
expect { cli.make_gif([]) }.to raise_error(SystemExit)
|
84
|
+
message = 'Something went wrong creating your GIF. The details:'
|
85
|
+
expect_help_with_message(@mock_stdout.string, message)
|
86
|
+
expect(@mock_stdout.string).to include('Please open an issue')
|
87
|
+
expect(@mock_stdout.string).to include("#{exception}")
|
88
|
+
expect(@mock_stdout.string).to include(exception.backtrace.join("\n"))
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'prints help information' do
|
92
|
+
expect { cli.make_gif(['-h']) }.to raise_error(SystemExit)
|
93
|
+
cli.argument_parser.argument_summary.each do |help_info|
|
94
|
+
expect(@mock_stdout.string).to include(help_info)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|