livejournal2 0.4.0

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/sample/export ADDED
@@ -0,0 +1,144 @@
1
+ #!/usr/bin/ruby -I../lib
2
+
3
+ require 'livejournal/sync'
4
+ require 'livejournal/database'
5
+ require 'optparse'
6
+ require 'progressbar'
7
+
8
+ create = false
9
+ username = nil
10
+ password = nil
11
+ usejournal = nil
12
+ dbfile = nil
13
+ get_entries = true
14
+ get_comments = true
15
+ opts = OptionParser.new do |opts|
16
+ opts.on('-c', '--create',
17
+ 'Create a new database if necessary') { |create| }
18
+ opts.on('-u', '--user USERNAME',
19
+ 'Login username') { |username| }
20
+ opts.on('-p', '--password PASSWORD',
21
+ 'Login password') { |password| }
22
+ opts.on('-a', '--usejournal JOURNAL',
23
+ 'Journal to sync (USERNAME must have access)') { |usejournal| }
24
+ opts.on('-d', '--db FILENAME',
25
+ 'Filename for output database') { |dbfile| }
26
+ opts.on('--nocomments',
27
+ "Don't fetch comments (only entries)") { get_comments = false }
28
+ opts.on('--noentries',
29
+ "Don't fetch entries (only comments)") { get_entries = false }
30
+ end
31
+ opts.parse!(ARGV)
32
+
33
+ unless dbfile
34
+ puts opts
35
+ puts "ERROR: Must specify database file."
36
+ exit 1
37
+ end
38
+
39
+ begin
40
+ db = LiveJournal::Database.new(dbfile, create)
41
+ rescue Errno::ENOENT
42
+ puts "Use the --create flag to create a new database."
43
+ raise
44
+ end
45
+ username ||= db.username
46
+ usejournal ||= db.usejournal
47
+
48
+ unless username
49
+ puts opts
50
+ puts "ERROR: Must specify username."
51
+ exit 1
52
+ end
53
+
54
+ if usejournal
55
+ puts "Journal: #{usejournal} (syncing as #{username})."
56
+ else
57
+ puts "Journal: #{username}."
58
+ end
59
+
60
+ unless password
61
+ noecho = system('stty -echo')
62
+ print "Enter password"
63
+ print "(WARNING: echoed to screen)" unless noecho
64
+ print ": "
65
+ $stdout.flush
66
+
67
+ begin
68
+ password = gets.strip
69
+ ensure
70
+ if noecho
71
+ system('stty sane')
72
+ puts # since the user input didn't add the newline for us...
73
+ end
74
+ end
75
+ end
76
+
77
+ user = LiveJournal::User.new(username, password)
78
+ user.usejournal = usejournal
79
+ db.username = user.username
80
+ db.usejournal = usejournal if usejournal
81
+
82
+ if get_entries
83
+ puts "Fetching entries..."
84
+ lastsync = db.lastsync
85
+ puts "Resuming from #{lastsync}." if lastsync
86
+ sync = LiveJournal::Sync::Entries.new(user, lastsync)
87
+
88
+ ProgressBar::with_progress("Fetching metadata: ") do |bar|
89
+ sync.run_syncitems do |cur, total|
90
+ bar.update(cur, total)
91
+ end
92
+ end
93
+
94
+ ProgressBar::with_progress("Fetching bodies: ") do |bar|
95
+ cur = 0
96
+ sync.run_sync do |entries, lastsync, remaining|
97
+ db.transaction do
98
+ entries.each do |itemid, entry|
99
+ db.store_entry entry
100
+ end
101
+ end
102
+ db.lastsync = lastsync
103
+
104
+ cur += entries.length
105
+ bar.update(cur, remaining+cur)
106
+ end
107
+ end
108
+ end
109
+
110
+ if get_comments
111
+ puts "Fetching comments..."
112
+
113
+ cs = LiveJournal::Sync::Comments.new(user)
114
+ next_meta = db.last_comment_meta
115
+ if next_meta
116
+ next_meta += 1
117
+ else
118
+ next_meta = 0
119
+ end
120
+
121
+ ProgressBar::with_progress("Fetching metadata: ") do |bar|
122
+ cs.run_metadata(next_meta) do |cur, max, data|
123
+ db.store_comments_meta data.comments
124
+ db.store_usermap data.usermap
125
+ bar.update(cur, max)
126
+ end
127
+ end
128
+
129
+ next_full = db.last_comment_full
130
+ if next_full
131
+ next_full += 1
132
+ else
133
+ next_full = 0
134
+ end
135
+
136
+ ProgressBar::with_progress("Fetching bodies: ") do |bar|
137
+ cs.run_body(next_full) do |cur, max, data|
138
+ bar.update(cur, max)
139
+ db.store_comments_full data.comments
140
+ end
141
+ end
142
+ end
143
+
144
+ # vim: ts=2 sw=2 et :
data/sample/fuse ADDED
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/ruby -I../lib
2
+
3
+ require 'fusefs'
4
+ require 'livejournal/database'
5
+ require 'livejournal/entry'
6
+ require 'stringio'
7
+
8
+ class DBQuery
9
+ def initialize
10
+ @db = $db
11
+ end
12
+ def has_any?(where, *args)
13
+ exists = @db.db.get_first_value("SELECT itemid #{where} LIMIT 1", *args)
14
+ exists != nil
15
+ end
16
+ def has_year? year
17
+ has_any?("FROM entry WHERE year=?", year)
18
+ end
19
+ def has_month? year, month
20
+ has_any?("FROM entry WHERE year=? AND month=?", year, month)
21
+ end
22
+ def has_day? year, month, day
23
+ has_any?("FROM entry WHERE year=? AND month=? AND day=?", year, month, day)
24
+ end
25
+ def has_entry? itemid
26
+ has_any?("FROM entry WHERE itemid=?", itemid)
27
+ end
28
+
29
+ def get_array(sql, *args)
30
+ array = []
31
+ @db.db.execute("SELECT DISTINCT #{sql}", *args) do |row|
32
+ array << row[0]
33
+ end
34
+ array
35
+ end
36
+
37
+ def years
38
+ get_array('year FROM entry')
39
+ end
40
+ def months year
41
+ get_array('month FROM entry WHERE year=?', year).map { |m| "%02d" % m }
42
+ end
43
+ def days year, month
44
+ get_array('day FROM entry WHERE year=? AND month=?', year, month).map { |d| "%02d" % d }
45
+ end
46
+
47
+ def day_entries year, month, day
48
+ get_array('itemid FROM entry WHERE year=? AND month=? AND day=?',
49
+ year, month, day)
50
+ end
51
+ end
52
+
53
+ class Dispatcher < FuseFS::FuseDir
54
+ def initialize
55
+ @matches = []
56
+ end
57
+ def register(match, target)
58
+ @matches << [match, target]
59
+ end
60
+
61
+ def dispatch(sym, path, *args)
62
+ path, rest = split_path path if path
63
+ if path
64
+ @matches.each do |match, target|
65
+ if path =~ match
66
+ return target.dispatch(sym, rest, *(args+[path]))
67
+ end
68
+ end
69
+ end
70
+
71
+ # otherwise, dispatch it to the current object
72
+ begin
73
+ s = self.send(sym, path, *args)
74
+ rescue
75
+ p $!
76
+ end
77
+ return s
78
+ end
79
+ end
80
+
81
+ class DispatchDir < FuseFS::FuseDir
82
+ def initialize dispatcher
83
+ @dispatcher = dispatcher
84
+ end
85
+
86
+ def self.add_dispatch(*args)
87
+ args.each do |sym|
88
+ class_eval %{def #{sym}(path, *args)
89
+ @dispatcher.dispatch(#{sym.inspect}, path, *args)
90
+ end}
91
+ end
92
+ end
93
+
94
+ add_dispatch :directory?, :file?
95
+ add_dispatch :contents, :read_file
96
+ end
97
+
98
+ class LJFS < Dispatcher
99
+ class Day < Dispatcher
100
+ def contents path, year, mon, day
101
+ entries = $dbq.day_entries year, mon, day
102
+ entries.map do |itemid|
103
+ "#{itemid}.txt"
104
+ end
105
+ end
106
+ def directory? path, year, mon, day
107
+ path == nil and $dbq.has_day? year, mon, day
108
+ end
109
+ def file? path, year, mon, day
110
+ return false unless $dbq.has_day? year, mon, day
111
+ return false unless path =~ /^(\d+)\.txt$/
112
+ itemid = $1
113
+ return false unless $dbq.has_entry? itemid
114
+ true
115
+ end
116
+ def read_file path, year, mon, day
117
+ return false unless $dbq.has_day? year, mon, day
118
+ return false unless path =~ /^(\d+)\.txt$/
119
+ itemid = $1
120
+ return false unless $dbq.has_entry? itemid
121
+ entry = $db.get_entry itemid
122
+ out = StringIO.new
123
+ if entry.subject
124
+ out.puts entry.subject
125
+ out.puts("=" * entry.subject.length)
126
+ out.puts
127
+ end
128
+ out.puts entry.event
129
+ out.rewind
130
+ return out.read
131
+ end
132
+ end
133
+ class Month < Dispatcher
134
+ def initialize
135
+ super
136
+ register(/^\d{2}/, Day.new)
137
+ end
138
+ def directory? path, year, mon
139
+ path == nil and $dbq.has_month? year, mon
140
+ end
141
+ def file? path, year, mon
142
+ false
143
+ end
144
+ def contents path, year, mon
145
+ $dbq.days year, mon
146
+ end
147
+ end
148
+
149
+ class Year < Dispatcher
150
+ def initialize
151
+ super
152
+ register(/^\d{2}/, Month.new)
153
+ end
154
+ def directory? path, year
155
+ path == nil and $dbq.has_year? year
156
+ end
157
+ def file? path, year
158
+ false
159
+ end
160
+ def contents path, year
161
+ $dbq.months year
162
+ end
163
+ end
164
+
165
+ def initialize
166
+ super
167
+ register(/^\d{4}$/, Year.new)
168
+ end
169
+
170
+ def directory? path
171
+ return false
172
+ end
173
+ def file? path
174
+ return path == 'username'
175
+ end
176
+ def read_file path
177
+ return $db.username
178
+ end
179
+ def contents *args
180
+ return ['username'] + $dbq.years
181
+ end
182
+ end
183
+
184
+ unless ARGV.length == 2
185
+ puts "usage: #{$0} dbfile mountpoint"
186
+ exit 1
187
+ end
188
+ dbfile, mountpoint = ARGV
189
+
190
+ $db = LiveJournal::Database.new dbfile
191
+ $dbq = DBQuery.new # temp hack
192
+ root = DispatchDir.new LJFS.new
193
+
194
+ FuseFS.set_root root
195
+ FuseFS.mount_under mountpoint
196
+ FuseFS.run
197
+
198
+ # vim: ts=2 sw=2 et :
data/sample/graph ADDED
@@ -0,0 +1,208 @@
1
+ #!/usr/bin/ruby -I../lib
2
+
3
+ begin
4
+ require 'rubygems'
5
+ require 'gruff'
6
+ rescue LoadError => e
7
+ puts <<EOT
8
+ Error loading libraries: #{e}
9
+ Though ljrb itself doesn't require them, the graph sample uses
10
+ the gruff library: http://nubyonrails.com/pages/gruff ,
11
+ which itself requires rubygems.
12
+ EOT
13
+ exit 1
14
+ end
15
+ require 'livejournal/database'
16
+ require 'livejournal/entry'
17
+ require 'optparse'
18
+ require 'time'
19
+
20
+ class Options
21
+ # required options
22
+ attr_reader :dbfile, :outfile, :mode
23
+ # optional options
24
+ attr_reader :smooth, :normalize, :geom, :starttime, :endtime
25
+ # mode-specific options
26
+ attr_reader :tags
27
+
28
+ def initialize
29
+ @smooth = 1
30
+ @dbfile = nil
31
+ @outfile = nil
32
+ @normalize = false
33
+ @mode = nil
34
+ @geom = 800
35
+ @opts = OptionParser.new do |o|
36
+ o.banner = "usage: graph [options] [modeparam [modeparam ...]]"
37
+ o.on('-d', '--db=FILE', 'database (required)') { |@dbfile| }
38
+ o.on('-o', '--out=FILENAME.PNG', 'output file (required)') { |@outfile| }
39
+ o.on('-g', '--geom=WxH', 'output geometry') { |@geom| }
40
+ o.on('--smooth=N',
41
+ 'merge every set of n consecutive datapoints') do |smooth|
42
+ @smooth = smooth.to_i
43
+ end
44
+ o.on('--normalize',
45
+ 'normalize relative to entry counts') { |@normalize| }
46
+ o.on('--start=TIME', 'start time') { |t| @starttime = Time.parse t }
47
+ o.on('--end=TIME', 'end time') { |t| @endtime = Time.parse t }
48
+ o.on('-m', '--mode=MODE',
49
+ 'mode (required) one of {tags, security}') do |mode|
50
+ case mode
51
+ when 'tags'
52
+ @mode = :tags
53
+ when 'security'
54
+ @mode = :security
55
+ else
56
+ die "bad mode #{mode}"
57
+ end
58
+ end
59
+ end
60
+ self
61
+ end
62
+ def parse!(argv)
63
+ @opts.parse! ARGV
64
+ check_params
65
+ self
66
+ end
67
+ def die(msg)
68
+ puts msg
69
+ puts @opts
70
+ exit 1
71
+ end
72
+ def check_params
73
+ die "must specify database." unless @dbfile
74
+ die "must specify output file." unless @outfile
75
+ die "must specify mode." unless @mode
76
+
77
+ case @mode
78
+ when :tags
79
+ @tags = ARGV
80
+ unless @tags.length > 0
81
+ die "must specify at least one tag as an extra command-line parameter."
82
+ end
83
+ end
84
+ end
85
+ end
86
+ $opts = Options.new.parse! ARGV
87
+
88
+ class DataSet
89
+ # store a series of (time, class, count) observations.
90
+
91
+ def initialize
92
+ # data: klass -> time -> count
93
+ @data = Hash.new { |h,k| h[k] = Hash.new { |h,k| h[k] = 0 } }
94
+ @firsttime = @lasttime = nil
95
+ @bucketing = :month
96
+ end
97
+
98
+ def bucket(time)
99
+ case @bucketing
100
+ when :month
101
+ mon = time.month
102
+ if $opts.smooth != 1
103
+ # when smooth = 3,
104
+ # 1 -> 1
105
+ # 3 -> 1
106
+ # 4 -> 4
107
+ # 5 -> 4
108
+ # 12 -> 10
109
+ mon = ((mon-1)/$opts.smooth * $opts.smooth) + 1
110
+ end
111
+ "%d-%02d" % [time.year, mon]
112
+ end
113
+ end
114
+
115
+ def each_bucket
116
+ case @bucketing
117
+ when :month
118
+ time = Date.parse(@firsttime.strftime('%Y-%m-01'))
119
+ lasttime = Date.parse(@lasttime.strftime('%Y-%m-01'))
120
+ lastbucket = nil
121
+ while time < lasttime
122
+ b = bucket(time)
123
+ yield b if b != lastbucket
124
+ lastbucket = b
125
+ time = time >> 1
126
+ end
127
+ end
128
+ end
129
+
130
+ def labels(count=5)
131
+ total = 0
132
+ each_bucket { total += 1 }
133
+ skip = (total / count.to_f).round
134
+
135
+ labels = {}
136
+ i = 0
137
+ each_bucket do |bucket|
138
+ labels[i] = bucket if i % skip == 0
139
+ i += 1
140
+ end
141
+ labels
142
+ end
143
+
144
+ def get_class(klass)
145
+ buckets = []
146
+ data = @data[klass]
147
+ each_bucket do |bucket|
148
+ buckets << if data.has_key? bucket
149
+ if $opts.normalize
150
+ data[bucket] / @data[:total][bucket].to_f
151
+ else
152
+ data[bucket]
153
+ end
154
+ else
155
+ 0
156
+ end
157
+ end
158
+ buckets
159
+ end
160
+
161
+ def add(klass, time, count=1)
162
+ @firsttime = time if @firsttime.nil? or time < @firsttime
163
+ @lasttime = time if @lasttime.nil? or time > @lasttime
164
+ @data[klass][bucket(time)] += count
165
+ end
166
+ end
167
+
168
+ dataset = DataSet.new
169
+ db = LiveJournal::Database.new $opts.dbfile
170
+ total_entries = 0
171
+
172
+ db.each_entry do |entry|
173
+ next if $opts.starttime and $opts.starttime > entry.time
174
+ next if $opts.endtime and $opts.endtime < entry.time
175
+
176
+ dataset.add(:total, entry.time, 1)
177
+ case $opts.mode
178
+ when :tags
179
+ entry.taglist.each do |tag|
180
+ dataset.add(tag, entry.time)
181
+ end
182
+ when :security
183
+ case entry.security
184
+ when :public; dataset.add(:public, entry.time)
185
+ when :friends; dataset.add(:friends, entry.time)
186
+ else; dataset.add(:other, entry.time)
187
+ end
188
+ end
189
+ total_entries += 1
190
+ end
191
+
192
+ g = Gruff::Line.new($opts.geom)
193
+ g.labels = dataset.labels
194
+ case $opts.mode
195
+ when :tags
196
+ g.title = 'Tag Rate'
197
+ $opts.tags.each do |tag|
198
+ g.data(tag, dataset.get_class(tag))
199
+ end
200
+ when :security
201
+ g.title = 'Entry Security'
202
+ [:public, :friends, :other].each do |sec|
203
+ g.data(sec.to_s, dataset.get_class(sec))
204
+ end
205
+ end
206
+ g.write $opts.outfile
207
+
208
+ # vim: ts=2 sw=2 et :