photostat 0.0.1

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