photein 0.0.4 → 0.0.5

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.
Files changed (5) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +32 -66
  3. data/lib/photein/version.rb +1 -1
  4. metadata +1 -3
  5. data/bin/xferase +0 -170
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 852660eba8fefe43e1b18967162a149c13f35f85e811f08089403da3f2dd0652
4
- data.tar.gz: 8a6af0e7ce270985f6a427dfc509be093d884baedccd33ac778611285fedf71a
3
+ metadata.gz: f2e520a4d9508ba94604273c88b9f0d3bb1052aae63cef56c856595e5b0b3866
4
+ data.tar.gz: 14633d71731ff23c3350c861bfe33d289bfb53777d6369a6245c203de93dacde
5
5
  SHA512:
6
- metadata.gz: f7fc315331d2de5da4df3fb5aacfd7a58220f3581e3753a2c01802e428432e57ab89ac7d66c44bbecfbd8aae7f2e8966e6a18c1857ed26b68b0e2dc046bbf780
7
- data.tar.gz: 27f432d3dae62069f2197051e523da3db8e3743467ce2dd17be4e7788720ef74090beb345ee98072d1df2cffe86fb7be22bde32d8d4460048eb7603f92537dbe
6
+ metadata.gz: cf796a670cc45ce45238b4e9b0e23641ce1dc21aa67008bb016e9507b0470bebdd4790620bfd5325eabdcbe833165588c32811b244b5f8271ff8fa05d5b2d473
7
+ data.tar.gz: deaea8ad0c1ad541c5e160ea927dbfef115902a7f25b96205e67c780e45477a0def030fe1dc4ba2063cc3c936fb7298ccb9f7f4dca70069e4e7ac4ec7fdd586f
data/README.md CHANGED
@@ -1,29 +1,11 @@
1
- Ph📸tein / 🔀ferase
2
- ===================
1
+ Ph📸tein
2
+ ========
3
3
 
4
- All your photos under one roof.
4
+ A no-nonsense way to organize your personal photo library.
5
5
 
6
- What do they do?
6
+ What does it do?
7
7
  ----------------
8
8
 
9
- `photein` is a CLI utility for batch-importing
10
- your personal photos & videos into a central library.
11
-
12
- `xferase` is an always-on background service that uses `photein`
13
- to continuously import new photos & videos as they come in.
14
-
15
- When combined with other software,
16
- they can be used as a kind of self-hosted / DIY alternative
17
- to cloud photo services like Google Photos or iCloud.
18
-
19
- > ⚠️ **Note**
20
- >
21
- > Unlike true cloud photo services,
22
- > this approach works by keeping a full copy of your photo library
23
- > in local storage on each device you sync to.
24
-
25
- ### Photein
26
-
27
9
  Photein manages your photos **at the filesystem level**.
28
10
  It won’t let you browse or edit your photos,
29
11
  but it will give them a uniform folder structure and filenames,
@@ -55,51 +37,29 @@ based on metadata timestamps, filename timestamps, or file creation times.
55
37
  > ⚠️ **Note**
56
38
  >
57
39
  > If you use a photo management app that decides
58
- > where and how your photos should be stored on your system (like Apple Photos),
59
- > Photein is not for you.
60
-
61
- ### Xferase
62
-
63
- Xferase watches a directory of your choosing (its “inbox”),
64
- and whenever any files are placed there,
65
- it automatically imports them into your photo library.
66
-
67
- It creates and manages two parallel copies of your library
68
- (one original/hi-res, one optimized for web)
69
- and ensures that when you delete a photo from one,
70
- it is automatically removed from the other.
40
+ > where and how your photos should be stored on disk
41
+ > (👀 looking at you, Apple Photos), Photein is not for you.
71
42
 
72
- With the help of [Syncthing][] and systemd,
73
- you can automatically pull new photos from your camera or Android phone
74
- into Xferase’s inbox.
75
- Syncthing can also push your complete, web-optimized photo library
76
- back to your phone (or, say, push your hi-res library out to another machine).
43
+ It can also optimize photos and videos for reduced file size.
77
44
 
78
- [Syncthing]: https://syncthing.net/
45
+ What _doesn’t_ it do?
46
+ ---------------------
79
47
 
80
- Why?
81
- ----
48
+ On its own, Photein is **not** an alternative
49
+ to cloud photo services like Google Photos or iCloud—but
50
+ in combination with other software, it can be.
82
51
 
83
- I could not find any existing software product that:
52
+ If you want to:
84
53
 
85
- 1. imports photos from many sources\* with **no mouse or keyboard interaction**
54
+ * import photos from your phone as soon as you take them
55
+ * import photos from a digital camera / SD card as soon as you plug it in
56
+ * mirror a low-res copy of your entire photo library to your Android phone
86
57
 
87
- \*_e.g.,_ 📱 cell phone / 📷 digital camera / 💬 chat app download
58
+ check out Photein’s sister utility [Xferase][],
59
+ or try the [automation guides][] below.
88
60
 
89
- 2. enforces a clean, consistent, **user-visible directory & filename scheme**
90
-
91
- (I want to be able to access my photos from the file manager,
92
- find them in an “Open...” dialog,
93
- or sync them to other devices with generic tools
94
- like rsync, Dropbox, or Syncthing.)
95
-
96
- 3. comes with **no recurring subscription fee**—or better yet, is FOSS
97
-
98
- (My digital photo/video library belongs to me,
99
- but if I don’t control the pipeline for viewing/managing it,
100
- then does it really?)
101
-
102
- 4. works with Linux
61
+ [Xferase]: https://github.com/rlue/xferase
62
+ [automation guides]: #automation-guides
103
63
 
104
64
  Installation
105
65
  ------------
@@ -110,7 +70,7 @@ $ gem install photein
110
70
 
111
71
  ### Dependencies
112
72
 
113
- * Ruby 2.7+
73
+ * Ruby 2.6+
114
74
  * [ExifTool][]
115
75
  * [MediaInfo][]
116
76
  * ImageMagick (for `--optimize-for=web` option)
@@ -123,8 +83,6 @@ $ gem install photein
123
83
  Usage
124
84
  -----
125
85
 
126
- ### Simple import
127
-
128
86
  ```sh
129
87
  $ photein \
130
88
  --source /media/ricoh_gr/DCIM \ # batch-import photos from here
@@ -145,9 +103,17 @@ Use `photein --help` for a summary of all options.
145
103
 
146
104
  ### Automation guides
147
105
 
148
- * [📷➡️🖥️ Set up auto-import from a digital camera](doc/auto-import-digital-camera.md)
149
- * [📱➡️🖥️ Set up auto-import from an Android phone](doc/auto-import-android-phone.md)
150
- * [📱🔄🖥️ Mirror your library across multiple devices](doc/mirroring-a-library-on-multiple-devices.md)
106
+ Using Photein + systemd, you can:
107
+
108
+ * [📷➡️🖥️ Set up auto-import from a digital camera](guides/auto-import-digital-camera.md)
109
+
110
+ But for more complex tasks, like:
111
+
112
+ * 📱➡️🖥️ Setting up auto-import from an Android phone
113
+ * 📱🔄🖥️ Mirroring your library across multiple devices
114
+
115
+ check out the documentation for [Xferase][],
116
+ an always-on background service based on Photein.
151
117
 
152
118
  Development
153
119
  -----------
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Photein
4
- VERSION = '0.0.4'
4
+ VERSION = '0.0.5'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: photein
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.0.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ryan Lue
@@ -154,13 +154,11 @@ description: ''
154
154
  email: hello@ryanlue.com
155
155
  executables:
156
156
  - photein
157
- - xferase
158
157
  extensions: []
159
158
  extra_rdoc_files: []
160
159
  files:
161
160
  - README.md
162
161
  - bin/photein
163
- - bin/xferase
164
162
  - lib/photein.rb
165
163
  - lib/photein/config.rb
166
164
  - lib/photein/image.rb
data/bin/xferase DELETED
@@ -1,170 +0,0 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
3
-
4
- # TODO: Explain yourself
5
-
6
- # Parse opts -------------------------------------------------------------------
7
- require 'photein'
8
- require 'optparse'
9
-
10
- parser = OptionParser.new do |opts|
11
- opts.version = Photein::VERSION
12
- opts.banner = <<~BANNER
13
- Usage: xferase [--version] [-h | --help] [<args>]
14
- BANNER
15
-
16
- opts.on(
17
- '-i INBOX', '--inbox=INBOX',
18
- 'path to the inbox'
19
- )
20
-
21
- opts.on(
22
- '-s STAGING', '--staging=STAGING',
23
- 'path to the staging directory'
24
- )
25
-
26
- opts.on(
27
- '-o LIB_ORIG', '--lib-orig=LIB_ORIG',
28
- 'path to the original/hi-res library'
29
- )
30
-
31
- opts.on(
32
- '-w LIB_WEB', '--lib-web=LIB_WEB',
33
- 'path to the web-optimized library'
34
- )
35
-
36
- opts.on(
37
- '-d INTERVAL', '--debounce=INTERVAL',
38
- 'wait n seconds for additional files before import'
39
- )
40
- end
41
-
42
- begin
43
- params = {}
44
- parser.parse!(into: params)
45
- params.freeze
46
-
47
- %i[inbox staging lib-orig lib-web].each do |type|
48
- raise "no #{type} directory given" if !params.key?(type)
49
- end
50
- rescue => e
51
- warn(e.message)
52
- warn(parser.help) if e.is_a?(OptionParser::ParseError)
53
- exit 1
54
- end
55
-
56
- # Config -----------------------------------------------------------------------
57
- require 'logger'
58
-
59
- DEBOUNCE_WAIT = params.fetch(:'debounce') { 60 }.to_i # seconds
60
- SRC_DEST_MAP = {
61
- "#{params.fetch(:staging)}/desktop" => params.fetch(:'lib-orig'),
62
- "#{params.fetch(:staging)}/web" => params.fetch(:'lib-web'),
63
- }
64
-
65
- IMPORT_CMD = %(
66
- photein \
67
- --verbose \
68
- --source %s \
69
- --dest %s \
70
- --optimize-for %s
71
- )
72
-
73
- FORMAT_GROUPS = [%w(.jpg .dng .heic), %w(.mov .mp4), %w(.png)]
74
-
75
- LOGGER = Logger.new($stdout).tap(&:info!)
76
-
77
- # Setup ------------------------------------------------------------------------
78
- require 'debouncer'
79
- require 'fileutils'
80
- require 'pathname'
81
- require 'rb-inotify'
82
- require 'tmpdir'
83
-
84
- Thread.abort_on_exception = true
85
- SRC_DEST_MAP.keys.each(&FileUtils.method(:mkdir_p))
86
-
87
- mutex = Mutex.new
88
- debouncer = Debouncer.new(DEBOUNCE_WAIT) do |*files|
89
- SRC_DEST_MAP.keys.each { |dir| FileUtils.ln(files, dir) }
90
- FileUtils.rm(files + Dir["#{params.fetch(:staging)}/web/*.DNG"]) # FIXME (ugly hack)
91
-
92
- break if debouncer.inspect_params[:threads] > 2 # don't let threads pile up
93
-
94
- mutex.synchronize do
95
- SRC_DEST_MAP.each do |src, dest|
96
- system(IMPORT_CMD % [src, dest, src.split('/').last])
97
- end
98
- rescue => e
99
- warn e.message
100
- end
101
- end.reducer(:+)
102
-
103
- # Resume from interruption/failure ---------------------------------------------
104
- debouncer.call unless SRC_DEST_MAP.keys.all?(&Dir.method(:empty?))
105
-
106
- Dir["#{params.fetch(:inbox)}/**/*"]
107
- .select(&File.method(:file?))
108
- .each { |path| debouncer.call(path) }
109
-
110
- # Start ------------------------------------------------------------------------
111
- Thread.new do
112
- call_debouncer = ->(event) do
113
- debouncer.call(event.absolute_name)
114
- rescue => e
115
- warn e.message
116
- end
117
-
118
- import_notifier = INotify::Notifier.new
119
- import_notifier.watch(params.fetch(:inbox), :close_write, &call_debouncer)
120
-
121
- # NOTE: inotify is not recursive,
122
- # so subdirectories must be watched separately!
123
- # (Why do Syncthing folders get special treatment?
124
- # Because ST works by creating hidden tempfiles and moving them upon completion)
125
- stfolders, simple_subdirs = Dir["#{params.fetch(:inbox)}/**/*"]
126
- .select(&File.method(:directory?))
127
- .partition { |dir| Dir.children(dir).include?('.stfolder') }
128
-
129
- simple_subdirs.each { |dir| import_notifier.watch(dir, :close_write, &call_debouncer) }
130
- stfolders.each { |dir| import_notifier.watch(dir, :moved_to, &call_debouncer) }
131
-
132
- import_notifier.run
133
- end
134
-
135
- Thread.new do
136
- sync_deletions = ->(event) do
137
- LOGGER.info("#{event.name} has disappeared!")
138
-
139
- deleted_file = Pathname(event.absolute_name).expand_path
140
-
141
- sister_file = if deleted_file.to_s.start_with?(File.expand_path(params['LIB_ORIG']))
142
- deleted_file.sub(*params.values_at('LIB_ORIG', 'LIB_WEB'))
143
- else
144
- deleted_file.sub(*params.values_at('LIB_WEB', 'LIB_ORIG'))
145
- end
146
-
147
- sister_formats = FORMAT_GROUPS.find { |group| group.include?(deleted_file.extname) }
148
-
149
- related_files = [deleted_file, sister_file]
150
- .product(sister_formats)
151
- .map { |file, ext| file.sub_ext(ext) }
152
- .select(&:file?)
153
-
154
- # (Why mv to tmpdir first? Why not rm straight away?
155
- # Because rm would recursively trigger this inotify callback.)
156
- related_files.each { |f| LOGGER.info("deleting #{f.realpath}") }
157
- FileUtils.mv(related_files, Dir.tmpdir)
158
- FileUtils.rm(related_files.map { |f| File.join(Dir.tmpdir, f.basename) })
159
- end
160
-
161
- deletion_notifier = INotify::Notifier.new
162
-
163
- # NOTE: inotify is not recursive,
164
- # so subdirectories must be watched separately!
165
- (Dir["#{params.fetch(:'lib-orig')}/**/*"] + Dir["#{params.fetch(:'lib-web')}/**/*"])
166
- .select(&File.method(:directory?))
167
- .each { |dir| deletion_notifier.watch(dir, :delete, &sync_deletions) }
168
-
169
- deletion_notifier.run
170
- end.join