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.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.rspec +2 -0
- data/.rubocop.yml +50 -0
- data/.ruby-version +1 -0
- data/.travis.yml +14 -0
- data/Gemfile +12 -0
- data/LICENSE.txt +22 -0
- data/README.md +188 -0
- data/Rakefile +16 -0
- data/bin/console +15 -0
- data/bin/flickrage +13 -0
- data/bin/setup +8 -0
- data/flickrage.gemspec +43 -0
- data/lib/flickrage.rb +183 -0
- data/lib/flickrage/cli.rb +228 -0
- data/lib/flickrage/entity.rb +7 -0
- data/lib/flickrage/entity/image.rb +42 -0
- data/lib/flickrage/entity/image_list.rb +66 -0
- data/lib/flickrage/helpers.rb +80 -0
- data/lib/flickrage/log.rb +88 -0
- data/lib/flickrage/pipeline.rb +64 -0
- data/lib/flickrage/service.rb +9 -0
- data/lib/flickrage/service/composer.rb +51 -0
- data/lib/flickrage/service/downloader.rb +63 -0
- data/lib/flickrage/service/resizer.rb +59 -0
- data/lib/flickrage/service/search.rb +53 -0
- data/lib/flickrage/types.rb +6 -0
- data/lib/flickrage/version.rb +4 -0
- data/lib/flickrage/worker.rb +10 -0
- data/lib/flickrage/worker/base.rb +73 -0
- data/lib/flickrage/worker/compose.rb +66 -0
- data/lib/flickrage/worker/download.rb +87 -0
- data/lib/flickrage/worker/resize.rb +85 -0
- data/lib/flickrage/worker/search.rb +106 -0
- data/log/.keep +0 -0
- data/tmp/.keep +0 -0
- metadata +322 -0
@@ -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,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
|