flickrage 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|