photostat 0.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.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ Gemfile.lock
2
+ pkg/*
3
+ .rbx/*
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in photostat.gemspec
4
+ gemspec
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ Photostat
2
+ =========
3
+
4
+ For managing photos, like a hacker. It is already usable for me, but
5
+ work is still in progress and I don't recommend it to people yet.
6
+
7
+ Here's what this app currently does:
8
+
9
+ * it builds a local repository of photos organized in $REPO/$Y-$m directories
10
+ * movies (MOV) are copied in $REPO/movies
11
+ * it takes care of duplicates (the file's identity is based on an MD5 hash + the created date)
12
+ * the creation date is taken from the file's EXIF data
13
+ * in case of movies, it is the date of the last modification
14
+ * on flickr:sync it uploads missing files from local repo to Flickr
15
+ (takes care of duplicates) and also downloads tags / visibility info
16
+
17
+
18
+ Rationale
19
+ ---------
20
+
21
+ I'm tired of graphical UIs that suck.
22
+
23
+ I also need consistent backup in the cloud.
24
+ Now I'm working on Flickr integration, Picasa coming next.
25
+
26
+ I want 2 cheap cloud backups and a properly managed local repository.
27
+ These photos are too important for me.
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/photostat ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ #require "rubygems"
4
+ #require "bundler/setup"
5
+
6
+ require "photostat"
7
+ Photostat.execute
data/junk/filehash.rb ADDED
@@ -0,0 +1,25 @@
1
+ require 'all_your_base'
2
+
3
+ module FileHash
4
+
5
+ def self.file_md5(file)
6
+ out = `md5sum #{file}`
7
+ out =~ /^(\S+)/
8
+ $1.strip
9
+ end
10
+
11
+ def self.calculate(file)
12
+ base16charset = ('0'..'9').to_a + ('a'..'f').to_a
13
+ base16 = AllYourBase::Are.new({:charset => base16charset})
14
+ base62 = AllYourBase::Are.new({:charset => AllYourBase::Are::BASE_62_CHARSET})
15
+
16
+ md5 = file_md5(file).to_s.strip
17
+ ret = base16.convert_to_base_10(md5)
18
+ base62.convert_from_base_10(ret)
19
+ end
20
+ end
21
+
22
+
23
+ if __FILE__ == $0 and ARGV.length
24
+ puts FileHash.calculate(ARGV[0])
25
+ end
@@ -0,0 +1,242 @@
1
+ require "optparse"
2
+ require 'yaml'
3
+ require 'flickraw'
4
+ require 'ostruct'
5
+ require 'benchmark'
6
+
7
+
8
+ module FlickrCLI
9
+
10
+ class Image
11
+ attr_accessor :md5, :local_path, :photoid
12
+
13
+ def self.from_path(path)
14
+ img = Image.new
15
+ img.local_path = path
16
+ img.md5 = calculate_md5(path)
17
+ return img
18
+ end
19
+
20
+ def self.calculate_md5(file)
21
+ out = `md5sum #{file}`
22
+ out =~ /^(\S+)/
23
+ $1.strip
24
+ end
25
+
26
+ def flickr_id!
27
+ if not @photoid and md5
28
+ photos = flickr.photos.search(:user_id => "me", :tags => "checksum:md5=" + md5)
29
+ @photoid = photos[0].id if photos and photos.size > 0
30
+ end
31
+ @photoid
32
+ end
33
+
34
+ def upload(options)
35
+ set_name = options.delete(:set)
36
+
37
+ options[:title] ||= File.basename(local_path)
38
+ options[:tags] ||= ''
39
+ options[:tags] += ' sync checksum:md5=' + md5
40
+ options[:safety_level] ||= '1'
41
+ options[:content_type] ||= '1'
42
+ options[:is_family] ||= '0'
43
+ options[:is_friend] ||= '0'
44
+ options[:is_public] ||= '0'
45
+ options[:hidden] ||= '2'
46
+
47
+ photoid = flickr.upload_photo(local_path, options)
48
+ raise "Upload failed for #{local_path}" if not photoid
49
+
50
+ #if set_name
51
+ # set = flickr.photosets.getList.find {|set| set.title == set_name}
52
+ # if not set
53
+ # set = flickr.photosets.create(:title => set_name, :primary_photo_id => photoid)
54
+ # else
55
+ # flickr.photosets.addPhoto(:photoset_id => set.id, :photo_id => photoid)
56
+ # end
57
+ #end
58
+ end
59
+ end
60
+
61
+
62
+ class ImageService
63
+ def initialize
64
+ @cache_file = ENV['HOME'] + "/.flickr-cli/flickr-id-md5-cache"
65
+ @local_images = []
66
+
67
+ if File.exists? @cache_file
68
+ @cache = YAML::load_file(@cache_file)
69
+ else
70
+ @cache = {}
71
+ end
72
+ end
73
+
74
+ def image_from_path(path)
75
+ img = Image.from_path(path)
76
+ img.photoid = @cache[img.md5]
77
+
78
+ if not img.photoid
79
+ photoid = img.flickr_id!
80
+ @cache[img.md5] = photoid
81
+ end
82
+
83
+ @local_images << img
84
+ return img
85
+ end
86
+
87
+ def save
88
+ File.open(@cache_file, 'w') do |f|
89
+ f.write(@cache.to_yaml)
90
+ end
91
+ end
92
+
93
+ def self.open
94
+ fh = ImageService.new
95
+ yield fh
96
+ ensure
97
+ fh.save if fh
98
+ end
99
+
100
+ def self.search_local_dir(directory)
101
+ files = Dir[File.join(directory, "**", "*")].find_all{|path| path =~ /\.(jpe?g|png)$/i }
102
+ total = files.size
103
+ count = 0
104
+
105
+ benchmarks = []
106
+ last_avg = nil
107
+
108
+ self.open do |service|
109
+ files.each {|path|
110
+
111
+ uploaded = false
112
+ bm = Benchmark.measure do
113
+ count += 1
114
+ img = service.image_from_path(path)
115
+
116
+ stats = OpenStruct.new
117
+ stats.count = count
118
+ stats.total = total
119
+ stats.unit_time = last_avg
120
+
121
+ est = last_avg ? (total - count) * last_avg : nil
122
+ stats.estimate_time = est
123
+
124
+ unless img.photoid
125
+ yield img, stats
126
+ uploaded = true
127
+ end
128
+ end
129
+
130
+ if uploaded
131
+ benchmarks << bm.to_a[-1]
132
+ bp = benchmarks.size >= 10 ? benchmarks[-10,10] : benchmarks
133
+ last_avg = bp.inject{|m,e| m+e} / bp.size
134
+ end
135
+ }
136
+ end
137
+ end
138
+ end
139
+
140
+
141
+ class << self
142
+
143
+ # Takes a period of time in seconds and returns it in human-readable form (down to minutes)
144
+ def time_period_to_s(time_period)
145
+ begin
146
+ time_period = time_period.to_i
147
+ return nil if time_period == 0
148
+ rescue
149
+ return nil
150
+ end
151
+
152
+ interval_array = [ [:weeks, 604800], [:days, 86400], [:hours, 3600], [:mins, 60] ]
153
+ parts = []
154
+
155
+ interval_array.each do |sub|
156
+ if time_period>= sub[1] then
157
+ time_val, time_period = time_period.divmod( sub[1] )
158
+ name = sub[0].to_s
159
+
160
+ if time_val > 0
161
+ parts << "#{time_val} #{sub[0]}"
162
+ end
163
+ end
164
+ end
165
+
166
+ return parts.join(', ')
167
+ end
168
+
169
+ def get_config_options
170
+ conf_file = ENV['HOME'] + "/.flickr-cli/config"
171
+ YAML::load_file(conf_file)
172
+ end
173
+
174
+ def authenticate(config)
175
+ FlickRaw.api_key = config[:app][:api_key]
176
+ FlickRaw.shared_secret = config[:app][:shared_secret]
177
+
178
+ flickr.auth.checkToken :auth_token => config[:user][:auth_token]
179
+ end
180
+
181
+ def synchronize_directory(directory, options)
182
+ config = get_config_options
183
+ auth = authenticate(config)
184
+
185
+ puts "UPLOADING to Flickr ...\n"
186
+ ImageService.search_local_dir(directory) do |img, stats|
187
+ estimate = time_period_to_s(stats.estimate_time)
188
+ $stdout.write "\rProgress: #{stats.count} / #{stats.total}" + (estimate ? " (remaining: #{estimate})" : '')
189
+ img.upload(:tags => "cristian baby sync private", :is_public => '0', :is_friend => "0", :is_family => "1", :safety_level => "1", :hidden => "2")
190
+ end
191
+
192
+ puts "\nDONE!\n"
193
+ end
194
+
195
+ def run
196
+ options = OpenStruct.new
197
+
198
+ command = nil
199
+ command = ARGV[0] if ARGV.length
200
+
201
+ parser = OptionParser.new do |opts|
202
+ opts.program_name = "Flickr Sync"
203
+ opts.banner = "\nUsage: #$0 <directory> [options]\n\n"
204
+
205
+ opts.on_tail("-h", "--help", "Show this message") do
206
+ puts opts
207
+ puts
208
+ exit
209
+ end
210
+ end
211
+
212
+ begin
213
+ directory = ARGV && ARGV[0]
214
+ if not directory or not File.directory?(directory)
215
+ raise OptionParser::MissingArgument, "<directory>"
216
+ end
217
+
218
+ parser.parse!
219
+
220
+ conf_file = ENV['HOME'] + "/.flickr-cli/config"
221
+ if not File.exists?(conf_file)
222
+ raise "Configuration file missing, run ./flickr-conf"
223
+ end
224
+
225
+ rescue Exception => e
226
+ unless e.to_s == 'exit'
227
+ puts
228
+ puts "ERROR: #{e}"
229
+ puts
230
+ end
231
+ exit
232
+ end
233
+
234
+ synchronize_directory(directory, options)
235
+ end
236
+ end
237
+ end
238
+
239
+
240
+ if __FILE__ == $0
241
+ FlickrCLI::run
242
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'thin'
4
+ require 'rack'
5
+
6
+ app = Rack::Builder.new {
7
+ run Rack::Cascade.new([
8
+ Rack::Directory.new("/home/alex/Pictures/")
9
+ ])
10
+ }.to_app
11
+
12
+ Rack::Handler::Thin.run app, :Port => 3000, :Host => "0.0.0.0"
data/junk/trash.rb ADDED
@@ -0,0 +1,41 @@
1
+ puts "Fetching photos in set ..."
2
+
3
+ set = flickr.photosets.getList.find {|set| set.title == options.set_name}
4
+ has_set = (set ? true : false)
5
+
6
+ debugger
7
+
8
+ if has_set
9
+ resp = flickr.photosets.getPhotos(:photoset_id => set.id)
10
+
11
+ puts resp.to_yaml
12
+ resp.each do |photo|
13
+ puts photo.to_yaml
14
+ end
15
+ exit
16
+ end
17
+
18
+ exit
19
+ puts "Indexing local files ..."
20
+
21
+ available_files = {}
22
+ photos_list(directory) do |fpath, md5|
23
+ puts "#{md5} #{fpath}"
24
+ raise Exception, "File conflict: #{fpath} #{available_files[md5]}" if available_files[md5]
25
+ available_files[md5] = fpath
26
+ end
27
+
28
+ puts "Uploading ..."
29
+
30
+ # uploading photos !!!
31
+ available_files.each do |md5, path|
32
+ puts path
33
+ photoid = flickr.upload_photo(path, :title => md5, :tags => "private", :is_public => '0', :is_friend => "0", :is_family => "0", :safety_level => "1", :hidden => "2")
34
+ if not has_set
35
+ set = flickr.photosets.create(:title => "Private", :primary_photo_id => photoid)
36
+ has_set = true
37
+ else
38
+ flickr.photosets.addPhoto(:photoset_id => set.id, :photo_id => photoid)
39
+ end
40
+ puts "... done!"
41
+ end
@@ -0,0 +1,33 @@
1
+ Sequel.migration do
2
+ up do
3
+ create_table :photos do
4
+ primary_key :id
5
+
6
+ String :uid, :null => false
7
+ String :type, :null => false
8
+
9
+ String :local_path, :null => false
10
+ String :visibility, :null => false
11
+ DateTime :created_at, :null => false
12
+
13
+ String :md5, :null => true
14
+
15
+ index :uid, :unique => true
16
+ index :local_path
17
+ index :type
18
+ index :md5
19
+ end
20
+
21
+ create_table :tags do
22
+ primary_key :id
23
+ String :name, :null => false
24
+ String :photo_id, :null => false
25
+
26
+ index [:photo_id, :name], :unique => true
27
+ end
28
+ end
29
+
30
+ down do
31
+ drop_column :photos, :tags
32
+ end
33
+ end
@@ -0,0 +1,5 @@
1
+ Sequel.migration do
2
+ change do
3
+ add_index :photos, :created_at
4
+ end
5
+ end
@@ -0,0 +1,7 @@
1
+ Sequel.migration do
2
+ change do
3
+ alter_table :photos do
4
+ add_column :has_flickr_upload, FalseClass, :default => false, :null => false
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,26 @@
1
+ require 'sequel'
2
+ Sequel.datetime_class = DateTime
3
+
4
+
5
+ module Photostat
6
+ module DB
7
+ def self.instance
8
+ unless @DB
9
+ config = Photostat.config
10
+ system = File.join(config[:repository_path], 'system')
11
+ Dir.mkdir system unless File.directory? system
12
+
13
+ path = File.join(system, 'photostat.db')
14
+ @DB = Sequel.sqlite(path)
15
+ end
16
+ return @DB
17
+ end
18
+
19
+ def self.migrate!
20
+ db = self.instance
21
+ Sequel.extension :migration
22
+ Sequel::Migrator.apply(db, File.dirname(__FILE__))
23
+ end
24
+ end
25
+ end
26
+
@@ -0,0 +1,111 @@
1
+ module Photostat
2
+
3
+ module Plugins
4
+ def self.all_in_order
5
+ unless @plugins_ordered
6
+ @plugins_ordered ||= []
7
+ end
8
+ @plugins_ordered
9
+ end
10
+
11
+ def self.all
12
+ unless @plugins
13
+ @plugins ||= {}
14
+ end
15
+ @plugins
16
+ end
17
+
18
+ def self.load_all!
19
+ # loads all available plugins
20
+ Photostat.root.join('plugins').children.sort.each do |plugin_path|
21
+ next if File.directory? plugin_path
22
+ next unless plugin_path.to_s =~ /\/\d+_\w+.rb$/
23
+ next unless File.basename(plugin_path) != '00_base.rb'
24
+ require plugin_path.to_s
25
+ end
26
+ end
27
+
28
+ class Base
29
+ def self.help_text(msg=nil)
30
+ @help_text = msg if msg
31
+ @help_text
32
+ end
33
+
34
+ def self.exposes_help
35
+ @exposes_help
36
+ end
37
+
38
+ def self.exposes(name=nil, help_text=nil)
39
+ @exposes_names ||= []
40
+ @exposes_help ||= {}
41
+
42
+ if name
43
+ @exposes_names << name
44
+ @exposes_help[name] = help_text if help_text
45
+ end
46
+ @exposes_names
47
+ end
48
+
49
+ def self.register_plugin(sub)
50
+ sub.name =~ /Photostat[:]{2}(\w+)/
51
+ name = $1
52
+
53
+ new_name = ''
54
+ name.each_char do |ch|
55
+ if ch != ch.downcase
56
+ ch = ch.downcase
57
+ ch = "_" + ch if new_name.length > 0
58
+ end
59
+ new_name += ch
60
+ end
61
+
62
+ sub.instance_variable_set :@plugin_name, new_name
63
+ ::Photostat::Plugins.all[new_name] = sub
64
+ ::Photostat::Plugins.all_in_order << sub
65
+ end
66
+
67
+ def self.inherited(sub)
68
+ register_plugin sub
69
+ end
70
+
71
+ def self.included(sub)
72
+ register_plugin sub
73
+ end
74
+
75
+ def self.extended(sub)
76
+ register_plugin sub
77
+ end
78
+
79
+ def self.plugin_name
80
+ @plugin_name
81
+ end
82
+
83
+ def activate!
84
+ # blank
85
+ end
86
+
87
+ def help
88
+ puts "Photostat version #{Photostat::VERSION}"
89
+ puts "Plugin: " + self.class.plugin_name.upcase
90
+ puts
91
+ puts "Usage: photostat <command> [options]*"
92
+ puts "For help on each individual command: photostat <command> --help"
93
+ puts
94
+ puts "Where command is one of the following:"
95
+ self.class.exposes.each do |name|
96
+ msg = " #{self.class.plugin_name}:#{name}"
97
+ if self.class.exposes_help[name]
98
+ msg += "\t- " + self.class.exposes_help[name]
99
+ end
100
+ puts msg
101
+ end
102
+ puts
103
+ end
104
+
105
+ def config
106
+ # blank
107
+ end
108
+ end
109
+ end
110
+
111
+ end
@@ -0,0 +1,141 @@
1
+ module Photostat
2
+
3
+ class Local < Plugins::Base
4
+ include OSUtils
5
+ include FileUtils
6
+
7
+ help_text "Manages your local photos repository"
8
+
9
+ exposes :config, "Configures your local database, repository path and Flickr login"
10
+ exposes :import, "Imports images from a directory path (recursively) to your Photostat repository"
11
+
12
+ def activate!
13
+ unless @activated
14
+ @db = Photostat::DB.instance
15
+ Photostat::DB.migrate!
16
+ @activated = true
17
+ end
18
+ end
19
+
20
+ def import
21
+ opts = Trollop::options do
22
+ opt :path, "Local path to import", :required => true, :type => :string, :short => "-p"
23
+ opt :tags, "List of tags to classify imported pictures", :type => :strings
24
+ opt :visibility, "Choices are 'private', 'protected' and 'public'", :required => true, :type => :string
25
+ opt :move, "Move, instead of copy (better performance, defaults to false, careful)", :type => :boolean
26
+ opt :dry, "Just fake it and print the resulting files", :type => :boolean
27
+ end
28
+
29
+ Trollop::die :path, "must be a valid directory" unless File.directory? opts[:path]
30
+ Trollop::die :visibility, "is invalid. Choices are: private, protected and public" unless ['private', 'protected', 'public'].member? opts[:visibility]
31
+ opts[:tags] ||= []
32
+
33
+ activate!
34
+
35
+ source = File.expand_path opts[:path]
36
+ config = Photostat.config
37
+
38
+ files = files_in_dir(source, :match => /(.jpe?g|.mov)$/i, :absolute? => true)
39
+ count, total = 0, files.length
40
+ puts
41
+
42
+ interrupted = false
43
+ trap("INT") { interrupted = true }
44
+
45
+ files.each do |fpath|
46
+ break if interrupted
47
+ count += 1
48
+
49
+ STDOUT.print "\r - processed: #{count} / #{total}"
50
+ STDOUT.flush
51
+
52
+ if fpath =~ /.jpe?g/i
53
+ type = 'jpg'
54
+ exif = EXIFR::JPEG.new fpath
55
+ dt = (exif.date_time || File.mtime(fpath)).getgm
56
+ else
57
+ type = 'mov'
58
+ dt = File.mtime(fpath).getgm
59
+ end
60
+
61
+ md5 = partial_file_md5 fpath
62
+ uid = dt.strftime("%Y%m%d%H%M%S") + "-" + md5[0,6] + "." + type
63
+
64
+ local_dir = type == 'jpg' ? dt.strftime("%Y-%m") : 'movies'
65
+ local_path = File.join(local_dir, uid)
66
+ dest_dir = File.join(config[:repository_path], local_dir)
67
+ dest_path = File.join(config[:repository_path], local_path)
68
+
69
+ photo = @db[:photos].where(:uid => uid).first
70
+ photo_id = photo ? photo[:id] : nil
71
+
72
+ unless photo || opts[:dry]
73
+ photo_id = @db[:photos].insert(
74
+ :uid => uid,
75
+ :type => type,
76
+ :local_path => local_path,
77
+ :visibility => opts[:visibility],
78
+ :created_at => dt,
79
+ )
80
+ end
81
+
82
+ opts[:tags].each do |name|
83
+ next if opts[:dry]
84
+ next unless @db[:tags].where(:name => name, :photo_id => photo_id).empty?
85
+ @db[:tags].insert(:name => name, :photo_id => photo_id)
86
+ end
87
+
88
+ next if File.exists? dest_path
89
+ next if File.expand_path(dest_path) == File.expand_path(fpath)
90
+
91
+ unless opts[:dry]
92
+ mkdir_p dest_dir
93
+ cp fpath, dest_path unless opts[:move]
94
+ mv fpath, dest_path if opts[:move]
95
+ end
96
+ end
97
+
98
+ if !files or files.length == 0
99
+ puts " - nothing to do"
100
+ end
101
+
102
+ if interrupted
103
+ puts
104
+ puts " - interrupted by user"
105
+ puts
106
+ exit 0
107
+ end
108
+
109
+ puts
110
+ end
111
+
112
+ def config
113
+ puts
114
+ config_file = File.expand_path "~/.photostat"
115
+
116
+ config = {}
117
+ config = YAML::load(File.read config_file) if File.exists? config_file
118
+
119
+ config[:repository_path] ||= "~/Photos"
120
+ config[:repository_path] = input(
121
+ "Wanted location for your Photostat repository",
122
+ :dir? => true, :default => config[:repository_path])
123
+ config[:repository_path] = File.expand_path(config[:repository_path])
124
+
125
+ puts
126
+ unless File.directory? config[:repository_path]
127
+ Dir.mkdir config[:repository_path]
128
+ puts " >>>> repository #{config[:repository_path]} created"
129
+ end
130
+
131
+ File.open(config_file, 'w') do |fh|
132
+ fh.write(YAML::dump(config))
133
+ puts " >>>> generated ~/.photostat config"
134
+ end
135
+
136
+ puts
137
+ end
138
+
139
+ end
140
+
141
+ end
@@ -0,0 +1,225 @@
1
+ module Photostat
2
+ class Flickr < Plugins::Base
3
+ include OSUtils
4
+
5
+ help_text "Manages your Flickr account"
6
+
7
+ exposes :config, "Configures Flickr login"
8
+ exposes :sync, "Uploads local photos to Flickr, downloads Flickr tags / visibility info"
9
+
10
+ def activate!
11
+ return if @activated
12
+ @activated = 1
13
+
14
+ # authenticating to Flickr
15
+ require 'flickraw'
16
+
17
+ @config = Photostat.config
18
+ config unless @config[:flickr]
19
+
20
+ FlickRaw.api_key = @config[:flickr][:api_key]
21
+ FlickRaw.shared_secret = @config[:flickr][:shared_secret]
22
+
23
+ flickr.access_token = @config[:flickr][:access_token]
24
+ flickr.access_secret = @config[:flickr][:access_secret]
25
+
26
+ begin
27
+ login = flickr.test.login
28
+ rescue FlickRaw::FailedResponse => e
29
+ STDERR.puts "ERROR: Flickr Authentication failed : #{e.msg}"
30
+ exit 1
31
+ end
32
+
33
+ Photostat::DB.migrate!
34
+ @db = Photostat::DB.instance
35
+ end
36
+
37
+ def update_md5_info_on_files!
38
+ activate!
39
+
40
+ rs = @db[:photos].where(:type => "jpg", :md5 => nil)
41
+
42
+ count = 0
43
+ total = rs.count
44
+ puts if total > 0
45
+
46
+ @db[:photos].where(:type => "jpg", :md5 => nil).all do |obj|
47
+ md5 = file_md5 File.join(@config[:repository_path], obj[:local_path])
48
+ @db[:photos].where(:uid => obj[:uid]).update(:md5 => md5)
49
+
50
+ count += 1
51
+ STDOUT.write("\r - processed md5 hash for local files: #{count} / #{total}")
52
+ STDOUT.flush
53
+ end
54
+
55
+ puts if total > 0
56
+ end
57
+
58
+ def update_flickr_info!
59
+ activate!
60
+ update_md5_info_on_files!
61
+
62
+ db = Photostat::DB.instance
63
+
64
+ rs = flickr.photos.search(:user_id => "me", :extras => 'machine_tags, tags, date_taken, date_upload', :per_page => 500)
65
+ pages_nr = rs.pages
66
+ page_idx = 1
67
+
68
+ count = 0
69
+ not_local = 0
70
+ not_tagged = 0
71
+ are_valid = 0
72
+ total = rs.total.to_i
73
+
74
+ valid_ids = []
75
+
76
+ puts if total > 0
77
+ while rs.length > 0
78
+
79
+ rs.each do |fphoto|
80
+ count += 1
81
+
82
+ unless fphoto.machine_tags =~ /checksum:md5=(\w+)/
83
+ not_tagged += 1
84
+ next
85
+ end
86
+
87
+ md5 = $1
88
+ obj = db[:photos].where(:md5 => md5).first
89
+
90
+ if not obj
91
+ not_local += 1
92
+ next
93
+ end
94
+
95
+ db[:tags].where(:photo_id => obj[:id]).delete
96
+ if fphoto[:tags] and !fphoto.tags.empty?
97
+ tags = fphoto.tags.split.select{|x| ! ['private', 'sync'].member?(x) && x !~ /checksum:/}
98
+ tags.each do |tag_name|
99
+ db[:tags].insert(:name => tag_name, :photo_id => obj[:id])
100
+ end
101
+ end
102
+
103
+ visibility = 'private'
104
+ visibility = 'protected' if fphoto.isfamily
105
+ visibility = 'public' if fphoto.ispublic
106
+
107
+ db[:photos].where(:md5 => md5).update(:has_flickr_upload => true, :visibility => visibility)
108
+
109
+ STDOUT.write("\r - processed #{count} of #{total}, with #{not_tagged} not tagged on flickr, #{not_local} not local")
110
+ STDOUT.flush
111
+ end
112
+
113
+ page_idx += 1
114
+ break if page_idx > pages_nr
115
+ rs = flickr.photos.search(:user_id => "me", :extras => 'machine_tags', :per_page => 500, :page => page_idx)
116
+ end
117
+
118
+ puts("\r - processed #{count} of #{total}, with #{not_tagged} not tagged and #{not_local} not local")
119
+ end
120
+
121
+ def sync
122
+ activate!
123
+
124
+ update_md5_info_on_files!
125
+ update_flickr_info!
126
+
127
+ db = Photostat::DB.instance
128
+ config = Photostat.config
129
+
130
+ rs = db[:photos].where(:type => 'jpg', :has_flickr_upload => false)
131
+ total = rs.count
132
+ count = 0
133
+
134
+ rs.order('created_at').all do |obj|
135
+ count += 1
136
+ STDOUT.write("\r - uploading file: #{count} / #{total}")
137
+ STDOUT.flush
138
+
139
+ src_path = File.join(config[:repository_path], obj[:local_path])
140
+
141
+ options = {}
142
+ options[:title] = obj[:uid]
143
+ options[:tags] = 'sync checksum:md5=' + obj[:md5]
144
+ options[:safety_level] = '1'
145
+ options[:content_type] = '1'
146
+ options[:is_family] = '0'
147
+ options[:is_friend] = '0'
148
+ options[:is_public] = '0'
149
+ options[:hidden] = '2'
150
+
151
+ begin
152
+ photoid = flickr.upload_photo(src_path, options)
153
+ rescue
154
+ sleep 5
155
+ # retrying again
156
+ photoid = flickr.upload_photo(src_path, options)
157
+ end
158
+ end
159
+
160
+ puts "\n"
161
+ end
162
+
163
+ def config
164
+ puts
165
+ config_file = File.expand_path "~/.photostat"
166
+
167
+ unless File.exists? config_file
168
+ Photostat.configure_plugin! :local
169
+ end
170
+
171
+ config = YAML::load(File.read config_file)
172
+
173
+ config[:flickr] ||= {
174
+ :api_key => nil,
175
+ :shared_secret => nil
176
+ }
177
+
178
+ puts "Configuring Flickr!"
179
+ puts "-------------------"
180
+ puts
181
+ puts "You need to create an app to access your account, see:"
182
+ puts " http://www.flickr.com/services/apps/create/apply/"
183
+ puts "Or if you already have an app key available, find it here:"
184
+ puts " http://www.flickr.com/services/apps/"
185
+ puts
186
+
187
+ config[:flickr][:api_key] = input("Flickr Authentication :: Api Key",
188
+ :default => config[:flickr][:api_key])
189
+ config[:flickr][:shared_secret] = input("Flickr Authentication :: Shared Secret",
190
+ :default => config[:flickr][:shared_secret])
191
+
192
+ require 'flickraw'
193
+
194
+ FlickRaw.api_key = config[:flickr][:api_key]
195
+ FlickRaw.shared_secret = config[:flickr][:shared_secret]
196
+
197
+ token = flickr.get_request_token
198
+ auth_url = flickr.get_authorize_url(token['oauth_token'], :perms => 'delete')
199
+
200
+ puts
201
+ puts "Open this url in your process to complete the authication process:"
202
+ puts " " + auth_url
203
+
204
+ verify = input("Copy here the number given when visiting the link above")
205
+ begin
206
+ flickr.get_access_token(token['oauth_token'], token['oauth_token_secret'], verify)
207
+ login = flickr.test.login
208
+ puts "You are now authenticated as #{login.username} with token #{flickr.access_token} and secret #{flickr.access_secret}"
209
+ rescue FlickRaw::FailedResponse => e
210
+ STDERR.puts "ERROR: Flickr Authentication failed : #{e.msg}"
211
+ exit 1
212
+ end
213
+
214
+ config[:flickr][:access_token] = flickr.access_token
215
+ config[:flickr][:access_secret] = flickr.access_secret
216
+
217
+ File.open(config_file, 'w') do |fh|
218
+ fh.write(YAML::dump(config))
219
+ puts
220
+ puts " >>>> modified ~/.photostat config"
221
+ puts
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,104 @@
1
+ require 'digest/md5'
2
+
3
+ module Photostat
4
+ module OSUtils
5
+ def input(msg, options=nil)
6
+ options ||= {}
7
+ default = options[:default]
8
+ is_dir = options[:dir?]
9
+
10
+ if default and !default.empty?
11
+ msg = msg + " (#{default}): "
12
+ else
13
+ msg = msg + ": "
14
+ end
15
+
16
+ print msg
17
+ resp = nil
18
+
19
+ while not resp
20
+ resp = STDIN.readline.strip
21
+ resp = default if !resp or resp.empty?
22
+ resp = File.expand_path resp if is_dir and resp and !resp.empty?
23
+
24
+ error_msg = "Invalid response"
25
+
26
+ is_not_dir = false
27
+ if resp and !resp.empty? and !File.directory? resp
28
+ is_not_dir = true if File.exists? resp
29
+ is_not_dir = true unless File.directory? File.dirname(resp)
30
+ error_msg = "Invalid directory path"
31
+ end
32
+
33
+ if !resp or resp.empty? or is_not_dir
34
+ puts "ERROR: #{error_msg}!"
35
+ print "\nTry again, #{msg}"
36
+ resp = nil
37
+ end
38
+ end
39
+
40
+ return resp
41
+ end
42
+
43
+ def exec(*params)
44
+ cmd = Escape.shell_command(params)
45
+ out = `#{cmd} 2>&1`
46
+ unless $?.exitstatus == 0
47
+ raise "Command exit with error!\nCommand: #{cmd}\nOut: #{out}"
48
+ end
49
+ return out
50
+ end
51
+
52
+ def files_in_dir(dir, options=nil)
53
+ files = []
54
+ dirs = []
55
+ dir = File.expand_path dir
56
+ current = dir
57
+
58
+ return [] unless File.directory? current
59
+
60
+ match = options[:match] if options
61
+ not_match = options[:not_match] if options
62
+ non_recursive = options[:non_recursive]
63
+ is_abs = options[:absolute?] or false
64
+
65
+ while current
66
+ Dir.entries(current).each do |name|
67
+ next unless name != '.' and name != '..'
68
+ path = File.join(current, name)
69
+
70
+ valid = true
71
+ valid = path =~ match if match and valid
72
+ valid = path !~ not_match if not_match and valid
73
+
74
+ if valid
75
+ rpath = path[dir.length+1,path.length]
76
+ yielded = is_abs ? File.join(dir, rpath) : rpath
77
+ files.push yielded
78
+ yield yielded if block_given?
79
+ end
80
+
81
+ dirs.push path if !non_recursive and File.directory? path
82
+ end
83
+
84
+ current = dirs.pop
85
+ end
86
+
87
+ files
88
+ end
89
+
90
+ def file_md5(file)
91
+ Digest::MD5.file(file).hexdigest
92
+ end
93
+
94
+ def partial_file_md5(file)
95
+ digest = Digest::MD5.new
96
+ digest << IO.read(file, 61440)
97
+ digest.hexdigest
98
+ end
99
+
100
+ def string_md5(string)
101
+ Digest::MD5.hexdigest(string)
102
+ end
103
+ end
104
+ end
data/lib/photostat.rb ADDED
@@ -0,0 +1,89 @@
1
+ require "benchmark"
2
+ require "pathname"
3
+ require "yaml"
4
+ require "logger"
5
+ require "exifr"
6
+ require 'fileutils'
7
+ require "escape"
8
+ require 'logger'
9
+ require 'trollop'
10
+
11
+ require "photostat/utils/os"
12
+ require "photostat/db/base"
13
+ require "photostat/plugins/00_base"
14
+
15
+ module Photostat
16
+ VERSION = "0.0.1"
17
+
18
+ def self.root
19
+ Pathname.new File.join(File.dirname(__FILE__), 'photostat')
20
+ end
21
+
22
+ def self.config
23
+ unless @config
24
+ @config = YAML.load(File.read(File.expand_path "~/.photostat"))
25
+ end
26
+ @config
27
+ end
28
+
29
+ def self.activate!
30
+ Photostat::Plugins.all_in_order.each do |plugin|
31
+ plugin.new.activate!
32
+ end
33
+ end
34
+
35
+ def self.configure_plugin!(name)
36
+ Photostat::Plugins.all[name.to_s].new.config
37
+ end
38
+
39
+ def self.activate_plugin!(name)
40
+ Photostat::Plugin.all[name.to_s].new.activate!
41
+ end
42
+
43
+ def self.execute
44
+ cmd, subc = nil, :help
45
+ cmd = ARGV.shift if ARGV and ARGV.length > 0
46
+ if cmd =~ /^(\w+):(\w+)$/
47
+ cmd, subc = $1, $2.to_sym
48
+ end
49
+
50
+ all_plugins = Photostat::Plugins.all
51
+ if cmd and all_plugins[cmd]
52
+ all_plugins[cmd].new.send subc
53
+ else
54
+ show_help
55
+ end
56
+ end
57
+
58
+ def self.show_help
59
+ puts "Photostat version #{Photostat::VERSION}"
60
+ puts
61
+ puts "Getting help for a particular plugin: photostat <plugin_name>"
62
+ puts "Getting help for a particular command:: photostat <command> --help"
63
+ puts
64
+ puts "Available plugins:"
65
+
66
+ Photostat::Plugins.all.each_key do |cmd_name|
67
+ cmd_obj = Photostat::Plugins.all[cmd_name]
68
+ help_text = "#{cmd_name}"
69
+ help_text += "\t- #{cmd_obj.help_text}" if cmd_obj.help_text
70
+ puts " #{help_text}"
71
+ end
72
+
73
+ puts
74
+ puts "All available commands:"
75
+ Photostat::Plugins.all.each_key do |cmd_name|
76
+ cmd_obj = Photostat::Plugins.all[cmd_name]
77
+ cmd_obj.exposes.each do |name|
78
+ msg = " #{cmd_obj.plugin_name}:#{name}"
79
+ if cmd_obj.exposes_help[name]
80
+ msg += "\t- " + cmd_obj.exposes_help[name]
81
+ end
82
+ puts msg
83
+ end
84
+ end
85
+ puts
86
+ end
87
+ end
88
+
89
+ Photostat::Plugins.load_all!
data/photostat.gemspec ADDED
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "photostat/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "photostat"
7
+ s.version = Photostat::VERSION
8
+ s.authors = ["Alexandru Nedelcu"]
9
+ s.email = ["me@alexn.org"]
10
+ s.homepage = "http://github.com/alexandru/photostat"
11
+ s.summary = %q{Managing Photos For Hackers}
12
+ s.description = %q{Photostat is a collection of command-line utilities for managing photos / syncronizing with Flickr - first version doesnt do much, it just helps me organize my files and upload them to Flickr}
13
+
14
+ s.rubyforge_project = "photostat"
15
+
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
19
+ s.require_paths = ["lib"]
20
+
21
+ s.add_development_dependency "bundler"
22
+ s.add_development_dependency "rake"
23
+
24
+ s.add_runtime_dependency 'sqlite3'
25
+ s.add_runtime_dependency "trollop"
26
+ s.add_runtime_dependency "escape"
27
+ s.add_runtime_dependency "flickraw"
28
+ s.add_runtime_dependency "exifr"
29
+ s.add_runtime_dependency "json"
30
+ end
metadata ADDED
@@ -0,0 +1,155 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: photostat
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Alexandru Nedelcu
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-10-29 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: bundler
16
+ requirement: &74575380 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: *74575380
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &74575170 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *74575170
36
+ - !ruby/object:Gem::Dependency
37
+ name: sqlite3
38
+ requirement: &74574890 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ! '>='
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ type: :runtime
45
+ prerelease: false
46
+ version_requirements: *74574890
47
+ - !ruby/object:Gem::Dependency
48
+ name: trollop
49
+ requirement: &74574610 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - ! '>='
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ type: :runtime
56
+ prerelease: false
57
+ version_requirements: *74574610
58
+ - !ruby/object:Gem::Dependency
59
+ name: escape
60
+ requirement: &74574400 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :runtime
67
+ prerelease: false
68
+ version_requirements: *74574400
69
+ - !ruby/object:Gem::Dependency
70
+ name: flickraw
71
+ requirement: &74574180 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ! '>='
75
+ - !ruby/object:Gem::Version
76
+ version: '0'
77
+ type: :runtime
78
+ prerelease: false
79
+ version_requirements: *74574180
80
+ - !ruby/object:Gem::Dependency
81
+ name: exifr
82
+ requirement: &74573940 !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ type: :runtime
89
+ prerelease: false
90
+ version_requirements: *74573940
91
+ - !ruby/object:Gem::Dependency
92
+ name: json
93
+ requirement: &74573730 !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ type: :runtime
100
+ prerelease: false
101
+ version_requirements: *74573730
102
+ description: Photostat is a collection of command-line utilities for managing photos
103
+ / syncronizing with Flickr - first version doesnt do much, it just helps me organize
104
+ my files and upload them to Flickr
105
+ email:
106
+ - me@alexn.org
107
+ executables:
108
+ - photostat
109
+ extensions: []
110
+ extra_rdoc_files: []
111
+ files:
112
+ - .gitignore
113
+ - Gemfile
114
+ - README.md
115
+ - Rakefile
116
+ - bin/photostat
117
+ - junk/filehash.rb
118
+ - junk/flickr-sync-old.rb
119
+ - junk/rack-sample.rb
120
+ - junk/trash.rb
121
+ - lib/photostat.rb
122
+ - lib/photostat/db/001_create_photos_with_tags.rb
123
+ - lib/photostat/db/002_photos_index.rb
124
+ - lib/photostat/db/003_photos_add_flickr_info.rb
125
+ - lib/photostat/db/base.rb
126
+ - lib/photostat/plugins/00_base.rb
127
+ - lib/photostat/plugins/01_local.rb
128
+ - lib/photostat/plugins/02_flickr.rb
129
+ - lib/photostat/utils/os.rb
130
+ - photostat.gemspec
131
+ homepage: http://github.com/alexandru/photostat
132
+ licenses: []
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ none: false
139
+ requirements:
140
+ - - ! '>='
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ required_rubygems_version: !ruby/object:Gem::Requirement
144
+ none: false
145
+ requirements:
146
+ - - ! '>='
147
+ - !ruby/object:Gem::Version
148
+ version: '0'
149
+ requirements: []
150
+ rubyforge_project: photostat
151
+ rubygems_version: 1.8.6
152
+ signing_key:
153
+ specification_version: 3
154
+ summary: Managing Photos For Hackers
155
+ test_files: []