bye-flickr 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []