flickarr 0.1.0

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,19 @@
1
+ module Flickarr
2
+ class Client
3
+ class ProfileQuery
4
+ def initialize flickr:, user_id:, rate_limiter:
5
+ @flickr = flickr
6
+ @rate_limiter = rate_limiter
7
+ @user_id = user_id
8
+ end
9
+
10
+ def info
11
+ @rate_limiter.track { @flickr.people.getInfo(user_id: @user_id) }
12
+ end
13
+
14
+ def profile
15
+ @rate_limiter.track { @flickr.profile.getProfile(user_id: @user_id) }
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,94 @@
1
+ require 'flickr'
2
+ require_relative 'client/photo_query'
3
+ require_relative 'client/profile_query'
4
+
5
+ module Flickarr
6
+ class Client
7
+ attr_reader :flickr
8
+
9
+ def initialize config
10
+ raise ConfigError, 'api_key is required' if config.api_key.nil? || config.api_key.empty?
11
+ raise ConfigError, 'shared_secret is required' if config.shared_secret.nil? || config.shared_secret.empty?
12
+
13
+ @flickr = Flickr.new config.api_key, config.shared_secret
14
+ @rate_limiter = RateLimiter.new
15
+
16
+ return unless config.access_token && config.access_secret
17
+
18
+ @flickr.access_token = config.access_token
19
+ @flickr.access_secret = config.access_secret
20
+ end
21
+
22
+ def collections user_id:
23
+ @rate_limiter.track do
24
+ flickr.collections.getTree(user_id: user_id)
25
+ end
26
+ end
27
+
28
+ def photo id:
29
+ PhotoQuery.new(flickr: flickr, id: id, rate_limiter: @rate_limiter)
30
+ end
31
+
32
+ PHOTO_EXTRAS = %w[
33
+ date_taken
34
+ date_upload
35
+ description
36
+ geo
37
+ icon_server
38
+ last_update
39
+ license
40
+ machine_tags
41
+ media
42
+ o_dims
43
+ original_format
44
+ owner_name
45
+ path_alias
46
+ tags
47
+ url_c
48
+ url_l
49
+ url_m
50
+ url_n
51
+ url_o
52
+ url_q
53
+ url_s
54
+ url_sq
55
+ url_t
56
+ url_z
57
+ views
58
+ ].join(',').freeze
59
+
60
+ def photos user_id:, page: 1, per_page: 100
61
+ @rate_limiter.track do
62
+ flickr.people.getPhotos(user_id: user_id, page: page, per_page: per_page, extras: PHOTO_EXTRAS)
63
+ end
64
+ end
65
+
66
+ SET_PHOTO_EXTRAS = %w[
67
+ date_taken
68
+ date_upload
69
+ description
70
+ media
71
+ original_format
72
+ tags
73
+ url_o
74
+ ].join(',').freeze
75
+
76
+ def set_photos photoset_id:, user_id:, page: 1, per_page: 500
77
+ @rate_limiter.track do
78
+ flickr.photosets.getPhotos(
79
+ photoset_id: photoset_id, user_id: user_id, page: page, per_page: per_page, extras: SET_PHOTO_EXTRAS
80
+ )
81
+ end
82
+ end
83
+
84
+ def sets user_id:
85
+ @rate_limiter.track do
86
+ flickr.photosets.getList(user_id: user_id, per_page: 500)
87
+ end
88
+ end
89
+
90
+ def profile user_id:
91
+ ProfileQuery.new(flickr: flickr, user_id: user_id, rate_limiter: @rate_limiter)
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,103 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'slugify'
4
+ require 'yaml'
5
+
6
+ module Flickarr
7
+ class Collection
8
+ COLLECTION_URL_PATTERN = %r{\Ahttps?://(?:www\.)?flickr\.com/photos/[^/]+/collections/(\d+)}
9
+
10
+ def self.id_from_url url
11
+ match = url.to_s.match COLLECTION_URL_PATTERN
12
+ match&.captures&.first
13
+ end
14
+
15
+ attr_reader :description,
16
+ :iconlarge,
17
+ :iconsmall,
18
+ :id,
19
+ :title
20
+
21
+ def initialize collection
22
+ @description = collection.description.to_s
23
+ @iconlarge = collection.iconlarge.to_s
24
+ @iconsmall = collection.iconsmall.to_s
25
+ @id = collection.id
26
+ @title = collection.title.to_s
27
+ @set_refs = collection.set
28
+ end
29
+
30
+ def dirname
31
+ [id, slug].compact.join '_'
32
+ end
33
+
34
+ def sets_to_a
35
+ @set_refs.map do |s|
36
+ set_slug = s.title.to_s.slugify
37
+ set_dirname = [s.id, set_slug.empty? ? nil : set_slug].compact.join('_')
38
+
39
+ {
40
+ description: s.description.to_s,
41
+ id: s.id,
42
+ path: File.join('Sets', set_dirname),
43
+ title: s.title.to_s
44
+ }
45
+ end
46
+ end
47
+
48
+ def slug
49
+ s = title.slugify
50
+ s.empty? ? nil : s
51
+ end
52
+
53
+ def to_h
54
+ {
55
+ description: description,
56
+ iconlarge: iconlarge,
57
+ iconsmall: iconsmall,
58
+ id: id,
59
+ title: title
60
+ }
61
+ end
62
+
63
+ def write archive_path:, overwrite: false
64
+ dir = collection_dir archive_path
65
+ json_path = File.join dir, 'collection.json'
66
+ existed = File.exist? json_path
67
+
68
+ return :skipped if existed && !overwrite
69
+
70
+ FileUtils.mkdir_p dir
71
+ write_collection_files dir: dir
72
+ write_sets_files dir: dir
73
+
74
+ existed ? :overwritten : :created
75
+ end
76
+
77
+ private
78
+
79
+ def collection_dir archive_path
80
+ File.join archive_path, 'Collections', dirname
81
+ end
82
+
83
+ def deep_stringify_keys obj
84
+ case obj
85
+ when Hash then obj.transform_keys(&:to_s).transform_values { deep_stringify_keys it }
86
+ when Array then obj.map { deep_stringify_keys it }
87
+ else obj
88
+ end
89
+ end
90
+
91
+ def write_collection_files dir:
92
+ File.write File.join(dir, 'collection.json'), JSON.pretty_generate(to_h)
93
+ File.write File.join(dir, 'collection.yaml'), YAML.dump(to_h.transform_keys(&:to_s))
94
+ end
95
+
96
+ def write_sets_files dir:
97
+ refs = sets_to_a
98
+
99
+ File.write File.join(dir, 'sets.json'), JSON.pretty_generate(refs)
100
+ File.write File.join(dir, 'sets.yaml'), YAML.dump(deep_stringify_keys(refs))
101
+ end
102
+ end
103
+ end
@@ -0,0 +1,88 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Flickarr
5
+ class Config
6
+ DEFAULT_LIBRARY_PATH = File.join(Dir.home, 'Pictures', 'Flickarr').freeze
7
+
8
+ attr_accessor :access_secret, :access_token, :api_key, :last_page_photos, :last_page_posts, :last_page_videos,
9
+ :library_path, :shared_secret, :total_collections, :total_photos, :total_sets, :total_videos,
10
+ :user_nsid, :username
11
+
12
+ def initialize
13
+ @access_secret = nil
14
+ @access_token = nil
15
+ @api_key = ENV.fetch('FLICKARR_API_KEY', nil)
16
+ @last_page_photos = nil
17
+ @last_page_posts = nil
18
+ @last_page_videos = nil
19
+ @library_path = DEFAULT_LIBRARY_PATH
20
+ @shared_secret = ENV.fetch('FLICKARR_SHARED_SECRET', nil)
21
+ @total_collections = nil
22
+ @total_photos = nil
23
+ @total_sets = nil
24
+ @total_videos = nil
25
+ @user_nsid = nil
26
+ @username = nil
27
+ end
28
+
29
+ def save path
30
+ hash = to_h.transform_keys(&:to_s)
31
+ yaml = YAML.dump hash
32
+ dir = File.dirname path
33
+
34
+ FileUtils.mkdir_p dir
35
+ File.write path, yaml
36
+ end
37
+
38
+ def archive_path
39
+ return nil if username.nil? || username.empty?
40
+
41
+ File.join library_path, username
42
+ end
43
+
44
+ def to_h
45
+ {
46
+ access_secret: access_secret,
47
+ access_token: access_token,
48
+ api_key: api_key,
49
+ last_page_photos: last_page_photos,
50
+ last_page_posts: last_page_posts,
51
+ last_page_videos: last_page_videos,
52
+ library_path: library_path,
53
+ shared_secret: shared_secret,
54
+ total_collections: total_collections,
55
+ total_photos: total_photos,
56
+ total_sets: total_sets,
57
+ total_videos: total_videos,
58
+ user_nsid: user_nsid,
59
+ username: username
60
+ }
61
+ end
62
+
63
+ def self.load path
64
+ config = new
65
+ return config unless File.exist?(path)
66
+
67
+ yaml = YAML.load_file(path, symbolize_names: true)
68
+ return config unless yaml.is_a?(Hash)
69
+
70
+ config.access_secret = yaml[:access_secret] if yaml[:access_secret]
71
+ config.access_token = yaml[:access_token] if yaml[:access_token]
72
+ config.api_key = yaml[:api_key] if yaml[:api_key]
73
+ config.last_page_photos = yaml[:last_page_photos] if yaml[:last_page_photos]
74
+ config.last_page_posts = yaml[:last_page_posts] if yaml[:last_page_posts]
75
+ config.last_page_videos = yaml[:last_page_videos] if yaml[:last_page_videos]
76
+ config.library_path = yaml[:library_path] if yaml[:library_path]
77
+ config.shared_secret = yaml[:shared_secret] if yaml[:shared_secret]
78
+ config.total_collections = yaml[:total_collections] if yaml[:total_collections]
79
+ config.total_photos = yaml[:total_photos] if yaml[:total_photos]
80
+ config.total_sets = yaml[:total_sets] if yaml[:total_sets]
81
+ config.total_videos = yaml[:total_videos] if yaml[:total_videos]
82
+ config.user_nsid = yaml[:user_nsid] if yaml[:user_nsid]
83
+ config.username = yaml[:username] if yaml[:username]
84
+
85
+ config
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,7 @@
1
+ module Flickarr
2
+ class Error < StandardError; end
3
+ class AuthError < Error; end
4
+ class ApiError < Error; end
5
+ class DownloadError < Error; end
6
+ class ConfigError < Error; end
7
+ end
@@ -0,0 +1,34 @@
1
+ module Flickarr
2
+ class License
3
+ LICENSES = {
4
+ '0' => { name: 'All Rights Reserved', url: nil },
5
+ '1' => { name: 'CC BY-NC-SA 2.0', url: 'https://creativecommons.org/licenses/by-nc-sa/2.0/' },
6
+ '2' => { name: 'CC BY-NC 2.0', url: 'https://creativecommons.org/licenses/by-nc/2.0/' },
7
+ '3' => { name: 'CC BY-NC-ND 2.0', url: 'https://creativecommons.org/licenses/by-nc-nd/2.0/' },
8
+ '4' => { name: 'CC BY 2.0', url: 'https://creativecommons.org/licenses/by/2.0/' },
9
+ '5' => { name: 'CC BY-SA 2.0', url: 'https://creativecommons.org/licenses/by-sa/2.0/' },
10
+ '6' => { name: 'CC BY-ND 2.0', url: 'https://creativecommons.org/licenses/by-nd/2.0/' },
11
+ '7' => { name: 'No Known Copyright Restrictions', url: 'https://www.flickr.com/commons/usage/' },
12
+ '8' => { name: 'United States Government Work', url: 'http://www.usa.gov/copyright.shtml' },
13
+ '9' => { name: 'CC0 1.0 Universal', url: 'https://creativecommons.org/publicdomain/zero/1.0/' },
14
+ '10' => { name: 'Public Domain Mark 1.0', url: 'https://creativecommons.org/publicdomain/mark/1.0/' }
15
+ }.freeze
16
+
17
+ attr_reader :id, :name, :url
18
+
19
+ def initialize id
20
+ @id = id.to_s
21
+ entry = LICENSES.fetch(@id, { name: 'Unknown', url: nil })
22
+ @name = entry[:name]
23
+ @url = entry[:url]
24
+ end
25
+
26
+ def to_h
27
+ {
28
+ id: id,
29
+ name: name,
30
+ url: url
31
+ }
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,23 @@
1
+ module Flickarr
2
+ class Photo < Post
3
+ def initialize info:, sizes:, exif: nil
4
+ super
5
+ @extension = info.originalformat.to_s
6
+ end
7
+
8
+ def download archive_path:
9
+ dir = post_dir archive_path
10
+ dest = File.join dir, "#{basename}.#{extension}"
11
+
12
+ FileUtils.mkdir_p dir
13
+ Down.download original_url, destination: dest
14
+ end
15
+
16
+ def original_url
17
+ original = @sizes.find { it.label == 'Original' }
18
+ size = original || @sizes.last
19
+
20
+ size.source
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,139 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'slugify'
4
+ require 'yaml'
5
+
6
+ module Flickarr
7
+ class PhotoSet
8
+ SET_URL_PATTERN = %r{\Ahttps?://(?:www\.)?flickr\.com/photos/[^/]+/(?:sets|albums)/(\d+)}
9
+
10
+ def self.id_from_url url
11
+ match = url.to_s.match SET_URL_PATTERN
12
+ match&.captures&.first
13
+ end
14
+
15
+ attr_reader :count_comments,
16
+ :count_photos,
17
+ :count_videos,
18
+ :count_views,
19
+ :date_create,
20
+ :date_update,
21
+ :description,
22
+ :id,
23
+ :owner,
24
+ :primary,
25
+ :title,
26
+ :username
27
+
28
+ def initialize set:, photo_items:
29
+ @count_comments = set.count_comments.to_s
30
+ @count_photos = set.count_photos.to_i
31
+ @count_videos = set.count_videos.to_i
32
+ @count_views = set.count_views.to_s
33
+ @date_create = set.date_create.to_s
34
+ @date_update = set.date_update.to_s
35
+ @description = set.description.to_s
36
+ @id = set.id
37
+ @owner = set.owner.to_s
38
+ @primary = set.primary.to_s
39
+ @title = set.title.to_s
40
+ @username = set.username.to_s
41
+ @photo_items = photo_items
42
+ end
43
+
44
+ def dirname
45
+ [id, slug].compact.join '_'
46
+ end
47
+
48
+ def photos_to_a
49
+ @photo_items.map do |item|
50
+ path = relative_photo_path item
51
+ {
52
+ datetaken: item.datetaken.to_s,
53
+ dateupload: item.dateupload.to_s,
54
+ id: item.id,
55
+ isprimary: item.isprimary.to_s == '1',
56
+ media: item.media.to_s,
57
+ path: path,
58
+ tags: item.tags.to_s,
59
+ title: item.title.to_s
60
+ }
61
+ end
62
+ end
63
+
64
+ def slug
65
+ s = title.slugify
66
+ s.empty? ? nil : s
67
+ end
68
+
69
+ def to_h
70
+ {
71
+ count_comments: count_comments,
72
+ count_photos: count_photos,
73
+ count_videos: count_videos,
74
+ count_views: count_views,
75
+ date_create: date_create,
76
+ date_update: date_update,
77
+ description: description,
78
+ id: id,
79
+ owner: owner,
80
+ primary: primary,
81
+ title: title,
82
+ username: username
83
+ }
84
+ end
85
+
86
+ def write archive_path:, overwrite: false
87
+ dir = dir_for_set archive_path
88
+ json_path = File.join dir, 'set.json'
89
+ existed = File.exist? json_path
90
+
91
+ return :skipped if existed && !overwrite
92
+
93
+ FileUtils.mkdir_p dir
94
+ write_set_files dir: dir
95
+ write_photos_files dir: dir
96
+
97
+ existed ? :overwritten : :created
98
+ end
99
+
100
+ private
101
+
102
+ def deep_stringify_keys obj
103
+ case obj
104
+ when Hash then obj.transform_keys(&:to_s).transform_values { deep_stringify_keys it }
105
+ when Array then obj.map { deep_stringify_keys it }
106
+ else obj
107
+ end
108
+ end
109
+
110
+ def relative_photo_path item
111
+ ext = item.media.to_s == 'video' ? 'mp4' : item.originalformat.to_s
112
+ slug = item.title.to_s.slugify
113
+ base = [item.id, slug.empty? ? nil : slug].compact.join('_')
114
+ date = if item.datetakenunknown.to_i.zero?
115
+ Date.parse item.datetaken
116
+ else
117
+ Time.at(item.dateupload.to_i).to_date
118
+ end
119
+
120
+ File.join date.strftime('%Y/%m/%d'), "#{base}.#{ext}"
121
+ end
122
+
123
+ def dir_for_set archive_path
124
+ File.join archive_path, 'Sets', dirname
125
+ end
126
+
127
+ def write_photos_files dir:
128
+ refs = photos_to_a
129
+
130
+ File.write File.join(dir, 'photos.json'), JSON.pretty_generate(refs)
131
+ File.write File.join(dir, 'photos.yaml'), YAML.dump(deep_stringify_keys(refs))
132
+ end
133
+
134
+ def write_set_files dir:
135
+ File.write File.join(dir, 'set.json'), JSON.pretty_generate(to_h)
136
+ File.write File.join(dir, 'set.yaml'), YAML.dump(to_h.transform_keys(&:to_s))
137
+ end
138
+ end
139
+ end