bye-flickr 1.0.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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 80703e70104c8f9222c918040e506979ae025819ad6cd1192687c8f9555a67cc
4
+ data.tar.gz: c207aee6f4ca65c80e839b24622d18c7d1eaf2b512920b3f70de850cb1074882
5
+ SHA512:
6
+ metadata.gz: f5ff0ed9afc666b67a859f6ca3772c523b154da300bd7dd710e017cfe8516520933adb94db1f2a85cbd842f6ad67c2905db09b242a97acd366b5d6c7c742879c
7
+ data.tar.gz: c32483cfd81b38ab3e883472fa74c838c0ddf1140c6a83004b5297f86afff50d58ac0b3be1a56ce9801b9e31857e9d666ddc785a4e4fbe3c86b25142bd27cd93
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Jens Krämer
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,101 @@
1
+ Bye, Flickr!
2
+ ============
3
+
4
+ Simple app to download everything from your flickr account. Your photos will be
5
+ put into a directory structure reflecting Flickr collections and sets. Metadata
6
+ for collections, sets and photos will be stored as JSON files, as well as
7
+ contacts and groups data.
8
+
9
+ Installation
10
+ ------------
11
+
12
+ You need Ruby. I used it with Ruby 2.5, 2.4 should be ok as well. Install the
13
+ gem, run `bye-flickr -h` for usage info.
14
+
15
+ ~~~~
16
+
17
+ $ gem install bye-flickr
18
+ Successfully installed bye-flickr-0.1.0
19
+ 1 gem installed
20
+
21
+ $ bye-flickr -h
22
+ usage: /home/jk/.gem/ruby/2.5.1/bin/bye-flickr [options]
23
+ Required arguments (create API key and secret in the Flickr web interface):
24
+ -d, --dir directory to store data
25
+ -k, --key API key
26
+ -s, --secret API secret
27
+
28
+ Optional arguments, if you already have authorized the app:
29
+ --at Access token
30
+ --as Access token secret
31
+
32
+ Other commands:
33
+ --version print the version
34
+ -h, --help
35
+
36
+ ~~~~
37
+
38
+ Usage
39
+ -----
40
+
41
+ First of all, head to your [Flickr
42
+ account](https://www.flickr.com/services/apps/create/apply/) and create an API
43
+ key. Choose non-commercial and pick any name you like for your 'App'. In the
44
+ end you will get a key and a secret which are what you need for the `-k` and
45
+ `-s` options. Pick a directory in a location with enough disk space and there you go:
46
+
47
+ ~~~~
48
+
49
+ $ bye-flickr -d /space/photos -k lengthyAPIkey -s notsolongsecret
50
+ token_rejected
51
+ Open this url in your browser to complete the authentication process:
52
+ https://api.flickr.com/services/oauth/authorize?oauth_token=some-token&perms=read
53
+ Copy here the number given when you complete the process.
54
+
55
+ ~~~~
56
+
57
+ Do as you're told and go to the URL, authorize the app, copy/paste the nine
58
+ digit number and hit Enter.
59
+
60
+ ~~~~
61
+ 179-386-583
62
+ You are now authenticated as flickrUserName with token some-other-token and secret yetanothersecret.
63
+ ~~~~
64
+
65
+ For subsequent runs you can take note of the access token and secret you just
66
+ got and use them as values for the `--at` and `--as` command line options. This
67
+ will save you from having to authorize the app through the web interface over
68
+ and over again.
69
+
70
+ To show it's working the app prints out a `.` for every photo downloaded, and
71
+ also prints the name of the directory (collection/set) it's currently working
72
+ on. Photos not belonging to any set are, surprise, put into a directory named
73
+ `not in any set`.
74
+
75
+ Depending on the size of your Flickr account and your bandwidth this may take a
76
+ long time. Downloading 26GB from my personal account took a couple of hours on
77
+ my Hetzner server.
78
+
79
+
80
+ Caveats
81
+ -------
82
+
83
+ I built this because I wanted to download my photos, so naturally I cut some
84
+ corners where I could. Two things that I'm aware of which might need improvement are:
85
+
86
+ - Support Flickr's pagination. If you have sets with more than 500 photos in it
87
+ (or more than 500 Photos that are not in any set) you will need that because
88
+ 500 is the maximum number of photos you can get with a single API request. My
89
+ sets aren't that large so I skipped this.
90
+ - There is no support for resuming an unfinished download, the app always
91
+ starts from scratch.
92
+
93
+ Pull Requests welcome :)
94
+
95
+
96
+ License
97
+ -------
98
+
99
+ MIT. See LICENSE for the text.
100
+
101
+
@@ -0,0 +1,44 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bye_flickr'
4
+ require 'slop'
5
+
6
+ o = Slop::Options.new
7
+
8
+ o.separator 'Required arguments (create API key and secret in the Flickr web interface):'
9
+ o.string '-d', '--dir', 'directory to store data', required: true
10
+ o.string '-k', '--key', 'API key', required: true
11
+ o.string '-s', '--secret', 'API secret', required: true
12
+
13
+ o.separator ''
14
+ o.separator 'Optional arguments, if you already have authorized the app:'
15
+ o.string '--at', 'Access token'
16
+ o.string '--as', 'Access token secret'
17
+
18
+ o.separator ''
19
+ o.separator 'Other commands:'
20
+ o.on '--version', 'print the version' do
21
+ puts ByeFlickr::VERSION
22
+ exit
23
+ end
24
+ o.on '-h', '--help' do
25
+ puts o
26
+ exit
27
+ end
28
+
29
+ begin
30
+ opts = Slop::Parser.new(o).parse ARGV
31
+ rescue Slop::Error
32
+ puts $!
33
+ puts o
34
+ exit
35
+ end
36
+
37
+ FlickRaw.api_key = opts[:key]
38
+ FlickRaw.shared_secret = opts[:secret]
39
+
40
+ flickr.access_token = opts[:at] if opts[:at]
41
+ flickr.access_secret = opts[:as] if opts[:as]
42
+
43
+ ByeFlickr::App.new(dir: opts[:dir]).run
44
+
@@ -0,0 +1,6 @@
1
+ require 'flickraw'
2
+
3
+ require 'bye_flickr/app'
4
+
5
+ module ByeFlickr
6
+ end
@@ -0,0 +1,133 @@
1
+ require 'fileutils'
2
+ require 'json'
3
+ require 'pathname'
4
+ require 'bye_flickr/auth'
5
+ require 'bye_flickr/photo_downloader'
6
+ require 'bye_flickr/response_to_json'
7
+
8
+ module ByeFlickr
9
+ class App
10
+ def initialize(dir: '.')
11
+ @basedir = Pathname(dir)
12
+ FileUtils.mkdir_p @basedir
13
+
14
+ @downloader = ByeFlickr::PhotoDownloader.new(@basedir)
15
+ end
16
+
17
+ # This code does not take into account pagination. 500 photos per set (the
18
+ # maximum supported per_page value) is enough for my purposes.
19
+ def run
20
+ user = Auth.call
21
+ @username = user[:username]
22
+ @id = user[:id]
23
+ exit if @username.nil? || @id.nil?
24
+
25
+ # Download photos that are not in any set
26
+ download_not_in_set
27
+
28
+ # Get collection info
29
+ @collections = flickr.collections.getTree
30
+ write_info(
31
+ @collections, path('collections.json')
32
+ )
33
+
34
+ # Get sets info
35
+ @sets = Hash[
36
+ flickr.photosets.getList(per_page: 500).photoset.map{|s|[s.id, s]}
37
+ ]
38
+
39
+ # Download collections and their included sets, removing downloaded sets
40
+ # from the @sets list
41
+ @collections.collection.each do |collection|
42
+ download_collection collection
43
+ end
44
+
45
+ # download the remaining sets, which aren't in any collection
46
+ @sets.values.each do |set|
47
+ download_set set, @basedir
48
+ end
49
+
50
+ # Fetch contacts and groups meta data
51
+ write_info(
52
+ flickr.contacts.getList, path('contacts.json')
53
+ )
54
+ write_info(
55
+ flickr.people.getGroups(user_id: @id), path('groups.json')
56
+ )
57
+
58
+ # wait for photo downloads to finish
59
+ @downloader.wait
60
+ end
61
+
62
+ def path(name, base = @basedir)
63
+ base.join(name.gsub(%r{/}, '_'))
64
+ end
65
+
66
+ def subdir(name, base = @basedir)
67
+ path(name, base).tap do |dir|
68
+ FileUtils.mkdir_p dir
69
+ end
70
+ end
71
+
72
+ def download_not_in_set
73
+ dir = subdir 'not in any set'
74
+ download_photos_to_dir(
75
+ flickr.photos.getNotInSet(extras: 'url_o', per_page: 500),
76
+ dir
77
+ )
78
+ end
79
+
80
+ # can collections be nested? If so, this code ignores them.
81
+ def download_collection(collection)
82
+ dir = subdir collection.title
83
+ FileUtils.mkdir_p dir
84
+ write_info collection, path("#{collection.title}.json")
85
+ collection.set.each do |set|
86
+ download_set set, dir
87
+ @sets.delete set.id # remove this set from the lists of sets to download
88
+ end
89
+ end
90
+
91
+ def download_set(set, basedir)
92
+ dir = subdir set.title, basedir
93
+
94
+ download_photos_to_dir(
95
+ flickr.photosets.getPhotos(photoset_id: set.id,
96
+ per_page: 500,
97
+ user_id: @id,
98
+ extras: 'url_o').photo,
99
+ dir
100
+ )
101
+
102
+ write_info(
103
+ flickr.photosets.getInfo(photoset_id: set.id),
104
+ path("#{set.title}.json", basedir)
105
+ )
106
+ rescue Net::OpenTimeout
107
+ puts "#{$!} - retrying to download Set #{set.title}"
108
+ retry
109
+ end
110
+
111
+ def write_info(info, path)
112
+ (File.open(path, 'wb') << ResponseToJson.(info)).close
113
+ end
114
+
115
+ def download_photos_to_dir(photos, dir)
116
+ puts dir
117
+ photos.each do |photo|
118
+ name = photo.title
119
+ name = name + '.jpg' unless name =~ /\.jpg$/i
120
+ name = "#{photo.id}.jpg"
121
+ @downloader.add_image photo.url_o, path(name, dir).to_s
122
+ end
123
+
124
+ photos.each do |photo|
125
+ write_info(
126
+ flickr.photos.getInfo(photo_id: photo.id),
127
+ path("#{photo.id}.json", dir)
128
+ )
129
+ end
130
+ end
131
+
132
+ end
133
+ end
@@ -0,0 +1,42 @@
1
+ module ByeFlickr
2
+ class Auth
3
+ def self.call
4
+ new.call
5
+ end
6
+
7
+ def call
8
+ unless test_login
9
+ request_auth
10
+ end
11
+ { username: @username, id: @id}
12
+ end
13
+
14
+ def test_login
15
+ login = flickr.test.login
16
+ @username = login.username
17
+ @id = login.id
18
+ true
19
+ rescue
20
+ puts $!
21
+ false
22
+ end
23
+
24
+ def request_auth
25
+ token = flickr.get_request_token
26
+ auth_url = flickr.get_authorize_url(token['oauth_token'], perms: 'read')
27
+
28
+ puts "Open this url in your browser to complete the authentication process:\n#{auth_url}"
29
+ puts "Copy here the number given when you complete the process."
30
+
31
+ verify = $stdin.gets.strip
32
+ flickr.get_access_token(token['oauth_token'], token['oauth_token_secret'], verify)
33
+
34
+ if test_login
35
+ puts "You are now authenticated as #{@username} with token #{flickr.access_token} and secret #{flickr.access_secret}"
36
+ else
37
+ puts "Login failed."
38
+ end
39
+ end
40
+
41
+ end
42
+ end
@@ -0,0 +1,101 @@
1
+ require 'concurrent'
2
+ require 'fileutils'
3
+ require 'net/http/persistent'
4
+ require 'tempfile'
5
+ require 'thread'
6
+
7
+ module ByeFlickr
8
+ class PhotoDownloader
9
+
10
+ Download = Struct.new(:url, :path, :tries)
11
+
12
+ attr_reader :errors
13
+
14
+ def initialize(dir, workers: 2)
15
+ @basedir = dir
16
+ @lock = Mutex.new
17
+ @http = Net::HTTP::Persistent.new
18
+
19
+ @images = Concurrent::Array.new
20
+ @errors = Concurrent::Array.new
21
+
22
+ @tempdir = @basedir.join 'tmp'
23
+ FileUtils.mkdir_p @tempdir
24
+ @running = true
25
+ @workers = 1.upto(workers).map do |i|
26
+ Thread.new{ Worker.new(self).run }
27
+ end
28
+ end
29
+
30
+ def wait
31
+ @running = false
32
+ @workers.each{|t|t.join}
33
+ if @errors.any?
34
+ (File.open(@basedir.join('errors.json'), 'wb') << @errors.to_json).close
35
+ end
36
+ end
37
+
38
+ def add_image(url, path)
39
+ @images << Download.new(url, path, 0)
40
+ end
41
+
42
+ def running?
43
+ !!@running
44
+ end
45
+
46
+ def next
47
+ @images.shift
48
+ end
49
+
50
+ def add_failure(dl)
51
+ @errors << dl
52
+ end
53
+
54
+ def download(dl)
55
+ response = @http.request dl.url
56
+ f = Tempfile.create('bye-flickr-download', @tempdir)
57
+ f << response.body
58
+ f.close
59
+
60
+ @lock.synchronize do
61
+ i = 0
62
+ path = dl.path
63
+ while File.readable?(path)
64
+ i = i+1
65
+ path = "#{dl.path.sub(/\.jpg$/i, '')}_#{i}.jpg"
66
+ end
67
+ FileUtils.mv f, path
68
+ end
69
+
70
+ rescue
71
+ puts "#{$!}:\n#{dl.url} => #{dl.path}"
72
+ dl.tries += 1
73
+ if dl.tries > 2
74
+ puts "giving up on this one"
75
+ add_failure dl
76
+ else
77
+ @images << dl
78
+ end
79
+ end
80
+
81
+
82
+ class Worker
83
+ def initialize(downloader)
84
+ @downloader = downloader
85
+ end
86
+
87
+ def run
88
+ while file = @downloader.next or @downloader.running? do
89
+ if file
90
+ @downloader.download(file)
91
+ print '.'
92
+ else
93
+ # queue is empty but we're still running, wait a bit and try again
94
+ sleep 1
95
+ end
96
+ end
97
+ end
98
+ end
99
+
100
+ end
101
+ end
@@ -0,0 +1,39 @@
1
+ module ByeFlickr
2
+
3
+ class ResponseToJson
4
+ def initialize(r)
5
+ @r = r
6
+ end
7
+
8
+ def self.call(r)
9
+ new(r).serialize.to_json
10
+ end
11
+
12
+ def serialize(o = @r)
13
+ case o
14
+ when FlickRaw::Response
15
+ serialize_response o
16
+ when FlickRaw::ResponseList, Enumerable
17
+ serialize_response_list o
18
+ else
19
+ o
20
+ end
21
+ end
22
+
23
+
24
+ private
25
+
26
+ def serialize_response_list(list)
27
+ list.to_a.map{|o|serialize(o)}
28
+ end
29
+
30
+ def serialize_response(r)
31
+ Hash.new.tap do |hsh|
32
+ r.to_hash.each do |key, value|
33
+ hsh[key] = serialize value
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ end
@@ -0,0 +1,3 @@
1
+ module ByeFlickr
2
+ VERSION = '1.0.1'
3
+ end
metadata ADDED
@@ -0,0 +1,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bye-flickr
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jens Krämer
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-27 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: flickraw
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.9'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.9'
27
+ - !ruby/object:Gem::Dependency
28
+ name: net-http-persistent
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: slop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '4.6'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '4.6'
69
+ - !ruby/object:Gem::Dependency
70
+ name: bundler
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.16'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.16'
83
+ description: This gem will download all photos and as much metadata as possible from
84
+ your Flickr account. Metadata is stored in json files, one file per photo. Collection
85
+ / Set metadata and your group subscriptions and contacts are stored as JSON files,
86
+ as well.
87
+ email:
88
+ - jk@jkraemer.net
89
+ executables:
90
+ - bye-flickr
91
+ extensions: []
92
+ extra_rdoc_files: []
93
+ files:
94
+ - LICENSE
95
+ - README.md
96
+ - bin/bye-flickr
97
+ - lib/bye_flickr.rb
98
+ - lib/bye_flickr/app.rb
99
+ - lib/bye_flickr/auth.rb
100
+ - lib/bye_flickr/photo_downloader.rb
101
+ - lib/bye_flickr/response_to_json.rb
102
+ - lib/bye_flickr/version.rb
103
+ homepage: http://github.com/jkraemer/bye_flickr
104
+ licenses:
105
+ - MIT
106
+ metadata: {}
107
+ post_install_message:
108
+ rdoc_options: []
109
+ require_paths:
110
+ - lib
111
+ required_ruby_version: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: '0'
116
+ required_rubygems_version: !ruby/object:Gem::Requirement
117
+ requirements:
118
+ - - ">="
119
+ - !ruby/object:Gem::Version
120
+ version: '2.0'
121
+ requirements: []
122
+ rubyforge_project: bye-flickr
123
+ rubygems_version: 2.7.6
124
+ signing_key:
125
+ specification_version: 4
126
+ summary: Download all photos and metadata from your Flickr account.
127
+ test_files: []