flickrage 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+ require 'concurrent-edge'
3
+
4
+ module Flickrage
5
+ class Pipeline
6
+ include Flickrage::Helpers::Log
7
+
8
+ attr_reader :opts
9
+
10
+ def initialize(opts = {})
11
+ @opts = opts
12
+ end
13
+
14
+ #
15
+ # Main pipeline
16
+ #
17
+
18
+ def run
19
+ logger.warn('Thank you for choosing Flickrage, you will find me as your Flickr collage companion :)')
20
+
21
+ list = Concurrent
22
+ .future { search_worker.call }
23
+ .then { |image_list| download_worker.call(image_list) }
24
+ .then { |image_list| resize_worker.call(image_list) }
25
+ .then { |image_list| compose_worker.call(image_list) }
26
+ .then do |image_list|
27
+ logger.info("#{image_list&.size || 0} images composed")
28
+ image_list
29
+ end
30
+ .rescue { |e| logger.error(e) }
31
+ .wait.value
32
+
33
+ speaker.add_padding
34
+
35
+ raise CollageError, 'Try again later...' unless valid_list?(list)
36
+
37
+ logger.warn("Congrats! You can find composed collage at #{list.collage_path}")
38
+
39
+ list
40
+ end
41
+
42
+ private
43
+
44
+ def search_worker
45
+ @search_worker ||= opts[:search_worker] || Worker::Search.new(opts)
46
+ end
47
+
48
+ def download_worker
49
+ @download_worker ||= opts[:download_worker] || Worker::Download.new(opts)
50
+ end
51
+
52
+ def resize_worker
53
+ @resize_worker ||= opts[:resize_worker] || Worker::Resize.new(opts)
54
+ end
55
+
56
+ def compose_worker
57
+ @compose_worker ||= opts[:compose_worker] || Worker::Compose.new(opts)
58
+ end
59
+
60
+ def valid_list?(list)
61
+ list.respond_to?(:valid?) && list.valid?
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Service
4
+ autoload :Composer, 'flickrage/service/composer'
5
+ autoload :Downloader, 'flickrage/service/downloader'
6
+ autoload :Resizer, 'flickrage/service/resizer'
7
+ autoload :Search, 'flickrage/service/search'
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Service
4
+ class Composer
5
+ include Flickrage::Helpers::Log
6
+
7
+ attr_reader :file_name, :width, :height, :shuffle
8
+
9
+ def initialize(file_name, width, height, shuffle: true)
10
+ @file_name = file_name
11
+ @width = width
12
+ @height = height
13
+ @shuffle = shuffle
14
+ end
15
+
16
+ def run(image_list)
17
+ compose(image_list)
18
+ check_image(image_list)
19
+ rescue StandardError => e
20
+ logger.debug(e)
21
+ image_list
22
+ end
23
+
24
+ private
25
+
26
+ def compose(image_list)
27
+ montage = MiniMagick::Tool::Montage.new
28
+ images(image_list.resized).each { |image| montage << image.resize_path }
29
+
30
+ montage_width = width * (image_list.resized.size / Flickrage.config.grid).to_i
31
+ montage_height = height * Flickrage.config.grid
32
+
33
+ montage.geometry "#{montage_width}x#{montage_height}+0+0"
34
+ montage.tile "x#{Flickrage.config.grid}"
35
+ montage.mode 'Concatenate'
36
+ montage.background 'none'
37
+
38
+ montage << image_list.collage_path
39
+ montage.call
40
+ end
41
+
42
+ def images(images)
43
+ shuffle ? images.shuffle : images
44
+ end
45
+
46
+ def check_image(image_list)
47
+ File.exist?(image_list.collage_path) ? image_list.finish_compose : image_list
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+ require 'uri'
3
+ require 'net/http'
4
+
5
+ module Flickrage
6
+ module Service
7
+ class Downloader
8
+ include Flickrage::Helpers::Log
9
+
10
+ def run(image)
11
+ uri = gen_uri(image.url)
12
+ image.file_name = file_name(uri)
13
+ download_file(uri, image.local_path)
14
+ check_image(image)
15
+ rescue StandardError => e
16
+ logger.debug(e)
17
+ image
18
+ end
19
+
20
+ private
21
+
22
+ def download_file(uri, path, limit = 10)
23
+ raise Flickrage::DownloadError, 'Redirect limit arrived' if limit.zero?
24
+
25
+ Net::HTTP.start(uri.host, uri.port,
26
+ use_ssl: uri.scheme == 'https') do |conn|
27
+ request = Net::HTTP::Get.new(uri)
28
+ response = conn.request request
29
+
30
+ case response
31
+ when Net::HTTPSuccess
32
+ write_file(path, response.body)
33
+ when Net::HTTPRedirection
34
+ download_file(gen_uri(response['location']),
35
+ path,
36
+ limit - 1)
37
+ else
38
+ response.error!
39
+ end
40
+ end
41
+ end
42
+
43
+ def write_file(path, body, mode = 'wb')
44
+ File.open(path, mode) do |file|
45
+ file.flock(File::LOCK_EX)
46
+ file << body
47
+ end
48
+ end
49
+
50
+ def gen_uri(url)
51
+ URI.parse(url)
52
+ end
53
+
54
+ def file_name(uri)
55
+ File.basename(uri.path)
56
+ end
57
+
58
+ def check_image(image)
59
+ File.exist?(image.local_path) ? image.finish_download : image
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Service
4
+ class Resizer
5
+ include Flickrage::Helpers::Log
6
+
7
+ attr_reader :width, :height
8
+
9
+ def initialize(width, height)
10
+ @width = width
11
+ @height = height
12
+ end
13
+
14
+ def run(image)
15
+ return image unless image.downloaded?
16
+ resize_to_fill(image)
17
+ check_image(image)
18
+ rescue => e
19
+ logger.debug(e)
20
+ image
21
+ end
22
+
23
+ private
24
+
25
+ def resize_to_fill(image, gravity = 'Center')
26
+ img = MiniMagick::Image.open(image.local_path)
27
+
28
+ cols, rows = img[:dimensions]
29
+ img.combine_options do |cmd|
30
+ if width != cols || height != rows
31
+ scale_x = width / cols.to_f
32
+ scale_y = height / rows.to_f
33
+ if scale_x >= scale_y
34
+ cols = (scale_x * (cols + 0.5)).round
35
+ rows = (scale_x * (rows + 0.5)).round
36
+ cmd.resize cols.to_s
37
+ else
38
+ cols = (scale_y * (cols + 0.5)).round
39
+ rows = (scale_y * (rows + 0.5)).round
40
+ cmd.resize "x#{rows}"
41
+ end
42
+ end
43
+ cmd.gravity gravity
44
+ cmd.background 'rgba(255,255,255,0.0)'
45
+ cmd.extent ">#{width}x#{height}" if cols != width || rows != height
46
+ end
47
+ img.write image.resize_path
48
+ end
49
+
50
+ def file_name(image)
51
+ image.resize_path
52
+ end
53
+
54
+ def check_image(image)
55
+ File.exist?(image.resize_path) ? image.finish_resize : image
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Service
4
+ class Search
5
+ include Flickrage::Helpers::Log
6
+
7
+ def run(keyword)
8
+ result = search(keyword)
9
+
10
+ return if result.size < 1
11
+
12
+ image(result.first, keyword)
13
+ rescue StandardError => e
14
+ logger.debug(e)
15
+ nil
16
+ end
17
+
18
+ private
19
+
20
+ def image(result, keyword)
21
+ return unless result.respond_to?(:url_l)
22
+
23
+ Flickrage::Entity::Image.new(
24
+ id: result.id,
25
+ title: title(result.title),
26
+ keyword: keyword,
27
+ url: result.url_l,
28
+ width: result.width_l,
29
+ height: result.height_l,
30
+ )
31
+ end
32
+
33
+ def search(keyword)
34
+ flickr.photos.search(params(text: keyword))
35
+ end
36
+
37
+ def title(text)
38
+ return text if text.nil? || text.size < 50
39
+ text[0..50] + '...'
40
+ end
41
+
42
+ def params(opts = {})
43
+ {
44
+ content_type: '1',
45
+ extras: 'url_l',
46
+ sort: 'interestingness-desc',
47
+ per_page: 1,
48
+ pages: 1
49
+ }.merge(opts)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Types
4
+ include Dry::Types.module
5
+ end
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ VERSION = '0.1.1'
4
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Worker
4
+ autoload :Base, 'flickrage/worker/base'
5
+ autoload :Resize, 'flickrage/worker/resize'
6
+ autoload :Download, 'flickrage/worker/download'
7
+ autoload :Search, 'flickrage/worker/search'
8
+ autoload :Compose, 'flickrage/worker/compose'
9
+ end
10
+ end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+ require 'concurrent-edge'
3
+ require 'mini_magick'
4
+
5
+ module Flickrage
6
+ module Worker
7
+ class Base
8
+ include Flickrage::Helpers::Log
9
+ include Flickrage::Helpers::Tty
10
+
11
+ MAX_ASK_ERRORS = 3
12
+
13
+ PRINT_IMAGE_HEADERS_LITE = %w(keyword id).freeze
14
+ PRINT_IMAGE_HEADERS = PRINT_IMAGE_HEADERS_LITE + %w(url title width height)
15
+
16
+ attr_accessor :opts, :service, :spin
17
+
18
+ def initialize(opts = {}, service = nil)
19
+ @service = service
20
+ @opts = default_opts.merge(opts)
21
+ @spin = nil
22
+ @opts[:ask_error_counter] = 0
23
+ end
24
+
25
+ def call; end
26
+
27
+ private
28
+
29
+ def default_opts
30
+ {}
31
+ end
32
+
33
+ #
34
+ # Output helpers
35
+ #
36
+
37
+ def increment_error_counter(error, value)
38
+ @opts[:ask_error_counter] += 1
39
+
40
+ raise error, value if opts[:ask_error_counter] > MAX_ASK_ERRORS
41
+ end
42
+
43
+ def reset_error_counter
44
+ @opts[:ask_error_counter] = 0
45
+ end
46
+
47
+ #
48
+ # Thread pool access
49
+ #
50
+
51
+ def thread_pool
52
+ Flickrage.config.pool ||= Concurrent::FixedThreadPool.new(Flickrage.config.pool_size)
53
+ end
54
+
55
+ def clean_thread_pool
56
+ return unless thread_pool
57
+ thread_pool.kill
58
+ Flickrage.config.pool = nil
59
+ end
60
+
61
+ #
62
+ # Update spin
63
+ # Due lots of changes in the spinner, keeping wrapper here.
64
+ #
65
+
66
+ def update_spin(spin, tags)
67
+ spin.clear_line
68
+ spin.update(tags)
69
+ spin.spin
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+ module Flickrage
3
+ module Worker
4
+ class Compose < Base
5
+ def call(image_list)
6
+ raise Flickrage::CollageError, 'Not enough images for collage' if image_list.resized&.size < 1
7
+
8
+ speaker.add_padding
9
+ logger.debug('Collage building process')
10
+
11
+ image_list.collage_path = init_file_name
12
+
13
+ @spin = spinner(message: 'Collage making')
14
+ result = service.run(image_list)
15
+
16
+ if result.composed?
17
+ spin.success
18
+ else
19
+ spin.error('(failed: Collage was not made)')
20
+ raise Flickrage::CollageError
21
+ end
22
+
23
+ result
24
+ ensure
25
+ spin&.stop
26
+ end
27
+
28
+ private
29
+
30
+ def service
31
+ return @service unless @service.nil?
32
+ @service = Service::Composer.new(opts['file_name'],
33
+ Flickrage.config.width,
34
+ Flickrage.config.height)
35
+ end
36
+
37
+ def init_file_name
38
+ return opts['file_name'] if validate_file_name
39
+
40
+ output = speaker.ask('Please enter the collage file name:', path: true)
41
+
42
+ unless valid_file_name?(output)
43
+ increment_error_counter(Flickrage::FileNameError,
44
+ "#{output}, must be valid, supported extensions: .png, .jpg or .gif")
45
+ return init_file_name
46
+ end
47
+
48
+ reset_error_counter
49
+
50
+ opts['file_name'] = output
51
+ end
52
+
53
+ def validate_file_name
54
+ return false unless opts['file_name']
55
+ return true if valid_file_name?(opts['file_name'])
56
+ opts['file_name'] = nil
57
+ false
58
+ end
59
+
60
+ def valid_file_name?(value)
61
+ return false unless value
62
+ value.match(/^([a-zA-Z0-9\-_\.]+)\.(png|jpg|jpeg|gif)$/)
63
+ end
64
+ end
65
+ end
66
+ end