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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6dd50d5580622268a50b6050a2d74bb4cca723d0d0e66c6238e5d86b017b952d
4
- data.tar.gz: a50e621f678f3f8a3479ecef24dca5d448269adb7a714a803225e5ced9d9e1aa
3
+ metadata.gz: bfbcdb81c34decb6b15d1934f9bd8e51d1107e16b04abad502318e4709638b89
4
+ data.tar.gz: 4a6fd7023a681e476bc3fb646e3349eec285ecbdbc4d3e901542c42687bb0345
5
5
  SHA512:
6
- metadata.gz: a1673833cb8cdd13da7166d9bb26576793cf19acfda964e0d94f068d565c73e66b59971921c13dcca0262999c1426860b2cc1e59ee9daa9b1bd15a0af3c34210
7
- data.tar.gz: 0b0efcf5314432863584a1fb640b5353510df81fea003a0794c779c1e53e11dd3e5e1a79f938151319c4091a54017e156f7564b3018942fba4b5a78817bc4420
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
@@ -49,7 +49,7 @@ flickarr auth
49
49
  flickarr export
50
50
  ```
51
51
 
52
- See [HOWTO.md](HOWTO.md) for detailed setup instructions.
52
+ See [doc/HOWTO.md](doc/HOWTO.md) for detailed setup instructions.
53
53
 
54
54
  ## Usage
55
55
 
@@ -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://www.flickr.com/services/apps/create/
26
- 2. Click "Apply for a Non-Commercial Key"
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
+ ![Screenshot of Flickr App Gardn page](how-to-1.png)
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
+ ![Screenshot of Flickr app application page](how-to-2.png)
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
+ ![Screenshot of Flickr API key creation page](how-to-3.png)
37
+
30
38
  4. Submit the form
39
+
31
40
  5. Copy your **Key** and **Secret** from the confirmation page
32
41
 
33
- <!-- TODO: screenshot of Flickr API key creation page -->
34
- <!-- TODO: screenshot of Flickr API key and secret confirmation page -->
42
+ ![Screenshot of Flickr API key and secret confirmation page](how-to-4.png)
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
+ ![Screenshot of Flickr verification page](how-to-5.png)
86
+
76
87
  5. Paste the code back into your terminal
77
88
 
78
- <!-- TODO: screenshot of Flickr OAuth authorization page -->
79
- <!-- TODO: screenshot of Flickr verification code page -->
89
+ ![Screenshot of Flickr auhorization code page](how-to-6.png)
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://www.flickr.com/photos/USERNAME/sets/72157718538273371/
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://www.flickr.com/photos/USERNAME/collections/72157666222057746/
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
- status = post.write(archive_path: archive, overwrite: @overwrite)
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)
@@ -28,13 +28,15 @@ module Flickarr
28
28
  end
29
29
 
30
30
  def dirname
31
- [id, slug].compact.join '_'
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
- set_dirname = [s.id, set_slug.empty? ? nil : set_slug].compact.join('_')
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,
@@ -10,7 +10,21 @@ module Flickarr
10
10
  dest = File.join dir, "#{basename}.#{extension}"
11
11
 
12
12
  FileUtils.mkdir_p dir
13
- Down.download original_url, destination: dest
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
@@ -42,7 +42,8 @@ module Flickarr
42
42
  end
43
43
 
44
44
  def dirname
45
- [id, slug].compact.join '_'
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
- base = [item.id, slug.empty? ? nil : slug].compact.join('_')
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 = info.dates
70
- @location = extract_location info
71
- @sizes = sizes
72
- @urls = extract_urls info
73
- @camera = exif&.camera.to_s if exif
74
- @exif = extract_exif exif
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
- [id, slug].compact.join '_'
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
@@ -3,7 +3,7 @@ require 'net/http'
3
3
  require 'uri'
4
4
 
5
5
  module Flickarr
6
- VERSION = '0.1.5'.freeze
6
+ VERSION = '0.1.7'.freeze
7
7
 
8
8
  class Version
9
9
  RUBYGEMS_URL = 'https://rubygems.org/api/v1/gems/flickarr.json'.freeze
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.5
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