wgif 0.0.1.pre

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,19 @@
1
+ require 'RMagick'
2
+
3
+ module WGif
4
+ class GifMaker
5
+ def make_gif(frames_dir, filename, dimensions)
6
+ image = Magick::ImageList.new(*frames_dir)
7
+ resize(image, dimensions)
8
+ image.coalesce
9
+ image.optimize_layers Magick::OptimizeLayer
10
+ image.write(filename)
11
+ end
12
+
13
+ def resize(image, dimensions)
14
+ image.each do |frame|
15
+ frame.change_geometry(dimensions) { |cols, rows, img| img.resize!(cols, rows) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,34 @@
1
+ module WGif
2
+ class Installer
3
+
4
+ DEPENDENCIES = [['ffmpeg', 'ffmpeg'], ['imagemagick', 'convert']]
5
+
6
+ def run
7
+ if homebrew_installed?
8
+ DEPENDENCIES.each do |dependency, binary|
9
+ install(dependency, binary)
10
+ end
11
+ else
12
+ puts "WGif can't find Homebrew. Visit http://brew.sh/ to get it."
13
+ Kernel.exit 0
14
+ end
15
+ end
16
+
17
+ def homebrew_installed?
18
+ Kernel.system 'brew info > /dev/null'
19
+ end
20
+
21
+ def install(dependency, binary)
22
+ unless installed?(binary)
23
+ puts "Installing #{dependency}..."
24
+ Kernel.system "brew install #{dependency} > /dev/null"
25
+ puts "Successfully installed #{dependency}."
26
+ end
27
+ end
28
+
29
+ def installed?(binary)
30
+ Kernel.system "which #{binary} > /dev/null"
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module WGif
2
+ VERSION = "0.0.1.pre"
3
+ end
data/lib/wgif/video.rb ADDED
@@ -0,0 +1,58 @@
1
+ require 'streamio-ffmpeg'
2
+ require 'fileutils'
3
+
4
+ module WGif
5
+ class Video
6
+ attr_accessor :name, :clip, :logger
7
+
8
+ def initialize name, filepath
9
+ @name = name
10
+ @clip = FFMPEG::Movie.new(filepath)
11
+ FileUtils.mkdir_p "/tmp/wgif/"
12
+ @logger = Logger.new("/tmp/wgif/#{name}.log")
13
+ FFMPEG.logger = @logger
14
+ end
15
+
16
+ def trim start_timestamp, duration
17
+ options = {
18
+ audio_codec: "copy",
19
+ video_codec: "copy",
20
+ custom: "-ss #{start_timestamp} -t 00:00:#{'%06.3f' % duration}"
21
+ }
22
+ trimmed = transcode(@clip, "/tmp/wgif/#{@name}-clip.mov", options)
23
+ WGif::Video.new "#{@name}-clip", "/tmp/wgif/#{@name}-clip.mov"
24
+ end
25
+
26
+ def to_frames(options={})
27
+ make_frame_dir
28
+ if options[:frames]
29
+ framerate = options[:frames] / @clip.duration
30
+ else
31
+ framerate = 24
32
+ end
33
+ transcode(@clip, "/tmp/wgif/frames/\%2d.png", "-vf fps=#{framerate}")
34
+ open_frame_dir
35
+ end
36
+
37
+ private
38
+
39
+ def make_frame_dir
40
+ FileUtils.rm Dir.glob("/tmp/wgif/frames/*.png")
41
+ FileUtils.mkdir_p "/tmp/wgif/frames"
42
+ end
43
+
44
+ def open_frame_dir
45
+ Dir.glob("/tmp/wgif/frames/*.png")
46
+ end
47
+
48
+ def transcode(clip, file, options)
49
+ begin
50
+ clip.transcode(file, options)
51
+ rescue FFMPEG::Error => error
52
+ raise WGif::ClipEncodingException unless error.message.include? "no output file created"
53
+ raise WGif::ClipEncodingException if error.message.include? "Invalid data found when processing input"
54
+ end
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,18 @@
1
+ require 'pathname'
2
+
3
+ module WGif
4
+ class VideoCache
5
+
6
+ def initialize
7
+ @cache = {}
8
+ end
9
+
10
+ def get(video_id)
11
+ path = "/tmp/wgif/#{video_id}"
12
+ if Pathname.new(path).exist?
13
+ WGif::Video.new(video_id, path)
14
+ end
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+ require 'simplecov'
3
+
4
+ if ENV['COVERAGE'] == 'true'
5
+ SimpleCov.start do
6
+ add_filter '/spec/'
7
+ end
8
+ end
@@ -0,0 +1,191 @@
1
+ require 'spec_helper'
2
+ require 'wgif/cli'
3
+
4
+ describe WGif::CLI do
5
+ let(:cli) { described_class.new }
6
+
7
+ it 'parses a URL from command line args' do
8
+ args = cli.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 = cli.parse_args ["http://example.com"]
14
+ args[:trim_from].should eq("00:00:00")
15
+ end
16
+
17
+ it 'trims clips to 1s by default' do
18
+ args = cli.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 = cli.parse_options ["-f", "40"]
24
+ options[:frames].should eq(40)
25
+ end
26
+
27
+ it 'parses the long frame count option' do
28
+ options = cli.parse_options ["--frames", "40"]
29
+ options[:frames].should eq(40)
30
+ end
31
+
32
+ it 'parses the short start time option' do
33
+ options = cli.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 = cli.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 = cli.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 = cli.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 = cli.parse_options ["-w", "400"]
54
+ expect(options[:dimensions]).to eq("400")
55
+ end
56
+
57
+ it 'parses the long dimensions option' do
58
+ options = cli.parse_options ["--width", "300"]
59
+ expect(options[:dimensions]).to eq("300")
60
+ end
61
+
62
+ it 'handles args in wacky order' do
63
+ args = cli.parse_args([
64
+ "-d",
65
+ "1.5",
66
+ "http://example.com",
67
+ "--frames",
68
+ "60",
69
+ "my-great-gif.gif",
70
+ "-s",
71
+ "00:00:05"])
72
+
73
+ expect(args).to eq(url: "http://example.com",
74
+ trim_from: "00:00:05",
75
+ duration: 1.5,
76
+ frames: 60,
77
+ output: "my-great-gif.gif",
78
+ dimensions: "480")
79
+ end
80
+
81
+ context 'validating args' do
82
+
83
+ it 'checks for a missing output file' do
84
+ args = cli.parse_args([
85
+ "http://example.com",
86
+ ])
87
+ expect{ cli.validate_args args }.to raise_error(WGif::MissingOutputFileException)
88
+ end
89
+
90
+ it 'checks for an invalid URL' do
91
+ args = cli.parse_args([
92
+ "crazy nonsense",
93
+ "output.gif"
94
+ ])
95
+ expect{ cli.validate_args args }.to raise_error(WGif::InvalidUrlException)
96
+ end
97
+
98
+ it 'checks for an invalid timestamp' do
99
+ args = cli.parse_args([
100
+ "http://lol.wut",
101
+ "output.gif",
102
+ "-s",
103
+ "rofl"
104
+ ])
105
+ expect{ cli.validate_args args }.to raise_error(WGif::InvalidTimestampException)
106
+ end
107
+
108
+ it 'returns true when args are OK' do
109
+ args = cli.parse_args([
110
+ "https://crazynonsense.info",
111
+ "output.gif"
112
+ ])
113
+ expect{ cli.validate_args args }.not_to raise_error
114
+ end
115
+ end
116
+
117
+ context 'error handling' do
118
+
119
+ before do
120
+ @mock_stdout = StringIO.new
121
+ @real_stdout, $stdout = $stdout, @mock_stdout
122
+ end
123
+
124
+ after do
125
+ $stdout = @real_stdout
126
+ end
127
+
128
+ def expect_help_with_message(out, message)
129
+ expect(out).to include(message)
130
+ expect(out).to include('Usage: wgif [YouTube URL] [output file] [options]')
131
+ cli.parser.summarize.each do |help_info|
132
+ expect(out).to include(help_info)
133
+ end
134
+ expect(out).to include('Example:')
135
+ end
136
+
137
+ it 'catches invalid URLs' do
138
+ OptionParser.any_instance.stub(:parse!).and_raise(WGif::InvalidUrlException)
139
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
140
+ expect_help_with_message(@mock_stdout.string, 'That looks like an invalid URL. Check the syntax.')
141
+ end
142
+
143
+ it 'catches invalid timestamps' do
144
+ OptionParser.any_instance.stub(:parse!).and_raise(WGif::InvalidTimestampException)
145
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
146
+ expect_help_with_message(@mock_stdout.string, 'That looks like an invalid timestamp. Check the syntax.')
147
+ end
148
+
149
+ it 'catches missing output args' do
150
+ OptionParser.any_instance.stub(:parse!).and_raise(WGif::MissingOutputFileException)
151
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
152
+ expect_help_with_message(@mock_stdout.string, 'Please specify an output file.')
153
+ end
154
+
155
+ it 'catches missing videos' do
156
+ OptionParser.any_instance.stub(:parse!).and_raise(WGif::VideoNotFoundException)
157
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
158
+ expect_help_with_message(@mock_stdout.string, "WGif can't find a valid YouTube video at that URL.")
159
+ end
160
+
161
+ it 'catches encoding exceptions' do
162
+ OptionParser.any_instance.stub(:parse!).and_raise(WGif::ClipEncodingException)
163
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
164
+ expect_help_with_message(@mock_stdout.string, "WGif encountered an error transcoding the video.")
165
+ end
166
+
167
+ it 'raises SystemExit when thrown' do
168
+ OptionParser.any_instance.stub(:parse!).and_raise(SystemExit)
169
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
170
+ end
171
+
172
+ it 'Prints the backtrace for all other exceptions' do
173
+ exception = Exception.new 'crazy error'
174
+ OptionParser.any_instance.stub(:parse!).and_raise(exception)
175
+ expect{ cli.make_gif([]) }.to raise_error(SystemExit)
176
+ expect_help_with_message(@mock_stdout.string, 'Something went wrong creating your GIF. The details:')
177
+ expect(@mock_stdout.string).to include('Please open an issue')
178
+ expect(@mock_stdout.string).to include("#{exception}")
179
+ expect(@mock_stdout.string).to include(exception.backtrace.join("\n"))
180
+ end
181
+
182
+ it 'prints help information' do
183
+ expect{ cli.make_gif(['-h']) }.to raise_error(SystemExit)
184
+ cli.parser.summarize.each do |help_info|
185
+ expect(@mock_stdout.string).to include(help_info)
186
+ end
187
+ end
188
+
189
+ end
190
+
191
+ end
@@ -0,0 +1,38 @@
1
+ require 'spec_helper'
2
+ require 'wgif/download_bar'
3
+
4
+ describe WGif::DownloadBar do
5
+
6
+ context 'setup' do
7
+
8
+ let(:download_bar) { described_class.new }
9
+ let(:mock_progress_bar) { double(ProgressBar) }
10
+
11
+ before do
12
+ ProgressBar.stub(:create).and_return(mock_progress_bar)
13
+ end
14
+
15
+ it 'creates a ProgressBar with the correct format, smoothing, and size' do
16
+ progress_bar_params = {
17
+ format: '==> %p%% |%B|',
18
+ smoothing: 0.8,
19
+ total: nil
20
+ }
21
+ expect(ProgressBar).to receive(:create).with(progress_bar_params)
22
+ described_class.new
23
+ end
24
+
25
+ it 'updates the total size' do
26
+ expect(mock_progress_bar).to receive(:total=).with(500)
27
+ download_bar.update_total(500)
28
+ end
29
+
30
+ it 'increments the current progress' do
31
+ expect(mock_progress_bar).to receive(:progress).and_return(1)
32
+ expect(mock_progress_bar).to receive(:progress=)
33
+ download_bar.increment_progress(100)
34
+ end
35
+
36
+ end
37
+
38
+ end
@@ -0,0 +1,72 @@
1
+ require 'spec_helper'
2
+ require 'wgif/downloader'
3
+ require 'wgif/video'
4
+
5
+ describe WGif::Downloader do
6
+ let(:downloader) { described_class.new }
7
+ let(:clip_url) { 'http://lol.wut/watch?v=roflcopter' }
8
+
9
+ before do
10
+ FileUtils.rm_rf('/tmp/wgif')
11
+ download_bar = double(WGif::DownloadBar).as_null_object
12
+ WGif::DownloadBar.stub(:new).and_return(download_bar)
13
+ end
14
+
15
+ it 'retrieves a YouTube download URL' do
16
+ ViddlRb.should_receive(:get_urls).with(clip_url).and_return(['clip url'])
17
+ downloader.video_url(clip_url).should eq('clip url')
18
+ end
19
+
20
+ it 'retrieves a YouTube video ID' do
21
+ downloader.video_id(clip_url).should eq('roflcopter')
22
+ end
23
+
24
+ it 'throws an error if the video is not found' do
25
+ ViddlRb.should_receive(:get_urls).with(clip_url).and_return(['http://lol.wut'])
26
+ expect{ downloader.get_video(clip_url) }.to raise_error(WGif::VideoNotFoundException)
27
+ end
28
+
29
+ it 'extracts a YouTube ID from a URL' do
30
+ downloader.video_id("https://www.youtube.com/watch?v=tmNXKqeUtJM").should eq("tmNXKqeUtJM")
31
+ end
32
+
33
+ context 'downloading videos' do
34
+
35
+ before do
36
+ ViddlRb.stub(:get_urls).and_return([clip_url])
37
+ fake_request = double('Typhoeus::Request')
38
+ fake_response = double('Typhoeus::Response')
39
+ Typhoeus::Request.should_receive(:new).once.with(clip_url).and_return(fake_request)
40
+ fake_request.should_receive(:on_headers)
41
+ fake_request.should_receive(:on_body)
42
+ fake_request.should_receive(:run).and_return(fake_response)
43
+ fake_response.should_receive(:response_code).and_return(200)
44
+ end
45
+
46
+ it 'downloads a clip' do
47
+ video = double(name: 'video')
48
+ WGif::Video.should_receive(:new).with('roflcopter', "/tmp/wgif/roflcopter").
49
+ and_return(video)
50
+ video = downloader.get_video(clip_url)
51
+ end
52
+
53
+ it 'does not download the clip when already cached' do
54
+ downloader.get_video(clip_url)
55
+ downloader.get_video(clip_url)
56
+ end
57
+ end
58
+
59
+ context 'errors' do
60
+
61
+ it 'throws an exception when the download URL is not found' do
62
+ ViddlRb.stub(:get_urls).and_raise(RuntimeError)
63
+ expect{ downloader.video_url('invalid url') }.to raise_error(WGif::VideoNotFoundException)
64
+ end
65
+
66
+ it 'throws an exception when the download URL is invalid' do
67
+ expect{ downloader.video_id(nil) }.to raise_error(WGif::InvalidUrlException)
68
+ end
69
+
70
+ end
71
+
72
+ end