flico 0.0.2 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.ruby-version +1 -1
- data/Gemfile +2 -0
- data/Gemfile.lock +18 -18
- data/LICENSE.text +1 -1
- data/Rakefile +5 -3
- data/bin/console +4 -3
- data/bin/flico +2 -0
- data/flico.gemspec +20 -20
- data/lib/flico/app.rb +47 -41
- data/lib/flico/collager.rb +19 -39
- data/lib/flico/dictionary.rb +28 -23
- data/lib/flico/flickr/base_api.rb +66 -0
- data/lib/flico/image_file.rb +29 -0
- data/lib/flico.rb +19 -73
- data/spec/base_api_spec.rb +33 -0
- data/spec/dictionary_spec.rb +21 -0
- data/spec/spec_helper.rb +53 -53
- metadata +41 -31
- data/lib/flico/cell.rb +0 -30
- data/lib/flico/flickr_command.rb +0 -58
- data/lib/flico/grid.rb +0 -81
- data/lib/flico/saver.rb +0 -44
- data/spec/flickr_command_spec.rb +0 -24
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 473a26ece9ff8cf869ea977bdc1a96a5b800848ce82f4ee0282dad74bda96bdc
|
4
|
+
data.tar.gz: b4e83f8d45ea19e79e4d5884603f3941af769e279d8267dff9379f662f3acbbf
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 55a33bfccc1180547b859c572ef4802f4bd6b6299298319e0b21519ae381b4f040b0620ffbde06e002c9d9216d6f00b559af148aa38b93d9968b10464351c918
|
7
|
+
data.tar.gz: c96edd9e612f9cb0dbc1711c0d0dedf5ade8be1b1e685316055cd4d48b3bbda9bbdc6c2b8aa8eea6073a9da88f10d8589b9f1c4bfad9db033d7d20664ec3ab7a
|
data/.ruby-version
CHANGED
@@ -1 +1 @@
|
|
1
|
-
2.
|
1
|
+
3.2.2
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,33 +1,33 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
flico (0.0
|
4
|
+
flico (1.0.0)
|
5
5
|
flickraw
|
6
6
|
mini_magick
|
7
7
|
|
8
8
|
GEM
|
9
9
|
remote: https://rubygems.org/
|
10
10
|
specs:
|
11
|
-
diff-lcs (1.
|
12
|
-
flickraw (0.9.
|
13
|
-
mini_magick (4.
|
14
|
-
rake (
|
15
|
-
rspec (3.
|
16
|
-
rspec-core (~> 3.
|
17
|
-
rspec-expectations (~> 3.
|
18
|
-
rspec-mocks (~> 3.
|
19
|
-
rspec-core (3.
|
20
|
-
rspec-support (~> 3.
|
21
|
-
rspec-expectations (3.
|
11
|
+
diff-lcs (1.5.0)
|
12
|
+
flickraw (0.9.10)
|
13
|
+
mini_magick (4.12.0)
|
14
|
+
rake (13.0.6)
|
15
|
+
rspec (3.12.0)
|
16
|
+
rspec-core (~> 3.12.0)
|
17
|
+
rspec-expectations (~> 3.12.0)
|
18
|
+
rspec-mocks (~> 3.12.0)
|
19
|
+
rspec-core (3.12.2)
|
20
|
+
rspec-support (~> 3.12.0)
|
21
|
+
rspec-expectations (3.12.3)
|
22
22
|
diff-lcs (>= 1.2.0, < 2.0)
|
23
|
-
rspec-support (~> 3.
|
24
|
-
rspec-mocks (3.
|
23
|
+
rspec-support (~> 3.12.0)
|
24
|
+
rspec-mocks (3.12.5)
|
25
25
|
diff-lcs (>= 1.2.0, < 2.0)
|
26
|
-
rspec-support (~> 3.
|
27
|
-
rspec-support (3.
|
26
|
+
rspec-support (~> 3.12.0)
|
27
|
+
rspec-support (3.12.1)
|
28
28
|
|
29
29
|
PLATFORMS
|
30
|
-
|
30
|
+
arm64-darwin-22
|
31
31
|
|
32
32
|
DEPENDENCIES
|
33
33
|
bundler
|
@@ -36,4 +36,4 @@ DEPENDENCIES
|
|
36
36
|
rspec
|
37
37
|
|
38
38
|
BUNDLED WITH
|
39
|
-
|
39
|
+
2.4.13
|
data/LICENSE.text
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License
|
2
2
|
|
3
|
-
Copyright 2017 Amandeep Bhamra
|
3
|
+
Copyright 2017 Amandeep Singh Bhamra
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
6
|
|
data/Rakefile
CHANGED
data/bin/console
CHANGED
data/bin/flico
CHANGED
data/flico.gemspec
CHANGED
@@ -1,26 +1,26 @@
|
|
1
|
-
#
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
lib = File.expand_path('lib', __dir__)
|
3
4
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
5
|
|
5
6
|
Gem::Specification.new do |spec|
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
spec.name = 'flico'
|
8
|
+
spec.version = '2.0.0'
|
9
|
+
spec.summary = 'A CLI tool to create collage from keywords using Flickr'
|
10
|
+
spec.homepage = 'https://github.com/amandeepbhamra/flico'
|
11
|
+
spec.license = 'MIT'
|
12
|
+
|
13
|
+
spec.authors = ['Amandeep Singh Bhamra']
|
14
|
+
spec.email = ['amandeep.bhamra@gmail.com']
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
spec.bindir = 'bin'
|
18
|
-
spec.executables = 'flico'
|
19
|
-
spec.require_paths = ['lib']
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.bindir = 'bin'
|
18
|
+
spec.executables = 'flico'
|
19
|
+
spec.require_paths = ['lib']
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
21
|
+
spec.add_dependency 'flickraw', '~> 0.9.10'
|
22
|
+
spec.add_dependency 'mini_magick', '~> 4.12'
|
23
|
+
spec.add_development_dependency 'bundler', '~> 2.4', '>= 2.4.14'
|
24
|
+
spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.6'
|
25
|
+
spec.add_development_dependency 'rspec', '~> 3.12'
|
26
26
|
end
|
data/lib/flico/app.rb
CHANGED
@@ -1,45 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'flico/dictionary'
|
4
|
+
require 'flico/collager'
|
5
|
+
require 'flico/image_file'
|
6
|
+
|
1
7
|
module Flico
|
8
|
+
class ApplicationError < StandardError; end
|
9
|
+
|
10
|
+
class App
|
11
|
+
KEYWORDS_COUNT = 10
|
12
|
+
|
13
|
+
attr_reader :keywords, :options
|
14
|
+
|
15
|
+
def initialize(keywords, options)
|
16
|
+
@keywords = add_missing_keywords(keywords)
|
17
|
+
@options = options
|
18
|
+
end
|
19
|
+
|
20
|
+
def create
|
21
|
+
options[:file_name] = prepare_file_name(options)
|
22
|
+
|
23
|
+
Collager.new(prepare_images, options[:file_name]).save
|
24
|
+
|
25
|
+
puts "Flicollage saved at #{options[:file_name]}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def prepare_images
|
29
|
+
keywords.map { |keyword| ImageFile.new(keyword).fetch_from_flickr }
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def add_missing_keywords(words)
|
35
|
+
words_count = words.size
|
36
|
+
return words if words_count == KEYWORDS_COUNT
|
37
|
+
|
38
|
+
words += Dictionary.new.words(KEYWORDS_COUNT - words_count)
|
39
|
+
puts "KEYWORDS: #{words}"
|
40
|
+
words
|
41
|
+
end
|
2
42
|
|
3
|
-
|
4
|
-
|
5
|
-
class NoImage < StandardError; end
|
6
|
-
class FetchingError < StandardError; end
|
7
|
-
|
8
|
-
class App
|
9
|
-
|
10
|
-
attr_reader :resources
|
11
|
-
|
12
|
-
def initialize(resources)
|
13
|
-
@resources = resources
|
14
|
-
end
|
15
|
-
|
16
|
-
def create_collage
|
17
|
-
image_urls = []
|
18
|
-
begin
|
19
|
-
image_urls.push get_images
|
20
|
-
end while image_urls.count < 10
|
21
|
-
collage(image_urls)
|
22
|
-
end
|
23
|
-
|
24
|
-
def collage(images)
|
25
|
-
resources.save_collage.call(resources.collager.call(images))
|
26
|
-
end
|
27
|
-
|
28
|
-
def get_images
|
29
|
-
keywords_count = 10
|
30
|
-
keyword = resources.dictionary.call
|
31
|
-
image_url = resources.flickr_api.call(keyword)
|
32
|
-
downloaded_image = resources.fetch_image.call(image_url)
|
33
|
-
rescue NoImage => e
|
34
|
-
puts "Image not found for keyword '#{keyword}'. Message: #{e.message}. Retrying"
|
35
|
-
unless (tries -= 1) > 0
|
36
|
-
raise ApplicationError, "Failed getting image after retrying #{MAX_KEYWORD_RETRIES} times"
|
37
|
-
else
|
38
|
-
retry
|
39
|
-
end
|
40
|
-
rescue FetchingError => e
|
41
|
-
raise ApplicationError, e.message
|
42
|
-
end
|
43
|
-
end
|
43
|
+
def prepare_file_name(options)
|
44
|
+
return options[:file_name] unless options[:file_name].nil?
|
44
45
|
|
46
|
+
puts 'Enter file name for collage (press ENTER to use default)'
|
47
|
+
file_name = $stdin.gets.strip
|
48
|
+
file_name.empty? ? "flicollage-#{Time.now.strftime('%Y%m%d%H%M%S')}.png" : file_name
|
49
|
+
end
|
50
|
+
end
|
45
51
|
end
|
data/lib/flico/collager.rb
CHANGED
@@ -1,45 +1,25 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'mini_magick'
|
3
4
|
|
4
5
|
module Flico
|
5
|
-
|
6
|
-
|
7
|
-
attr_reader :grid
|
8
|
-
|
9
|
-
def initialize(grid=Grid.new)
|
10
|
-
@grid = grid
|
11
|
-
end
|
12
|
-
|
13
|
-
def call(image_urls)
|
14
|
-
images = image_urls.map { |p| MiniMagick::Image.open p.path }
|
15
|
-
temp_file = Tempfile.new ['collage_maker', '.png']
|
16
|
-
canvas = MiniMagick::Tool::Convert.new do |i|
|
17
|
-
i.size "#{grid.canv_width}x#{grid.canv_height}"
|
18
|
-
i.xc "white"
|
19
|
-
i << temp_file.path
|
20
|
-
end
|
21
|
-
|
22
|
-
resized_images = image_urls.map.with_index do |path, idx|
|
23
|
-
image = MiniMagick::Image.open path.path
|
24
|
-
image.crop(grid.crop_rectangle(idx, image.width, image.height).to_mm)
|
25
|
-
image.resize(grid.resize_rectangle(idx, image.width, image.height).to_mm)
|
26
|
-
print_to_canvas(image, grid.cell_rectangle(idx), temp_file)
|
27
|
-
end
|
28
|
-
temp_file
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
6
|
+
class Collager
|
7
|
+
attr_reader :images_url, :file_name
|
32
8
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
c.geometry rectangle.to_mm
|
38
|
-
end
|
39
|
-
result.write temp_file.path
|
40
|
-
temp_file.rewind
|
41
|
-
end
|
42
|
-
|
43
|
-
end
|
9
|
+
def initialize(images_url, file_name)
|
10
|
+
@images_url = images_url
|
11
|
+
@file_name = file_name
|
12
|
+
end
|
44
13
|
|
14
|
+
def save
|
15
|
+
montage = MiniMagick::Tool::Montage.new
|
16
|
+
montage.density '600'
|
17
|
+
montage.tile '5X2'
|
18
|
+
montage.geometry '+10+20'
|
19
|
+
montage.border '5'
|
20
|
+
images_url.each { |image_url| montage << image_url }
|
21
|
+
montage << file_name
|
22
|
+
montage.call
|
23
|
+
end
|
24
|
+
end
|
45
25
|
end
|
data/lib/flico/dictionary.rb
CHANGED
@@ -1,30 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Flico
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
4
|
+
class Dictionary
|
5
|
+
PATH = '/usr/share/dict/words'
|
6
|
+
ALPHABETS_LOWER_LIMIT = 3
|
7
|
+
ALPHABETS_UPPER_LIMIT = 10
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
@dictionary_path = validate(PATH)
|
11
|
+
@words = []
|
12
|
+
end
|
9
13
|
|
10
|
-
|
11
|
-
|
12
|
-
|
14
|
+
def words(count)
|
15
|
+
@words += pick_words(count)
|
16
|
+
end
|
13
17
|
|
14
|
-
|
15
|
-
@words += keywords
|
16
|
-
end
|
18
|
+
private
|
17
19
|
|
18
|
-
|
20
|
+
def pick_words(count)
|
21
|
+
File.readlines(PATH).select do |word|
|
22
|
+
word.size > ALPHABETS_LOWER_LIMIT && word.size < ALPHABETS_UPPER_LIMIT
|
23
|
+
end.sample(count).map(&:strip)
|
24
|
+
end
|
19
25
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
selected_line.strip
|
26
|
-
end
|
27
|
-
|
28
|
-
end
|
26
|
+
def validate(path)
|
27
|
+
unless File.exist?(path)
|
28
|
+
warn "Exiting due to missing dictionary at path: #{path}"
|
29
|
+
exit 1
|
30
|
+
end
|
29
31
|
|
32
|
+
path
|
33
|
+
end
|
34
|
+
end
|
30
35
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'flickraw'
|
4
|
+
|
5
|
+
module Flico
|
6
|
+
module Flickr
|
7
|
+
class BaseApi
|
8
|
+
attr_reader :key, :secret, :api
|
9
|
+
|
10
|
+
def initialize
|
11
|
+
@key = ENV['FLICKR_KEY']
|
12
|
+
@secret = ENV['FLICKR_SECRET']
|
13
|
+
@api = flickraw_api
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_photo(keyword)
|
17
|
+
valid_keyword?(keyword)
|
18
|
+
|
19
|
+
puts "Searching images for keyword: #{keyword}"
|
20
|
+
|
21
|
+
results = search_api(keyword)
|
22
|
+
|
23
|
+
results.count.zero? ? (warn "No Image for keyword: #{keyword}") : photo_url(results.first.id)
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def flickraw_api
|
29
|
+
valid_secrets?
|
30
|
+
|
31
|
+
FlickRaw.api_key = key
|
32
|
+
FlickRaw.shared_secret = secret
|
33
|
+
FlickRaw::Flickr.new
|
34
|
+
end
|
35
|
+
|
36
|
+
def valid_secrets?
|
37
|
+
if key.nil? || secret.nil?
|
38
|
+
warn 'Exiting due to missing required environment variables: FLICKR_KEY or FLICKR_SECRET'
|
39
|
+
exit 1
|
40
|
+
end
|
41
|
+
|
42
|
+
true
|
43
|
+
end
|
44
|
+
|
45
|
+
def search_api(keyword)
|
46
|
+
api.photos.search({ text: keyword, per_page: 10, sort: 'interestingness-desc' })
|
47
|
+
end
|
48
|
+
|
49
|
+
def info_api(image)
|
50
|
+
api.photos.getInfo(photo_id: image)
|
51
|
+
end
|
52
|
+
|
53
|
+
def size_api(id)
|
54
|
+
api.photos.getSizes(photo_id: id)
|
55
|
+
end
|
56
|
+
|
57
|
+
def photo_url(id)
|
58
|
+
size_api(id).find { |v| v['label'] == 'Square' }['source']
|
59
|
+
end
|
60
|
+
|
61
|
+
def valid_keyword?(keyword)
|
62
|
+
warn "Missing Keywords (can't be nil)" if keyword.nil?
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'flico/flickr/base_api'
|
4
|
+
|
5
|
+
module Flico
|
6
|
+
class NoImage < StandardError; end
|
7
|
+
class FetchingError < StandardError; end
|
8
|
+
|
9
|
+
class ImageFile
|
10
|
+
attr_accessor :keyword
|
11
|
+
|
12
|
+
def initialize(keyword)
|
13
|
+
@keyword = keyword
|
14
|
+
end
|
15
|
+
|
16
|
+
def fetch_from_flickr
|
17
|
+
Flickr::BaseApi.new.get_photo(keyword)
|
18
|
+
rescue NoImage => e
|
19
|
+
puts "Image not found for keyword '#{keyword}'. Message: #{e.message}. Retrying"
|
20
|
+
unless (tries -= 1).positive?
|
21
|
+
raise ApplicationError, "Failed getting image after retrying #{MAX_KEYWORD_RETRIES} times"
|
22
|
+
end
|
23
|
+
|
24
|
+
retry
|
25
|
+
rescue FetchingError => e
|
26
|
+
raise ApplicationError, e.message
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/flico.rb
CHANGED
@@ -1,79 +1,25 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
2
3
|
require 'ostruct'
|
3
4
|
require 'optparse'
|
4
5
|
require 'flico/app'
|
5
|
-
require 'flico/flickr_command'
|
6
|
-
require 'flico/dictionary'
|
7
|
-
require 'flico/saver'
|
8
|
-
require 'flico/collager'
|
9
6
|
|
10
7
|
module Flico
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
resources = validate_resources(keywords, options)
|
29
|
-
App.new(resources).create_collage
|
30
|
-
end
|
31
|
-
|
32
|
-
def self.validate_resources(keywords, options={})
|
33
|
-
dictionary = validate_dictionary
|
34
|
-
dictionary.append(keywords)
|
35
|
-
col = SaveCollage.new
|
36
|
-
col.output_file_name = options[:file_name]
|
37
|
-
|
38
|
-
Resource.new(flickr_api: validate_flickr_api, dictionary: dictionary, fetch_image: FetchImage.new, collager: Collager.new, save_collage: col)
|
39
|
-
end
|
40
|
-
|
41
|
-
def self.validate_flickr_api
|
42
|
-
key, secret = ENV['FLICKR_KEY'], ENV['FLICKR_SECRET']
|
43
|
-
unless key == nil && secret == nil
|
44
|
-
FlickRaw.api_key = key
|
45
|
-
FlickRaw.shared_secret = secret
|
46
|
-
FlickrCommand.setup(FlickRaw::Flickr.new)
|
47
|
-
else
|
48
|
-
STDERR.puts "Performing AutoExit due to missing required environment variables: FLICKR_KEY and FLICKR_SECRET"
|
49
|
-
exit 1
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def self.validate_dictionary
|
54
|
-
dictionary_path = '/usr/share/dict/words'
|
55
|
-
if File.exist?(dictionary_path)
|
56
|
-
dictionary = Dictionary.new(dictionary_path)
|
57
|
-
else
|
58
|
-
STDERR.puts "Performing AutoExit due to missing required dictionary at path: #{dictionary_path}"
|
59
|
-
exit 1
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
end
|
64
|
-
|
65
|
-
class Resource
|
66
|
-
|
67
|
-
attr_reader :flickr_api, :dictionary, :fetch_image, :collager, :save_collage
|
68
|
-
|
69
|
-
def initialize(params={})
|
70
|
-
@flickr_api = params.fetch(:flickr_api)
|
71
|
-
@dictionary = params.fetch(:dictionary)
|
72
|
-
@fetch_image = params.fetch(:fetch_image)
|
73
|
-
@collager = params.fetch(:collager)
|
74
|
-
@save_collage = params.fetch(:save_collage)
|
75
|
-
end
|
76
|
-
|
77
|
-
end
|
78
|
-
|
8
|
+
class CommandLineInterface
|
9
|
+
def self.parse_args(args)
|
10
|
+
options = {}
|
11
|
+
OptionParser.new do |opts|
|
12
|
+
opts.banner = 'Usage: flico [options] [10 keywords with space as delimiter]'
|
13
|
+
opts.on('-f', '--file_name [FileName]', 'Collage FileName') do |f|
|
14
|
+
options[:file_name] = f
|
15
|
+
end
|
16
|
+
end.parse!(args)
|
17
|
+
[args, options]
|
18
|
+
end
|
19
|
+
|
20
|
+
def self.start(args)
|
21
|
+
keywords, options = parse_args(args)
|
22
|
+
App.new(keywords, options).create
|
23
|
+
end
|
24
|
+
end
|
79
25
|
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'flico/flickr/base_api'
|
5
|
+
|
6
|
+
describe Flico::Flickr::BaseApi do
|
7
|
+
let(:photo_url) do
|
8
|
+
{
|
9
|
+
'label' => 'Square',
|
10
|
+
'width' => 75,
|
11
|
+
'height' => 75,
|
12
|
+
'source' => 'https://farm3.staticflickr.com/2805/33472607802_42e011bcf5_s.jpg',
|
13
|
+
'url' => 'https://farm3.staticflickr.com/2805/33472607802_42e011bcf5/sizes/sq/',
|
14
|
+
'media' => 'photo'
|
15
|
+
}
|
16
|
+
end
|
17
|
+
|
18
|
+
let(:subject) { described_class.new }
|
19
|
+
|
20
|
+
context 'search success' do
|
21
|
+
before do
|
22
|
+
ENV['FLICKR_KEY'] = 'FOO'
|
23
|
+
ENV['FLICKR_SECRET'] = 'BAR'
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should return image url for search keyword' do
|
27
|
+
allow_any_instance_of(Flico::Flickr::BaseApi).to receive(:flickraw_api).and_return(double('flickraw_api'))
|
28
|
+
allow_any_instance_of(Flico::Flickr::BaseApi).to receive(:get_photo).and_return(photo_url)
|
29
|
+
|
30
|
+
expect(subject.get_photo('Volkswagen Golf in Luxembourg')['source']).to eq('https://farm3.staticflickr.com/2805/33472607802_42e011bcf5_s.jpg')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'flico/dictionary'
|
5
|
+
|
6
|
+
describe Flico::Dictionary do
|
7
|
+
let(:subject) { described_class.new }
|
8
|
+
|
9
|
+
context '#words' do
|
10
|
+
let(:count) { 3 }
|
11
|
+
let(:words) { subject.words(count) }
|
12
|
+
|
13
|
+
it 'should not be empty' do
|
14
|
+
expect(words).to_not be_empty
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'should return 3 random words' do
|
18
|
+
expect(words.count).to eq count
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
# This file was generated by the `rspec --init` command. Conventionally, all
|
2
4
|
# specs live under a `spec` directory, which RSpec adds to the `$LOAD_PATH`.
|
3
5
|
# The generated `.rspec` file contains `--require spec_helper` which will cause
|
@@ -44,57 +46,55 @@ RSpec.configure do |config|
|
|
44
46
|
# triggering implicit auto-inclusion in groups with matching metadata.
|
45
47
|
config.shared_context_metadata_behavior = :apply_to_host_groups
|
46
48
|
|
47
|
-
# The settings below are suggested to provide a good initial experience
|
48
|
-
# with RSpec, but feel free to customize to your heart's content.
|
49
|
-
|
50
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
54
|
-
#
|
55
|
-
|
56
|
-
|
57
|
-
#
|
58
|
-
#
|
59
|
-
#
|
60
|
-
|
61
|
-
|
62
|
-
#
|
63
|
-
#
|
64
|
-
# - http://
|
65
|
-
# - http://
|
66
|
-
#
|
67
|
-
|
68
|
-
|
69
|
-
#
|
70
|
-
#
|
71
|
-
|
72
|
-
|
73
|
-
#
|
74
|
-
#
|
75
|
-
#
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
#
|
84
|
-
#
|
85
|
-
#
|
86
|
-
|
87
|
-
|
88
|
-
#
|
89
|
-
#
|
90
|
-
#
|
91
|
-
#
|
92
|
-
|
93
|
-
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
Kernel.srand config.seed
|
99
|
-
=end
|
49
|
+
# The settings below are suggested to provide a good initial experience
|
50
|
+
# with RSpec, but feel free to customize to your heart's content.
|
51
|
+
# # This allows you to limit a spec run to individual examples or groups
|
52
|
+
# # you care about by tagging them with `:focus` metadata. When nothing
|
53
|
+
# # is tagged with `:focus`, all examples get run. RSpec also provides
|
54
|
+
# # aliases for `it`, `describe`, and `context` that include `:focus`
|
55
|
+
# # metadata: `fit`, `fdescribe` and `fcontext`, respectively.
|
56
|
+
# config.filter_run_when_matching :focus
|
57
|
+
#
|
58
|
+
# # Allows RSpec to persist some state between runs in order to support
|
59
|
+
# # the `--only-failures` and `--next-failure` CLI options. We recommend
|
60
|
+
# # you configure your source control system to ignore this file.
|
61
|
+
# config.example_status_persistence_file_path = "spec/examples.txt"
|
62
|
+
#
|
63
|
+
# # Limits the available syntax to the non-monkey patched syntax that is
|
64
|
+
# # recommended. For more details, see:
|
65
|
+
# # - http://rspec.info/blog/2012/06/rspecs-new-expectation-syntax/
|
66
|
+
# # - http://www.teaisaweso.me/blog/2013/05/27/rspecs-new-message-expectation-syntax/
|
67
|
+
# # - http://rspec.info/blog/2014/05/notable-changes-in-rspec-3/#zero-monkey-patching-mode
|
68
|
+
# config.disable_monkey_patching!
|
69
|
+
#
|
70
|
+
# # This setting enables warnings. It's recommended, but in some cases may
|
71
|
+
# # be too noisy due to issues in dependencies.
|
72
|
+
# config.warnings = true
|
73
|
+
#
|
74
|
+
# # Many RSpec users commonly either run the entire suite or an individual
|
75
|
+
# # file, and it's useful to allow more verbose output when running an
|
76
|
+
# # individual spec file.
|
77
|
+
# if config.files_to_run.one?
|
78
|
+
# # Use the documentation formatter for detailed output,
|
79
|
+
# # unless a formatter has already been configured
|
80
|
+
# # (e.g. via a command-line flag).
|
81
|
+
# config.default_formatter = "doc"
|
82
|
+
# end
|
83
|
+
#
|
84
|
+
# # Print the 10 slowest examples and example groups at the
|
85
|
+
# # end of the spec run, to help surface which specs are running
|
86
|
+
# # particularly slow.
|
87
|
+
# config.profile_examples = 10
|
88
|
+
#
|
89
|
+
# # Run specs in random order to surface order dependencies. If you find an
|
90
|
+
# # order dependency and want to debug it, you can fix the order by providing
|
91
|
+
# # the seed, which is printed after each run.
|
92
|
+
# # --seed 1234
|
93
|
+
# config.order = :random
|
94
|
+
#
|
95
|
+
# # Seed global randomization in this process using the `--seed` CLI option.
|
96
|
+
# # Setting this allows you to use `--seed` to deterministically reproduce
|
97
|
+
# # test failures related to randomization by passing the same `--seed` value
|
98
|
+
# # as the one that triggered the failure.
|
99
|
+
# Kernel.srand config.seed
|
100
100
|
end
|
metadata
CHANGED
@@ -1,86 +1,98 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: flico
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 2.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
-
- Amandeep Bhamra
|
8
|
-
autorequire:
|
7
|
+
- Amandeep Singh Bhamra
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-07-05 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: flickraw
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version:
|
19
|
+
version: 0.9.10
|
20
20
|
type: :runtime
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version:
|
26
|
+
version: 0.9.10
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: mini_magick
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
|
-
- - "
|
31
|
+
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '4.12'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
|
-
- - "
|
38
|
+
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '4.12'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: bundler
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '2.4'
|
45
48
|
- - ">="
|
46
49
|
- !ruby/object:Gem::Version
|
47
|
-
version:
|
50
|
+
version: 2.4.14
|
48
51
|
type: :development
|
49
52
|
prerelease: false
|
50
53
|
version_requirements: !ruby/object:Gem::Requirement
|
51
54
|
requirements:
|
55
|
+
- - "~>"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '2.4'
|
52
58
|
- - ">="
|
53
59
|
- !ruby/object:Gem::Version
|
54
|
-
version:
|
60
|
+
version: 2.4.14
|
55
61
|
- !ruby/object:Gem::Dependency
|
56
62
|
name: rake
|
57
63
|
requirement: !ruby/object:Gem::Requirement
|
58
64
|
requirements:
|
65
|
+
- - "~>"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '13.0'
|
59
68
|
- - ">="
|
60
69
|
- !ruby/object:Gem::Version
|
61
|
-
version:
|
70
|
+
version: 13.0.6
|
62
71
|
type: :development
|
63
72
|
prerelease: false
|
64
73
|
version_requirements: !ruby/object:Gem::Requirement
|
65
74
|
requirements:
|
75
|
+
- - "~>"
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '13.0'
|
66
78
|
- - ">="
|
67
79
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
80
|
+
version: 13.0.6
|
69
81
|
- !ruby/object:Gem::Dependency
|
70
82
|
name: rspec
|
71
83
|
requirement: !ruby/object:Gem::Requirement
|
72
84
|
requirements:
|
73
|
-
- - "
|
85
|
+
- - "~>"
|
74
86
|
- !ruby/object:Gem::Version
|
75
|
-
version: '
|
87
|
+
version: '3.12'
|
76
88
|
type: :development
|
77
89
|
prerelease: false
|
78
90
|
version_requirements: !ruby/object:Gem::Requirement
|
79
91
|
requirements:
|
80
|
-
- - "
|
92
|
+
- - "~>"
|
81
93
|
- !ruby/object:Gem::Version
|
82
|
-
version: '
|
83
|
-
description:
|
94
|
+
version: '3.12'
|
95
|
+
description:
|
84
96
|
email:
|
85
97
|
- amandeep.bhamra@gmail.com
|
86
98
|
executables:
|
@@ -102,19 +114,18 @@ files:
|
|
102
114
|
- flico.gemspec
|
103
115
|
- lib/flico.rb
|
104
116
|
- lib/flico/app.rb
|
105
|
-
- lib/flico/cell.rb
|
106
117
|
- lib/flico/collager.rb
|
107
118
|
- lib/flico/dictionary.rb
|
108
|
-
- lib/flico/
|
109
|
-
- lib/flico/
|
110
|
-
-
|
111
|
-
- spec/
|
119
|
+
- lib/flico/flickr/base_api.rb
|
120
|
+
- lib/flico/image_file.rb
|
121
|
+
- spec/base_api_spec.rb
|
122
|
+
- spec/dictionary_spec.rb
|
112
123
|
- spec/spec_helper.rb
|
113
124
|
homepage: https://github.com/amandeepbhamra/flico
|
114
125
|
licenses:
|
115
126
|
- MIT
|
116
127
|
metadata: {}
|
117
|
-
post_install_message:
|
128
|
+
post_install_message:
|
118
129
|
rdoc_options: []
|
119
130
|
require_paths:
|
120
131
|
- lib
|
@@ -129,9 +140,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
129
140
|
- !ruby/object:Gem::Version
|
130
141
|
version: '0'
|
131
142
|
requirements: []
|
132
|
-
|
133
|
-
|
134
|
-
signing_key:
|
143
|
+
rubygems_version: 3.4.14
|
144
|
+
signing_key:
|
135
145
|
specification_version: 4
|
136
|
-
summary: CLI tool to create collage using Flickr
|
146
|
+
summary: A CLI tool to create collage from keywords using Flickr
|
137
147
|
test_files: []
|
data/lib/flico/cell.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
module Flico
|
2
|
-
|
3
|
-
class Cell
|
4
|
-
attr_reader :x, :y, :width, :height
|
5
|
-
|
6
|
-
def initialize(x=0, y=0, width, height)
|
7
|
-
@x, @y = x, y
|
8
|
-
@width, @height = width, height
|
9
|
-
end
|
10
|
-
|
11
|
-
def aspect_ratio
|
12
|
-
width.to_f / height.to_f
|
13
|
-
end
|
14
|
-
|
15
|
-
def to_mm
|
16
|
-
"#{width}x#{height}+#{x}+#{y}"
|
17
|
-
end
|
18
|
-
|
19
|
-
def ==(o)
|
20
|
-
o.class == self.class && o.state == state
|
21
|
-
end
|
22
|
-
|
23
|
-
protected
|
24
|
-
|
25
|
-
def state
|
26
|
-
[@x, @y, @width, @height]
|
27
|
-
end
|
28
|
-
end
|
29
|
-
|
30
|
-
end
|
data/lib/flico/flickr_command.rb
DELETED
@@ -1,58 +0,0 @@
|
|
1
|
-
module Flico
|
2
|
-
|
3
|
-
class FlickrCommand
|
4
|
-
|
5
|
-
def initialize(search_command:, sizes_command:)
|
6
|
-
@search_command = search_command
|
7
|
-
@sizes_command = sizes_command
|
8
|
-
end
|
9
|
-
|
10
|
-
def self.setup(flickraw)
|
11
|
-
new(search_command: SearchCommand.new(flickraw), sizes_command: SizesCommand.new(flickraw))
|
12
|
-
end
|
13
|
-
|
14
|
-
def call(keyword)
|
15
|
-
unless keyword.nil?
|
16
|
-
puts "Searching images for keyword: #{keyword}"
|
17
|
-
results = @search_command.call(keyword)
|
18
|
-
unless results.count == 0
|
19
|
-
get_image_url(results.first['id'])
|
20
|
-
else
|
21
|
-
STDERR.puts "No Image for keyword: #{keyword}"
|
22
|
-
end
|
23
|
-
else
|
24
|
-
STDERR.puts "Missing Keywords (can't be nil)"
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
def get_image_url(photo_id)
|
31
|
-
sizes = @sizes_command.call(photo_id)
|
32
|
-
mid = sizes.count / 2
|
33
|
-
sizes[mid]['source']
|
34
|
-
end
|
35
|
-
|
36
|
-
end
|
37
|
-
|
38
|
-
|
39
|
-
class FlickrBase
|
40
|
-
attr_reader :api
|
41
|
-
def initialize(flickraw)
|
42
|
-
@api = flickraw
|
43
|
-
end
|
44
|
-
end
|
45
|
-
|
46
|
-
class SearchCommand < FlickrBase
|
47
|
-
def call(keyword)
|
48
|
-
api.photos.search({text: keyword, per_page: 10, sort: 'interestingness-desc'})
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
class SizesCommand < FlickrBase
|
53
|
-
def call(photo_id)
|
54
|
-
api.photos.getSizes(photo_id: photo_id)
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
end
|
data/lib/flico/grid.rb
DELETED
@@ -1,81 +0,0 @@
|
|
1
|
-
require 'flico/cell'
|
2
|
-
|
3
|
-
module Flico
|
4
|
-
|
5
|
-
class Grid
|
6
|
-
|
7
|
-
GRID = [[0,1,1,2], [3,4,5,6], [7,8,8,9]]
|
8
|
-
CANV_WIDTH = 1750
|
9
|
-
CANV_HEIGHT = 1250
|
10
|
-
|
11
|
-
def canv_width
|
12
|
-
CANV_WIDTH
|
13
|
-
end
|
14
|
-
|
15
|
-
def canv_height
|
16
|
-
CANV_HEIGHT
|
17
|
-
end
|
18
|
-
|
19
|
-
def columns
|
20
|
-
GRID.max_by(&:size).size
|
21
|
-
end
|
22
|
-
|
23
|
-
def column_cells
|
24
|
-
GRID.transpose
|
25
|
-
end
|
26
|
-
|
27
|
-
def rows
|
28
|
-
GRID.size
|
29
|
-
end
|
30
|
-
|
31
|
-
def row_cells
|
32
|
-
GRID
|
33
|
-
end
|
34
|
-
|
35
|
-
def cell_width(name)
|
36
|
-
cell_size(CANV_WIDTH, columns, size_multiplier(row_cells, name))
|
37
|
-
end
|
38
|
-
|
39
|
-
def cell_height(name)
|
40
|
-
cell_size(CANV_HEIGHT, rows, size_multiplier(column_cells, name))
|
41
|
-
end
|
42
|
-
|
43
|
-
def crop_rectangle(cell_name, image_width, image_height)
|
44
|
-
x, y = 0, 0
|
45
|
-
image = Cell.new(image_width, image_height)
|
46
|
-
cell = cell_rectangle(cell_name)
|
47
|
-
|
48
|
-
unless image.aspect_ratio >= cell.aspect_ratio
|
49
|
-
final_width = image.width
|
50
|
-
final_height = image.height - (image.height - (image.width / cell.aspect_ratio))
|
51
|
-
else
|
52
|
-
final_width = image_width - (image.width - (image.height * cell.aspect_ratio))
|
53
|
-
final_height = image.height
|
54
|
-
end
|
55
|
-
|
56
|
-
crop_rectangle = Cell.new(x, y, final_width, final_height)
|
57
|
-
crop_rectangle
|
58
|
-
end
|
59
|
-
|
60
|
-
def resize_rectangle(cell_name, image_width, image_height)
|
61
|
-
image = Cell.new(image_width, image_height)
|
62
|
-
cell_rectangle(cell_name)
|
63
|
-
end
|
64
|
-
|
65
|
-
def cell_rectangle(name)
|
66
|
-
row, column = row_cells.find_index {|row| row.include? name }, column_cells.find_index {|row| row.include? name }
|
67
|
-
Cell.new(column * (CANV_WIDTH / columns), row * (CANV_HEIGHT / rows), cell_width(name), cell_height(name))
|
68
|
-
end
|
69
|
-
|
70
|
-
private
|
71
|
-
|
72
|
-
def size_multiplier(table, name)
|
73
|
-
table.map {|row| row.count(name)}.max
|
74
|
-
end
|
75
|
-
|
76
|
-
def cell_size(total, divisions, multiplier)
|
77
|
-
(multiplier == 0) ? 0 : ((total / divisions) * multiplier)
|
78
|
-
end
|
79
|
-
end
|
80
|
-
|
81
|
-
end
|
data/lib/flico/saver.rb
DELETED
@@ -1,44 +0,0 @@
|
|
1
|
-
require 'open-uri'
|
2
|
-
require 'tempfile'
|
3
|
-
require 'fileutils'
|
4
|
-
require 'date'
|
5
|
-
|
6
|
-
|
7
|
-
module Flico
|
8
|
-
|
9
|
-
class FetchingError < StandardError; end
|
10
|
-
|
11
|
-
class FetchImage
|
12
|
-
|
13
|
-
def call(url)
|
14
|
-
tempfile = Tempfile.new 'temp_image'
|
15
|
-
IO.copy_stream(open(url, read_timeout: 5), tempfile)
|
16
|
-
tempfile.rewind
|
17
|
-
tempfile
|
18
|
-
rescue OpenURI::HTTPError => e
|
19
|
-
raise FetchingError, "Couldn't download from: #{url} due to #{e.message}"
|
20
|
-
end
|
21
|
-
|
22
|
-
end
|
23
|
-
|
24
|
-
class SaveCollage
|
25
|
-
|
26
|
-
attr_accessor :output_file_name
|
27
|
-
|
28
|
-
def call(file_path)
|
29
|
-
file_name = output_file_name || validate_file_name
|
30
|
-
FileUtils.mv file_path, file_name
|
31
|
-
puts "Flicollage saved at #{file_name}"
|
32
|
-
end
|
33
|
-
|
34
|
-
private
|
35
|
-
|
36
|
-
def validate_file_name
|
37
|
-
puts "Enter file name for collage (press ENTER to use '#{default}')"
|
38
|
-
file_name = STDIN.gets.strip
|
39
|
-
file_name.empty? ? "flicollage-#{Time.now.strftime('%Y%m%d%H%M%S')}.png" : file_name
|
40
|
-
end
|
41
|
-
|
42
|
-
end
|
43
|
-
|
44
|
-
end
|
data/spec/flickr_command_spec.rb
DELETED
@@ -1,24 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require 'flico/flickr_command'
|
3
|
-
|
4
|
-
describe Flico::FlickrCommand do
|
5
|
-
|
6
|
-
let(:search_results) {[{"id"=>"33472607802", "owner"=>"78759190@N05", "secret"=>"42e011bcf5", "server"=>"2805", "farm"=>3, "title"=>"Volkswagen Golf in Luxembourg", "ispublic"=>1, "isfriend"=>0, "isfamily"=>0}]}
|
7
|
-
let(:sizes_results) {
|
8
|
-
[
|
9
|
-
{"label"=>"Square", "width"=>75, "height"=>75, "source"=>"https://farm3.staticflickr.com/2805/33472607802_42e011bcf5_s.jpg", "url"=>"https://farm3.staticflickr.com/2805/33472607802_42e011bcf5/sizes/sq/", "media"=>"photo"},
|
10
|
-
{"label"=>"Large Square", "width"=>"150", "height"=>"150", "source"=>"https://farm3.staticflickr.com/2805/33472607802_42e011bcf5_q.jpg", "url"=>"https://farm3.staticflickr.com/2805/33472607802_42e011bcf5/sizes/q/", "media"=>"photo"},
|
11
|
-
{"label"=>"Thumbnail", "width"=>"100", "height"=>"66", "source"=>"https://farm3.staticflickr.com/2805/33472607802_42e011bcf5_t.jpg", "url"=>"https://farm3.staticflickr.com/2805/33472607802_42e011bcf5/sizes/t/", "media"=>"photo"}
|
12
|
-
]
|
13
|
-
}
|
14
|
-
let(:subject) { described_class.new(search_command: search_command, sizes_command: sizes_command) }
|
15
|
-
let(:search_command) { double('search', call: search_results ) }
|
16
|
-
let(:sizes_command) { double('sizes', call: sizes_results)}
|
17
|
-
|
18
|
-
context 'search success' do
|
19
|
-
it 'should return image url for search keyword' do
|
20
|
-
expect(subject.call 'Volkswagen Golf in Luxembourg').to eq('https://farm3.staticflickr.com/2805/33472607802_42e011bcf5_q.jpg')
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
end
|