jack 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,5 @@
1
+ == 1.0.0 / 2007-08-25
2
+
3
+ * 1 major enhancement
4
+ * Birthday!
5
+
@@ -0,0 +1,15 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/jack.rb
6
+ lib/jack/queues.rb
7
+ lib/jack/queues/appcast.rb
8
+ lib/jack/queues/mock.rb
9
+ lib/jack/tasks.rb
10
+ lib/jack/tasks/ffmpeg.rb
11
+ lib/jack/tasks/locking.rb
12
+ lib/jack/tasks/s3.rb
13
+ test/queues_test.rb
14
+ test/tasks_test.rb
15
+ test/test_helper.rb
@@ -0,0 +1,48 @@
1
+ jack
2
+ by FIX (your name)
3
+ FIX (url)
4
+
5
+ == DESCRIPTION:
6
+
7
+ FIX (describe your package)
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ * FIX (list of features or problems)
12
+
13
+ == SYNOPSIS:
14
+
15
+ FIX (code sample of usage)
16
+
17
+ == REQUIREMENTS:
18
+
19
+ * FIX (list of requirements)
20
+
21
+ == INSTALL:
22
+
23
+ * FIX (sudo gem install, anything else)
24
+
25
+ == LICENSE:
26
+
27
+ (The MIT License)
28
+
29
+ Copyright (c) 2007 Rick Olson
30
+
31
+ Permission is hereby granted, free of charge, to any person obtaining
32
+ a copy of this software and associated documentation files (the
33
+ 'Software'), to deal in the Software without restriction, including
34
+ without limitation the rights to use, copy, modify, merge, publish,
35
+ distribute, sublicense, and/or sell copies of the Software, and to
36
+ permit persons to whom the Software is furnished to do so, subject to
37
+ the following conditions:
38
+
39
+ The above copyright notice and this permission notice shall be
40
+ included in all copies or substantial portions of the Software.
41
+
42
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
43
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
44
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
45
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
46
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
47
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
48
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,20 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ $: << 'lib'
6
+ require 'jack'
7
+
8
+ Hoe.new('jack', Jack::VERSION) do |p|
9
+ p.rubyforge_name = 'jack'
10
+ p.author = 'Rick Olson'
11
+ p.email = 'technoweenie@gmail.com'
12
+ # p.summary = 'FIX'
13
+ # p.description = p.paragraphs_of('README.txt', 2..5).join("\n\n")
14
+ # p.url = p.paragraphs_of('README.txt', 0).first.split(/\n/)[1..-1]
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+ p.extra_deps << 'rake' << 'open4'
17
+ p.test_globs << 'test/**/*_test.rb'
18
+ end
19
+
20
+ # vim: syntax=Ruby
@@ -0,0 +1,19 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'thread'
4
+ require 'jack/tasks'
5
+
6
+ module Jack
7
+ VERSION = '1.0.0'
8
+
9
+ class Task < Rake::Task
10
+ def execute
11
+ Thread.current[:task] = self
12
+ super
13
+ ensure
14
+ Thread.current[:task] = nil
15
+ end
16
+ end
17
+ end
18
+
19
+ include Jack::Tasks
@@ -0,0 +1,56 @@
1
+ module Jack
2
+ module Queues
3
+ def process_queue(queue_name, options = {}, &block)
4
+ Queues::Task.queue_task(queue_name, options, &block)
5
+ end
6
+
7
+ def queue
8
+ Thread.current[:task]
9
+ end
10
+
11
+ class Task < Jack::Task
12
+ attr_accessor :queue_name
13
+ attr_accessor :connection_args
14
+ attr_accessor :options
15
+
16
+ def kept
17
+ @kept ||= []
18
+ end
19
+
20
+ def keep(*messages)
21
+ messages.flatten!
22
+ messages.uniq!
23
+ kept.push *messages
24
+ end
25
+
26
+ def execute
27
+ task = lambda do
28
+ if messages.empty?
29
+ false
30
+ else
31
+ super
32
+ (messages - kept).each do |msg|
33
+ delete msg
34
+ end
35
+ kept.empty?
36
+ end
37
+ end
38
+ while task.call
39
+ @messages = nil
40
+ end
41
+ end
42
+
43
+ class << self
44
+ attr_accessor :default_connection_args
45
+ end
46
+
47
+ def self.queue_task(name, options = {}, &block)
48
+ task = define_task(name, &block)
49
+ task.queue_name = options.delete(:queue_name) || name
50
+ task.options = options
51
+ task.connection_args = options.delete(:connect) || default_connection_args
52
+ task
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ require 'appcast/client'
2
+ module Jack
3
+ module Queues
4
+ module Appcast
5
+ def connection
6
+ @connection ||= ::Appcast::Client.new(*connection_args)
7
+ end
8
+
9
+ def messages
10
+ if @messages.nil?
11
+ @messages = connection.list(@queue_name, @options)
12
+ logger.info("[Appcast] Found #{@messages.size} message(s)")
13
+ end
14
+ @messages
15
+ end
16
+
17
+ def delete(message)
18
+ logger.info("[Appcast] Deleting message for #{message.name}")
19
+ message.destroy
20
+ end
21
+
22
+ def create(name_or_data, data = nil)
23
+ if data.nil?
24
+ data = name_or_data
25
+ name_or_data = @queue_name
26
+ end
27
+ logger.info("[Appcast] Created message for #{name_or_data}")
28
+ connection.create(name_or_data, data)
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,29 @@
1
+ module Jack
2
+ module Queues
3
+ module Mock
4
+ def connection
5
+ @connection ||= []
6
+ end
7
+
8
+ def messages
9
+ @messages ||= (connection.shift || [])
10
+ end
11
+
12
+ def delete(message)
13
+ messages.delete message
14
+ end
15
+
16
+ def create(name_or_data, data = nil)
17
+ if data.nil?
18
+ data = name_or_data
19
+ name_or_data = @queue_name
20
+ end
21
+ created << [name_or_data, data]
22
+ end
23
+
24
+ def created
25
+ @created ||= []
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,35 @@
1
+ require 'logger'
2
+ require 'jack/tasks/ffmpeg'
3
+ require 'jack/tasks/locking'
4
+
5
+ module Jack
6
+ module Tasks
7
+ def setup_queue(queue_type, *args)
8
+ require "jack/queues"
9
+ require "jack/queues/#{queue_type}"
10
+ include Jack::Queues
11
+ mod = Jack::Queues.const_get(queue_type.to_s.capitalize)
12
+ Jack::Queues::Task.send :include, mod
13
+ Jack::Queues::Task.default_connection_args = args
14
+ end
15
+
16
+ def setup_s3(options)
17
+ require 'jack/tasks/s3'
18
+ @default_s3_bucket = options.delete(:bucket)
19
+ @s3_working_path = options.delete(:working).to_s
20
+ AWS::S3::Base.establish_connection!(options) if options.any?
21
+ include Jack::Tasks::S3
22
+ end
23
+
24
+ def setup_logger(*args)
25
+ @logger = Logger.new(*args)
26
+ end
27
+
28
+ def logger
29
+ @logger ||= Logger.new(STDERR)
30
+ end
31
+
32
+ include Jack::Tasks::Ffmpeg
33
+ include Jack::Tasks::Locking
34
+ end
35
+ end
@@ -0,0 +1,75 @@
1
+ require 'open4'
2
+
3
+ module Jack
4
+ module Tasks
5
+ module Ffmpeg
6
+ def ffmpeg(input, options = {})
7
+ cmd = ['ffmpeg']
8
+ input = { :file => input } unless input.is_a?(Hash)
9
+ ffmpeg_options :input, cmd, input
10
+ ffmpeg_options :output, cmd, options
11
+ execute_command cmd.join(" ")
12
+ end
13
+
14
+ def convert_to_flv(filename, size, output = nil, options = {})
15
+ if output.is_a?(Hash)
16
+ options = output
17
+ output = nil
18
+ end
19
+ default = {:rate => 25, :acodec => :mp3, :frequency => 22050, :overwrite => true, :size => size, :file => (output || filename + ".flv")}
20
+ ffmpeg filename, default.update(options)
21
+ options[:file]
22
+ end
23
+
24
+ def grab_screenshot_from(filename, size, output = nil, options = {})
25
+ if output.is_a?(Hash)
26
+ options = output
27
+ output = nil
28
+ end
29
+ default = {:vframes => 1, :format => :image2, :disable_audio => true, :size => size, :file => (output || filename + ".jpg")}
30
+ ffmpeg filename, default.update(options)
31
+ options[:file]
32
+ end
33
+
34
+ protected
35
+ def ffmpeg_options(param, cmd, options)
36
+ file = options.delete(:file)
37
+ options.inject(cmd) do |c, (key, value)|
38
+ c << \
39
+ case key
40
+ when :duration then "-t #{value}"
41
+ when :rate then "-r #{value}"
42
+ when :bitrate then "-b #{value}"
43
+ when :seek then "-ss #{value}"
44
+ when :verbose then "-v #{value == true ? :verbose : value}"
45
+ when :size then "-s #{value}"
46
+ when :overwrite then "-y"
47
+ when :format then "-f #{value}"
48
+ when :frequency then "-ar #{value}"
49
+ when :abitrate then "-ab #{value}"
50
+ when :disable_video then "-vn"
51
+ when :disable_audio then "-an"
52
+ else "-#{key} #{value}"
53
+ end
54
+ end
55
+ cmd << "#{param == :input ? '-i ' : ''}#{File.expand_path(file)}"
56
+ end
57
+
58
+ def execute_command(cmd)
59
+ logger.info "[ffmpeg] Executing: #{cmd}"
60
+ result = []
61
+ Open4.popen4 cmd do |pid, stdin, stdout, stderr|
62
+ result << stdout.read.to_s.strip
63
+ result << stderr.read.to_s.strip
64
+ end
65
+ unless result.first.blank?
66
+ logger.info "[ffmpeg] OUTPUT: #{result.first}"
67
+ end
68
+ unless result.last.blank?
69
+ logger.debug "[ffmpeg] ERROR: #{result.last}"
70
+ end
71
+ result
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,20 @@
1
+ module Jack
2
+ module Tasks
3
+ module Locking
4
+ def lock(*args)
5
+ require 'lockfile' unless Object.const_defined?(:Lockfile)
6
+ options = args.last.is_a?(Hash) ? args.pop : {:retries => 0}
7
+ lock_filename = args.shift || 'jack.lock'
8
+ if block_given?
9
+ lockfile = ::Lockfile.new(lock_filename, options)
10
+ begin
11
+ lockfile.lock
12
+ yield
13
+ ensure
14
+ lockfile.unlock if lockfile.locked?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,44 @@
1
+ require 'aws/s3'
2
+
3
+ module Jack
4
+ module Tasks
5
+ module S3
6
+ def store_in_s3(name, filename, bucket = nil, options = {})
7
+ if bucket.is_a?(Hash)
8
+ options = bucket
9
+ bucket = nil
10
+ end
11
+ bucket ||= default_s3_bucket
12
+ logger.info("[S3] Storing #{name} in #{bucket} from #{filename}")
13
+ AWS::S3::S3Object.store name, open(filename), bucket, options
14
+ end
15
+
16
+ def download_from_s3(name, bucket = nil)
17
+ bucket ||= default_s3_bucket
18
+ logger.info("[S3] Downloading #{name} from #{bucket}")
19
+ Dir.chdir s3_working_path do
20
+ open name, 'w' do |file|
21
+ AWS::S3::S3Object.stream name, bucket do |chunk|
22
+ file.write chunk
23
+ end
24
+ end
25
+ end
26
+ File.join s3_working_path, name
27
+ end
28
+
29
+ def delete_from_s3(name, bucket = nil)
30
+ bucket ||= default_s3_bucket
31
+ logger.info("[S3] Deleting #{name} from #{bucket}")
32
+ AWS::S3::S3Object.delete name, bucket
33
+ end
34
+
35
+ def default_s3_bucket
36
+ @default_s3_bucket
37
+ end
38
+
39
+ def s3_working_path
40
+ @s3_working_path
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,71 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ context "Jack Queues" do
4
+ specify "should provide access to queue object" do
5
+ @task = JACK.process_queue :queue_object do
6
+ JACK.poke
7
+ JACK.queue.should == @task
8
+ end
9
+ JACK.expects(:poke)
10
+ @task.connection << [:msg]
11
+ @task.execute
12
+ end
13
+
14
+ specify "should create message with current queue name" do
15
+ @task = JACK.process_queue :create do
16
+ JACK.poke
17
+ JACK.queue.create :foo
18
+ end
19
+ JACK.expects(:poke)
20
+ @task.connection << [:msg]
21
+ @task.execute
22
+ @task.created.should == [[:create, :foo]]
23
+ end
24
+
25
+ specify "should create message with custom queue name" do
26
+ @task = JACK.process_queue :create_custom do
27
+ JACK.poke
28
+ JACK.queue.create :foo, :bar
29
+ end
30
+ JACK.expects(:poke)
31
+ @task.connection << [:msg]
32
+ @task.execute
33
+ @task.created.should == [[:foo, :bar]]
34
+ end
35
+
36
+ specify "should set queue name to task name" do
37
+ @task = JACK.process_queue(:queue_name) {}
38
+ @task.queue_name.should == :queue_name
39
+ end
40
+
41
+ specify "should set options and custom queue name" do
42
+ @task = JACK.process_queue(:queue_options, :queue_name => :foo, :example => :option) {}
43
+ @task.queue_name.should == :foo
44
+ @task.options.should == {:example => :option}
45
+ end
46
+
47
+ specify "should execute queue task" do
48
+ @task = JACK.process_queue :queue_task do
49
+ JACK.poke
50
+ JACK.queue.messages.should == [:msg, :keep]
51
+ JACK.queue.keep :keep
52
+ end
53
+ JACK.expects(:poke)
54
+ @task.connection << [:msg, :keep] << [:msg2] # store messages
55
+ @task.execute
56
+ end
57
+
58
+ specify "should loop queue task" do
59
+ @task = JACK.process_queue :queue_loop do
60
+ if JACK.poke == 1
61
+ JACK.queue.messages.should == [:msg, :msg2]
62
+ else
63
+ JACK.queue.messages.should == [:msg3]
64
+ end
65
+ end
66
+ JACK.expects(:poke).times(2).returns(1,2)
67
+ @task.connection << [:msg, :msg2] << [:msg3] # store messages
68
+ @task.execute
69
+ @task.messages.should.be.empty
70
+ end
71
+ end
@@ -0,0 +1,100 @@
1
+ require File.join(File.dirname(__FILE__), 'test_helper')
2
+
3
+ context "Jack Tasks" do
4
+ specify "should store instance in current thread while executing" do
5
+ @task = Jack::Task.define_task :test_thread do
6
+ Thread.current[:task].should == @task
7
+ end
8
+
9
+ Thread.current[:task].should.be.nil
10
+ @task.execute
11
+ Thread.current[:task].should.be.nil
12
+ end
13
+
14
+ specify "should setup s3 bucket name" do
15
+ JACK.default_s3_bucket.should == 'stuff'
16
+ end
17
+
18
+ specify "should setup s3 working path" do
19
+ JACK.s3_working_path.should == 'foo/bar'
20
+ end
21
+
22
+ specify "should set s3 connection options" do
23
+ AWS::S3::Base.connection.options[:access_key_id].should == 'abc'
24
+ AWS::S3::Base.connection.options[:secret_access_key].should == 'def'
25
+ end
26
+
27
+ specify "should store with default bucket and options" do
28
+ @stream = stub
29
+ JACK.expects(:open).with('filename').returns(@stream)
30
+ AWS::S3::S3Object.expects(:store).with('foo', @stream, 'stuff', {:expires => 300})
31
+ JACK.store_in_s3 'foo', 'filename', :expires => 300
32
+ end
33
+
34
+ specify "should store with custom bucket and options" do
35
+ @stream = stub
36
+ JACK.expects(:open).with('filename').returns(@stream)
37
+ AWS::S3::S3Object.expects(:store).with('foo', @stream, 'things', {:expires => 300})
38
+ JACK.store_in_s3 'foo', 'filename', 'things', :expires => 300
39
+ end
40
+
41
+ specify "should download from s3 from default bucket" do
42
+ @stream = StringIO.new
43
+ Dir.expects(:chdir).with(JACK.s3_working_path).yields
44
+ JACK.expects(:open).with('foo', 'w').yields(@stream)
45
+ AWS::S3::S3Object.expects(:stream).with('foo', 'stuff').yields("content")
46
+ JACK.download_from_s3 'foo'
47
+ @stream.rewind
48
+ @stream.read.should == 'content'
49
+ end
50
+
51
+ specify "should download from s3 from custom bucket" do
52
+ @stream = StringIO.new
53
+ Dir.expects(:chdir).with(JACK.s3_working_path).yields
54
+ JACK.expects(:open).with('foo', 'w').yields(@stream)
55
+ AWS::S3::S3Object.expects(:stream).with('foo', 'things').yields("content")
56
+ JACK.download_from_s3 'foo', 'things'
57
+ @stream.rewind
58
+ @stream.read.should == 'content'
59
+ end
60
+
61
+ specify "should delete with default s3 bucket" do
62
+ AWS::S3::S3Object.expects(:delete).with('foo', 'stuff')
63
+ JACK.delete_from_s3 'foo'
64
+ end
65
+
66
+ specify "should delete with custom s3 bucket" do
67
+ AWS::S3::S3Object.expects(:delete).with('foo', 'things')
68
+ JACK.delete_from_s3 'foo', 'things'
69
+ end
70
+
71
+ specify "should create ffmpeg command" do
72
+ File.expects(:expand_path).with('foo').returns('foo_path')
73
+ File.expects(:expand_path).with('bar').returns('bar_path')
74
+ JACK.ffmpeg('foo', :file => 'bar').should == "ffmpeg -i foo_path bar_path"
75
+ end
76
+
77
+ {
78
+ [:duration, 3] => [:t],
79
+ [:rate, 3] => [],
80
+ [:seek, 3] => [:ss],
81
+ [:verbose, true] => [:v, :verbose],
82
+ [:size, 3] => [],
83
+ [:overwrite, nil] => [:y],
84
+ [:format, :test] => [:f],
85
+ [:frequency, :test] => [:ar],
86
+ [:abitrate, :test] => [:ab],
87
+ [:disable_video, nil] => [:vn],
88
+ [:disable_audio, nil] => [:an],
89
+ [:whatever, :test] => [:whatever]
90
+ }.each do |args, expected|
91
+ specify "should recognize ffmpeg argument -#{args[0]}" do
92
+ File.expects(:expand_path).with('foo').returns('foo_path')
93
+ File.expects(:expand_path).with('bar').returns('bar_path')
94
+ switch = expected[0] || args[0].to_s[0..0]
95
+ value = args[1] ? " #{expected[1] || args[1]}" : nil
96
+ JACK.ffmpeg('foo', args[0] => args[1], :file => 'bar').should == "ffmpeg -i foo_path -#{switch}#{value} bar_path"
97
+ end
98
+ end
99
+ end
100
+
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
2
+ require 'test/unit'
3
+ require 'jack'
4
+ require 'test/spec'
5
+ require 'mocha'
6
+
7
+ JACK = Object.new
8
+ JACK.extend Jack::Tasks
9
+ class << JACK
10
+ def include(*args)
11
+ extend(*args)
12
+ end
13
+
14
+ def logger_stream
15
+ @logger_stream ||= StringIO.new
16
+ end
17
+
18
+ def execute_command(cmd)
19
+ cmd
20
+ end
21
+ end
22
+
23
+ JACK.setup_queue :mock
24
+ JACK.setup_s3 :bucket => 'stuff', :working => 'foo/bar', :access_key_id => 'abc', :secret_access_key => 'def'
25
+ JACK.setup_logger JACK.logger_stream
metadata ADDED
@@ -0,0 +1,91 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.4
3
+ specification_version: 1
4
+ name: jack
5
+ version: !ruby/object:Gem::Version
6
+ version: 1.0.0
7
+ date: 2007-09-07 00:00:00 -05:00
8
+ summary: The author was too lazy to write a summary
9
+ require_paths:
10
+ - lib
11
+ email: technoweenie@gmail.com
12
+ homepage: http://www.zenspider.com/ZSS/Products/jack/
13
+ rubyforge_project: jack
14
+ description: The author was too lazy to write a description
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - Rick Olson
31
+ files:
32
+ - History.txt
33
+ - Manifest.txt
34
+ - README.txt
35
+ - Rakefile
36
+ - lib/jack.rb
37
+ - lib/jack/queues.rb
38
+ - lib/jack/queues/appcast.rb
39
+ - lib/jack/queues/mock.rb
40
+ - lib/jack/tasks.rb
41
+ - lib/jack/tasks/ffmpeg.rb
42
+ - lib/jack/tasks/locking.rb
43
+ - lib/jack/tasks/s3.rb
44
+ - test/queues_test.rb
45
+ - test/tasks_test.rb
46
+ - test/test_helper.rb
47
+ test_files:
48
+ - test/test_helper.rb
49
+ - test/queues_test.rb
50
+ - test/tasks_test.rb
51
+ rdoc_options:
52
+ - --main
53
+ - README.txt
54
+ extra_rdoc_files:
55
+ - History.txt
56
+ - Manifest.txt
57
+ - README.txt
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ requirements: []
63
+
64
+ dependencies:
65
+ - !ruby/object:Gem::Dependency
66
+ name: rake
67
+ version_requirement:
68
+ version_requirements: !ruby/object:Gem::Version::Requirement
69
+ requirements:
70
+ - - ">"
71
+ - !ruby/object:Gem::Version
72
+ version: 0.0.0
73
+ version:
74
+ - !ruby/object:Gem::Dependency
75
+ name: open4
76
+ version_requirement:
77
+ version_requirements: !ruby/object:Gem::Version::Requirement
78
+ requirements:
79
+ - - ">"
80
+ - !ruby/object:Gem::Version
81
+ version: 0.0.0
82
+ version:
83
+ - !ruby/object:Gem::Dependency
84
+ name: hoe
85
+ version_requirement:
86
+ version_requirements: !ruby/object:Gem::Version::Requirement
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 1.3.0
91
+ version: