wgif 0.0.1.pre

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.
@@ -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