flickrage 0.1.1

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,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