flickarr 0.1.5 → 0.1.7
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/HELP.txt +3 -0
- data/README.md +1 -1
- data/{HOWTO.md → doc/HOWTO.md} +18 -8
- data/doc/how-to-1.png +0 -0
- data/doc/how-to-2.png +0 -0
- data/doc/how-to-3.png +0 -0
- data/doc/how-to-4.png +0 -0
- data/doc/how-to-5.png +0 -0
- data/doc/how-to-6.png +0 -0
- data/lib/flickarr/cli.rb +38 -1
- data/lib/flickarr/collection.rb +4 -2
- data/lib/flickarr/photo.rb +15 -1
- data/lib/flickarr/photo_set.rb +4 -2
- data/lib/flickarr/post.rb +25 -8
- data/lib/flickarr/version.rb +1 -1
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: bfbcdb81c34decb6b15d1934f9bd8e51d1107e16b04abad502318e4709638b89
|
|
4
|
+
data.tar.gz: 4a6fd7023a681e476bc3fb646e3349eec285ecbdbc4d3e901542c42687bb0345
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 7b8230c529f206ebcc4281c2fd02197ef900595ff27a3b9161800f811616245290f784733b5cc53204d3b7e43ee574727a512c4b2446084947abc88cd37c0819
|
|
7
|
+
data.tar.gz: 253b362b06572d1c68f8fcaf538bfde88dcbe065b2d5784921e90b91cbfa40198186a99b9df090786d375fe5746e05b010ebaa2d111edbb2231f79d76610fb3a
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.1.7] - 2026-04-05
|
|
4
|
+
|
|
5
|
+
- Handle download errors (e.g. 410 Gone) gracefully in single-photo export instead of crashing
|
|
6
|
+
- Retry 410 Gone photo downloads using a URL constructed from `getInfo` fields (`server`, `originalsecret`)
|
|
7
|
+
|
|
8
|
+
## [0.1.6] - 2026-03-31
|
|
9
|
+
|
|
10
|
+
- Add `version` and `help` commands to help output
|
|
11
|
+
- Retry transient Flickr API errors ("service not currently available") with backoff, instead of crashing
|
|
12
|
+
- Truncate very long slugs to prevent `Errno::ENAMETOOLONG` on filenames exceeding 255 bytes
|
|
13
|
+
|
|
3
14
|
## [0.1.5] - 2026-03-23
|
|
4
15
|
|
|
5
16
|
- Add `flickarr version` / `-v` / `--version` command
|
data/HELP.txt
CHANGED
|
@@ -48,3 +48,6 @@ COMMANDS:
|
|
|
48
48
|
export:collections Export all collections (groups of albums)
|
|
49
49
|
export:collections <URL> Export a single collection (groups of albums) with references to albums
|
|
50
50
|
export:profile Export Flickr profile to archive
|
|
51
|
+
|
|
52
|
+
help, -h, --help Show this help
|
|
53
|
+
version, -v, --version Show version (checks for updates)
|
data/README.md
CHANGED
data/{HOWTO.md → doc/HOWTO.md}
RENAMED
|
@@ -22,16 +22,24 @@ gem install flickarr
|
|
|
22
22
|
|
|
23
23
|
You need your own Flickr API key and secret. Flickarr uses these to authenticate with the Flickr API on your behalf.
|
|
24
24
|
|
|
25
|
-
1. Go to https://
|
|
26
|
-
|
|
25
|
+
1. Go to https://flickr.com/services/apps/create and click the [Request an API Key](https://flickr.com/services/apps/create/apply) link in the **Get your API Key** section.
|
|
26
|
+

|
|
27
|
+
|
|
28
|
+
2. On the next page https://flickr.com/services/apps/create/apply click "[Apply for a Non-Commercial Key](https://flickr.com/services/apps/create/noncommercial)"
|
|
29
|
+
|
|
30
|
+

|
|
31
|
+
|
|
27
32
|
3. Fill in the form:
|
|
28
33
|
- **Application Name**: anything you want (e.g. "Flickarr Export")
|
|
29
34
|
- **Description**: anything (e.g. "Personal archive export")
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+
|
|
30
38
|
4. Submit the form
|
|
39
|
+
|
|
31
40
|
5. Copy your **Key** and **Secret** from the confirmation page
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
<!-- TODO: screenshot of Flickr API key and secret confirmation page -->
|
|
42
|
+

|
|
35
43
|
|
|
36
44
|
## 4. Initialize Flickarr
|
|
37
45
|
|
|
@@ -73,10 +81,12 @@ This starts the OAuth flow:
|
|
|
73
81
|
2. Flickr asks you to authorize the app — click "OK, I'll Authorize It"
|
|
74
82
|
3. Flickr shows you a verification code (9 digits, like `888-675-309`)
|
|
75
83
|
4. Copy that code
|
|
84
|
+
|
|
85
|
+

|
|
86
|
+
|
|
76
87
|
5. Paste the code back into your terminal
|
|
77
88
|
|
|
78
|
-
|
|
79
|
-
<!-- TODO: screenshot of Flickr verification code page -->
|
|
89
|
+

|
|
80
90
|
|
|
81
91
|
After authenticating, your access tokens, username, and user ID are saved to the config file. You only need to do this once.
|
|
82
92
|
|
|
@@ -134,7 +144,7 @@ flickarr export:albums
|
|
|
134
144
|
Export a single album by URL:
|
|
135
145
|
|
|
136
146
|
```sh
|
|
137
|
-
flickarr export:sets https://
|
|
147
|
+
flickarr export:sets https://flickr.com/photos/USERNAME/sets/72157718538273371/
|
|
138
148
|
```
|
|
139
149
|
|
|
140
150
|
Collections (groups of albums) work the same way:
|
|
@@ -146,7 +156,7 @@ flickarr export:collections
|
|
|
146
156
|
Export a single collection by URL:
|
|
147
157
|
|
|
148
158
|
```sh
|
|
149
|
-
flickarr export:collections https://
|
|
159
|
+
flickarr export:collections https://flickr.com/photos/USERNAME/collections/72157666222057746/
|
|
150
160
|
```
|
|
151
161
|
|
|
152
162
|
## 10. Check your progress
|
data/doc/how-to-1.png
ADDED
|
Binary file
|
data/doc/how-to-2.png
ADDED
|
Binary file
|
data/doc/how-to-3.png
ADDED
|
Binary file
|
data/doc/how-to-4.png
ADDED
|
Binary file
|
data/doc/how-to-5.png
ADDED
|
Binary file
|
data/doc/how-to-6.png
ADDED
|
Binary file
|
data/lib/flickarr/cli.rb
CHANGED
|
@@ -464,7 +464,14 @@ module Flickarr
|
|
|
464
464
|
return
|
|
465
465
|
end
|
|
466
466
|
|
|
467
|
-
|
|
467
|
+
begin
|
|
468
|
+
status = post.write(archive_path: archive, overwrite: @overwrite)
|
|
469
|
+
rescue Down::Error => e
|
|
470
|
+
warn "Download error on post #{post_id}: #{e.message}"
|
|
471
|
+
log_error archive: archive, post_id: post_id, username: config.username, error: e
|
|
472
|
+
return
|
|
473
|
+
end
|
|
474
|
+
|
|
468
475
|
path = File.join archive, post.folder_path
|
|
469
476
|
|
|
470
477
|
case status
|
|
@@ -510,6 +517,18 @@ module Flickarr
|
|
|
510
517
|
end
|
|
511
518
|
warn "Failed to fetch page #{page} after retries: #{e.message}"
|
|
512
519
|
break
|
|
520
|
+
rescue Flickr::FailedResponse => e
|
|
521
|
+
if transient_flickr_error?(e)
|
|
522
|
+
page_retries -= 1
|
|
523
|
+
if page_retries.positive?
|
|
524
|
+
warn "Transient API error fetching page #{page}: #{e.message} — retrying in 5s (#{page_retries} left)"
|
|
525
|
+
sleep 5
|
|
526
|
+
retry
|
|
527
|
+
end
|
|
528
|
+
warn "Failed to fetch page #{page} after retries: #{e.message}"
|
|
529
|
+
break
|
|
530
|
+
end
|
|
531
|
+
raise
|
|
513
532
|
end
|
|
514
533
|
total = response.total.to_i
|
|
515
534
|
total_pages = response.pages.to_i
|
|
@@ -600,6 +619,17 @@ module Flickarr
|
|
|
600
619
|
log_error archive: archive, post_id: post_id, username: config.username, error: e
|
|
601
620
|
return
|
|
602
621
|
rescue Flickr::FailedResponse => e
|
|
622
|
+
if transient_flickr_error?(e)
|
|
623
|
+
retries -= 1
|
|
624
|
+
if retries.positive?
|
|
625
|
+
warn "Transient API error on post #{post_id}: #{e.message} — retrying in 5s (#{retries} left)"
|
|
626
|
+
sleep 5
|
|
627
|
+
retry
|
|
628
|
+
end
|
|
629
|
+
warn "Failed post #{post_id} after retries: #{e.message}"
|
|
630
|
+
log_error archive: archive, post_id: post_id, username: config.username, error: e
|
|
631
|
+
return
|
|
632
|
+
end
|
|
603
633
|
warn "Error on post #{post_id}: #{e.message}"
|
|
604
634
|
log_error archive: archive, post_id: post_id, username: config.username, error: e
|
|
605
635
|
return
|
|
@@ -618,6 +648,13 @@ module Flickarr
|
|
|
618
648
|
end
|
|
619
649
|
end
|
|
620
650
|
|
|
651
|
+
def transient_flickr_error? error
|
|
652
|
+
message = error.message.to_s.downcase
|
|
653
|
+
code = error.respond_to?(:code) ? error.code.to_s : ''
|
|
654
|
+
|
|
655
|
+
code == '105' || message.include?('not currently available') || message.include?('service unavailable')
|
|
656
|
+
end
|
|
657
|
+
|
|
621
658
|
def log_error archive:, post_id:, username:, error:
|
|
622
659
|
log_path = File.join archive, '_errors.log'
|
|
623
660
|
FileUtils.mkdir_p File.dirname(log_path)
|
data/lib/flickarr/collection.rb
CHANGED
|
@@ -28,13 +28,15 @@ module Flickarr
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def dirname
|
|
31
|
-
|
|
31
|
+
truncated = Post.truncate_slug(slug, id: id, ext: '')
|
|
32
|
+
[id, truncated].compact.join '_'
|
|
32
33
|
end
|
|
33
34
|
|
|
34
35
|
def sets_to_a
|
|
35
36
|
@set_refs.map do |s|
|
|
36
37
|
set_slug = s.title.to_s.slugify
|
|
37
|
-
|
|
38
|
+
set_slug = Post.truncate_slug(set_slug, id: s.id, ext: '')
|
|
39
|
+
set_dirname = [s.id, set_slug].compact.join('_')
|
|
38
40
|
|
|
39
41
|
{
|
|
40
42
|
description: s.description.to_s,
|
data/lib/flickarr/photo.rb
CHANGED
|
@@ -10,7 +10,21 @@ module Flickarr
|
|
|
10
10
|
dest = File.join dir, "#{basename}.#{extension}"
|
|
11
11
|
|
|
12
12
|
FileUtils.mkdir_p dir
|
|
13
|
-
|
|
13
|
+
|
|
14
|
+
begin
|
|
15
|
+
Down.download original_url, destination: dest
|
|
16
|
+
rescue Down::ClientError => e
|
|
17
|
+
raise unless e.message.include?('410') && constructed_original_url
|
|
18
|
+
|
|
19
|
+
warn " Photo #{id}: getSizes URL returned 410, retrying with constructed URL..."
|
|
20
|
+
Down.download constructed_original_url, destination: dest
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def constructed_original_url
|
|
25
|
+
return nil unless @server && @originalsecret
|
|
26
|
+
|
|
27
|
+
"https://live.staticflickr.com/#{@server}/#{id}_#{@originalsecret}_o.#{extension}"
|
|
14
28
|
end
|
|
15
29
|
|
|
16
30
|
def original_url
|
data/lib/flickarr/photo_set.rb
CHANGED
|
@@ -42,7 +42,8 @@ module Flickarr
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def dirname
|
|
45
|
-
|
|
45
|
+
truncated = Post.truncate_slug(slug, id: id, ext: '')
|
|
46
|
+
[id, truncated].compact.join '_'
|
|
46
47
|
end
|
|
47
48
|
|
|
48
49
|
def photos_to_a
|
|
@@ -110,7 +111,8 @@ module Flickarr
|
|
|
110
111
|
def relative_photo_path item
|
|
111
112
|
ext = item.media.to_s == 'video' ? 'mp4' : item.originalformat.to_s
|
|
112
113
|
slug = item.title.to_s.slugify
|
|
113
|
-
|
|
114
|
+
slug = Post.truncate_slug(slug, id: item.id, ext: ext)
|
|
115
|
+
base = [item.id, slug].compact.join('_')
|
|
114
116
|
date = if item.datetakenunknown.to_i.zero?
|
|
115
117
|
Date.parse item.datetaken
|
|
116
118
|
else
|
data/lib/flickarr/post.rb
CHANGED
|
@@ -8,6 +8,7 @@ require 'yaml'
|
|
|
8
8
|
module Flickarr
|
|
9
9
|
class Post
|
|
10
10
|
FLICKR_URL_PATTERN = %r{\Ahttps?://(?:www\.)?flickr\.com/photos/[^/]+/(\d+)}
|
|
11
|
+
MAX_FILENAME_BYTES = 255
|
|
11
12
|
|
|
12
13
|
def self.build info:, sizes:, exif: nil
|
|
13
14
|
case info.media.to_s
|
|
@@ -25,14 +26,25 @@ module Flickarr
|
|
|
25
26
|
id = item.id
|
|
26
27
|
title = item.title.to_s
|
|
27
28
|
slug = title.slugify
|
|
28
|
-
basename = [id, slug.empty? ? nil : slug].compact.join('_')
|
|
29
29
|
ext = item.media.to_s == 'video' ? 'mp4' : item.originalformat.to_s
|
|
30
|
+
slug = truncate_slug(slug, id: id, ext: ext)
|
|
31
|
+
basename = [id, slug].compact.join('_')
|
|
30
32
|
date = compute_date_from_list_item(item)
|
|
31
33
|
folder = date.strftime '%Y/%m/%d'
|
|
32
34
|
|
|
33
35
|
File.join archive_path, folder, "#{basename}.#{ext}"
|
|
34
36
|
end
|
|
35
37
|
|
|
38
|
+
def self.truncate_slug slug, id:, ext:
|
|
39
|
+
return nil if slug.nil? || slug.empty?
|
|
40
|
+
|
|
41
|
+
# "id_slug.ext" must fit in MAX_FILENAME_BYTES
|
|
42
|
+
max_slug = MAX_FILENAME_BYTES - id.to_s.bytesize - ext.to_s.bytesize - 2 # underscore + dot
|
|
43
|
+
return slug if slug.bytesize <= max_slug
|
|
44
|
+
|
|
45
|
+
slug[0, max_slug].chomp('-')
|
|
46
|
+
end
|
|
47
|
+
|
|
36
48
|
def self.compute_date_from_list_item item
|
|
37
49
|
if item.datetakenunknown.to_i.zero?
|
|
38
50
|
Date.parse item.datetaken
|
|
@@ -66,16 +78,21 @@ module Flickarr
|
|
|
66
78
|
@visibility = extract_visibility info
|
|
67
79
|
@owner = extract_owner info
|
|
68
80
|
|
|
69
|
-
@dates
|
|
70
|
-
@
|
|
71
|
-
@
|
|
72
|
-
@
|
|
73
|
-
@
|
|
74
|
-
@
|
|
81
|
+
@dates = info.dates
|
|
82
|
+
@farm = info.respond_to?(:farm) ? info.farm.to_s : nil
|
|
83
|
+
@location = extract_location info
|
|
84
|
+
@originalsecret = info.respond_to?(:originalsecret) ? info.originalsecret.to_s : nil
|
|
85
|
+
@server = info.respond_to?(:server) ? info.server.to_s : nil
|
|
86
|
+
@sizes = sizes
|
|
87
|
+
@urls = extract_urls info
|
|
88
|
+
@camera = exif&.camera.to_s if exif
|
|
89
|
+
@exif = extract_exif exif
|
|
75
90
|
end
|
|
76
91
|
|
|
77
92
|
def basename
|
|
78
|
-
[
|
|
93
|
+
longest_ext = [extension, 'yaml'].max_by(&:bytesize)
|
|
94
|
+
truncated = self.class.truncate_slug(slug, id: id, ext: longest_ext)
|
|
95
|
+
[id, truncated].compact.join '_'
|
|
79
96
|
end
|
|
80
97
|
|
|
81
98
|
def date_taken
|
data/lib/flickarr/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: flickarr
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.7
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shane Becker
|
|
@@ -91,10 +91,16 @@ files:
|
|
|
91
91
|
- CHANGELOG.md
|
|
92
92
|
- CODE_OF_CONDUCT.md
|
|
93
93
|
- HELP.txt
|
|
94
|
-
- HOWTO.md
|
|
95
94
|
- LICENSE.txt
|
|
96
95
|
- README.md
|
|
97
96
|
- Rakefile
|
|
97
|
+
- doc/HOWTO.md
|
|
98
|
+
- doc/how-to-1.png
|
|
99
|
+
- doc/how-to-2.png
|
|
100
|
+
- doc/how-to-3.png
|
|
101
|
+
- doc/how-to-4.png
|
|
102
|
+
- doc/how-to-5.png
|
|
103
|
+
- doc/how-to-6.png
|
|
98
104
|
- exe/flickarr
|
|
99
105
|
- lib/flickarr.rb
|
|
100
106
|
- lib/flickarr/auth.rb
|