rubyhexagon 0.4.0 → 0.4.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 34c8430199af7c9d7c8e2efb98cf8da90cbcdf09
4
+ data.tar.gz: 5675d205567c98595d52b5651eefe4fdd99f5dc6
5
+ SHA512:
6
+ metadata.gz: 4907ea016fa1d66489323f71cf10ed2ff9bd07a251f7462c6d7c09d8820115477ae9f5378431b583724ce81d9866fe6626db07cbc2947c94a1e05a94168e7738
7
+ data.tar.gz: 6aa265a2deb00c532ec2fcc0d7ac60a79fea98cd5b237f5c628ea1846dc91ccab034cb045dc121e3db9bb5543bd878eb4c08d7aad0e0b69f3efed6237c37237a
data/lib/api.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -20,104 +20,60 @@
20
20
  require "standard/http"
21
21
 
22
22
  module E621
23
- attr_reader :user
24
23
  class API
25
- # Variable mod is the module (Post, Pool, Set,...) this API is called for.
26
- def initialize(mod=nil)
24
+ def initialize(user=nil,pass=nil)
27
25
  @http = HTTP.new
28
- @mod = mod
29
- @user = user_login
26
+ login(user,pass) if !defined? @@login
27
+ end
28
+ # A wrapper function to fit into the general theme better.
29
+ def post_list(*args)
30
+ post_index(*args)
30
31
  end
31
32
  # Send commands to API and parse all answers, even errors.
32
33
  # Also make it work with POST and GET requests.
33
34
  def method_missing(method,*args)
34
- if [:get,:post].include?(method) then
35
- action,request,cookie = args
36
- r,tries = request2body(request,cookie),0
37
- begin
38
- json = @http.__send__(method,"/#@mod/#{action}.json",r)
39
- rescue Timeout::Error
40
- sleep 2**tries
41
- tries += 1
42
- #E621.log.debug("#@mod/#{action} failed: #{e.class}")
43
- raise if tries >= 4
44
- # if we see more than 4 timeout errors, then there
45
- # is something wrong
46
- retry
47
- end
48
- #raise APIError,json["reason"] if json.include?("success") && !json["success"]
49
- return json
50
- else
51
- raise NoMethodError, "undefined method #{method} for #{self.class}"
52
- end
35
+ request = request2body(args.first)
36
+ method = method.to_s.split("_")
37
+ m = case method[0]
38
+ when "post" then
39
+ case method[1]
40
+ when "show" then :get
41
+ else :post
42
+ end
43
+ else :post
44
+ end
45
+ @http.__send__(m,"/#{method.join("/")}.json",request)
53
46
  end
54
47
  private
55
48
  # This helper function provides the functionality of translating Ruby Hashes
56
49
  # into HTTP POST body strings.
57
- def request2body(r,c)
58
- s = String.new
59
- r.each do |k,v|
60
- s += "&" unless s == String.new
50
+ def request2body(request)
51
+ query = @@login ? "#@@login&" : String.new
52
+ request = request.map do |k,v|
61
53
  if v.is_a?(Hash) then
62
- t = String.new
63
- v.each do |e,a|
64
- t += "&" unless t == String.new
65
- t += "#{k}[#{e}]=#{a}"
66
- end
67
- s += t
68
- else
69
- s += "#{k}=#{v}"
54
+ raise Argumenterror, "Hashes as request values aren't allowed."
55
+ elsif v.is_a?(Array) then
56
+ v = v.join(",")
70
57
  end
58
+ "#{k}=#{v}"
71
59
  end
72
- s += "&#@@login" if !c
73
- return s
60
+ query += request.join("&")
61
+ return query
74
62
  end
75
63
  # Perform an API and site login. This function needs to be called once
76
64
  # before any actual API call can be made.
77
- def user_login
78
- # Cookie login gets eliminated for now.
79
- name,pass = String.new, String.new
80
- # Perform a re-login if the last time is older than x days. Or if there is
81
- # no cookie.
82
- passwd = File.expand_path(Config.paths["passwd"])
83
- if File.exist?(passwd) then
84
- File.open(passwd){|f|@passwd=f.read.parse}
65
+ def login(user,pass)
66
+ if user == nil && pass == nil then
67
+ @@login = nil
85
68
  else
86
- @passwd = {"name"=>"","pass"=>"","last_login"=>0}
87
- end
88
- if (Time.now.to_i-@passwd["last_login"].to_i) < 3.days then
89
- @@login = @passwd["login"]
90
- else
91
- if @passwd["name"] != "" && @passwd["pass"] != "" then
92
- name,pass = @passwd["name"],@passwd["pass"]
93
- else
94
- puts "No user data found. Please provide user data."
95
- name,pass = get_credentials
96
- @passwd["name"],@passwd["pass"] = name,pass
97
- # Store that data for later use.
98
- end
99
- request = "name=#{name}&password=#{pass}"
69
+ request = {name:name,password:pass}
100
70
  body = @http.get("/user/login.json",request)
101
71
  if body.has_key?("success") && (!body["success"] || body["success"] == "failed") then
102
72
  raise AuthentificationError, "Username or password wrong!"
103
73
  else
104
74
  @@login = "login=#{body["name"]}&password_hash=#{body["password_hash"]}"
105
- @passwd["login"] = @@login # Save login string for later use.
106
75
  end
107
- @passwd["last_login"] = Time.now.to_i
108
- # Write everything back!
109
- File.open(passwd,"w"){|f|f.print @passwd.to_json}
110
76
  end
111
- return @passwd["name"]
112
- end
113
- # If there is no data saved for login, ask the user. They must know!
114
- def get_credentials
115
- print "Username: "
116
- name = $stdin.gets.chomp
117
- print "Password: "
118
- pass = `stty_o=$(stty -g);stty -echo;read pass;stty $stty_o; echo $pass`.chomp
119
- puts
120
- return [name,pass]
121
77
  end
122
78
  end
123
79
  end
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -20,7 +20,7 @@
20
20
  module E621
21
21
  class Container
22
22
  def method_missing(method)
23
- if instance_variables.include?("@#{method}".to_sym) then
23
+ if instance_variables.include?("@#{method}".to_sym) && method != :api then
24
24
  return instance_variable_get("@#{method}")
25
25
  else
26
26
  raise NoMethodError, "undefined method #{method} for #{self.class}"
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -22,90 +22,29 @@ require "digest/md5"
22
22
  module E621
23
23
  class Post < Container
24
24
  def initialize(post)
25
- set_variables(post)
26
- end
27
-
28
- def download_data
29
- host,port = @file_url.sub(%r{^.+?//},"").sub(%r{/.+$},""),@file_url.match(/^https/) ? 443 : 80
30
- http = HTTP.new(host,port)
31
- body = String.new
32
- while Digest::MD5.hexdigest(body) != @md5 do
33
- body = http.get(@file_url.sub(/^.+?\.net/,""),"")["body"]
34
- end
35
- return body
36
- end
37
-
38
- def download(where)
39
- if @file_url then
40
- host,port = @file_url.sub(%r{^.+?//},"").sub(%r{/.+$},""),@file_url.match(/^https/) ? 443 : 80
25
+ @api = API.new
26
+ if post.is_a?(Fixnum) || (post.is_a?(String) && post.match(/^[0-9,a-f]+$/i)) then
27
+ post = if post.is_a?(String) && post.length == 32 then {md5:post}
28
+ elsif post.is_a?(Fixnum) || (post.is_a?(String) && post.match(/^\d+$/)) then
29
+ {id:post.to_i}
30
+ else
31
+ raise ArgumentError, "Neither an ID nor a MD5 Hash."
32
+ end
33
+ post = @api.post_show(post)
34
+ elsif post.is_a?(Hash) then # Variable post is ready to use. Do nothing.
41
35
  else
42
- host,port = "static1.e621.net",443
43
- @file_url = "/data/#{@md5[0,2]}/#{@md5[2,2]}/#@md5.#@file_ext"
36
+ raise ArgumentError, "Post only accepts an id, a MD5 hashsum or a Ruby hash."
44
37
  end
45
- http = HTTP.new(host,port)
46
- if !File.exist?(where) then
47
- body = ""
48
- while Digest::MD5.hexdigest(body) != @md5 do
49
- body = http.get(@file_url.sub(/^.+?\.net/,""),"")["body"]
50
- end
51
- File.open(where,"w") do |f|
52
- f.print body
53
- end
54
- else
55
- md5 = String.new
56
- File.open(where,"r") do |f|
57
- md5 = Digest::MD5.hexdigest(f.read)
58
- end
59
- if md5 != @md5 then
60
- body = ""
61
- while Digest::MD5.hexdigest(body) != @md5 do
62
- body = http.get(@file_url.sub(/^.+?\.net/,""),"")["body"]
63
- end
64
- File.open(where,"w") do |f|
65
- f.print body
66
- end
67
- end
68
- end
69
- end
70
-
71
- def add_to_set(id)
72
- api = API.new("set")
73
- api.post("add_post",{"post_id"=>@id,"set_id"=>id})
74
- end
75
-
76
- def remove_from_set(id)
77
- api = API.new("set")
78
- api.post("remove_post",{"post_id"=>@id,"set_id"=>id})
79
- end
80
-
81
- def favorite
82
- api = API.new("favorite")
83
- api.post("create",{"id"=>@id})
84
- end
85
-
86
- def unfavorite
87
- api = API.new("favorite")
88
- api.post("destroy",{"id"=>@id})
89
- end
90
-
91
- def vote(dir)
92
- dir = if dir >= 0 then 1 elsif dir < 0 then -1 end
93
- api = API.new("post")
94
- api.post("vote",{"id"=>@id,"score"=>dir})
95
- end
96
-
97
- def keys
98
- return instance_variables.map{|i|i.to_s.sub("@","")}
38
+ set_variables(post)
99
39
  end
100
-
101
- def to_json(b=nil)
102
- json_hash = Hash.new
103
- instance_variables.each do |i|
104
- v = instance_variable_get(i)
105
- v = v.is_a?(Time) ? v.to_i : v
106
- json_hash.store(i.to_s.sub("@",""),v)
40
+ alias show initialize
41
+ # Cast a vote! Any value above 0 votes up, every value below 0 votes down.
42
+ # Zero itself causes an error.
43
+ def vote(direction)
44
+ if direction > 0 then @api.post_vote(1)
45
+ elsif direction < 0 then @api.post_vote(-1)
46
+ else raise ArgumentError, "Zero isn't allowed for voting results."
107
47
  end
108
- return json_hash.to_json
109
48
  end
110
49
  end
111
50
  end
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -23,12 +23,11 @@ require "standard/error"
23
23
  require "standard/time"
24
24
 
25
25
  require "api"
26
- require "config"
27
26
  require "container"
28
27
  require "search"
29
28
  require "pool"
30
29
 
31
30
  module E621
32
31
  Name = "rubyhexagon"
33
- Version = "0.4.0"
32
+ Version = "0.4.1"
34
33
  end
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -18,56 +18,64 @@
18
18
  =end
19
19
 
20
20
  require "post"
21
+ require "thread"
21
22
 
22
23
  module E621
23
- attr_reader :posts
24
24
  class Search
25
25
  # commands update/download
26
- def initialize(query)
27
- @api,@mt = API.new("post"),Mutex.new
28
- @bad_tags = /(\s|^)(#{Config.blacklist.map{|x|Regexp.escape(x)}.join("|")})(\s|$)/
29
- @request = {"limit"=>100,"page"=>1,"tags"=>query.join("+")}
30
- fetch
26
+ def initialize(query,page=1)
27
+ @api, @queue = API.new, SizedQueue.new(4096)
28
+ @request = {limit:100,page:page,tags:query.url_encode}
31
29
  end
32
30
 
33
- def next!
34
- @request["page"] += 1
35
- fetch
31
+ def next_page
32
+ @request[:page] += 1
36
33
  end
37
34
 
35
+ def previous_page
36
+ @request[:page] -= 1
37
+ end
38
+ # Return each post via a block and process them individually.
39
+ # This could cause a race condition in the unlikely manner when the queue is
40
+ # empty and not being filled again, but the worker is still runnning.
38
41
  def each_post
39
- while @posts != Array.new do
40
- @posts.each do |post|
42
+ fetch_all
43
+ until !@worker.status do
44
+ until @queue.empty? do
45
+ post = @queue.pop
41
46
  yield post
42
47
  end
43
- next!
44
48
  end
45
49
  end
46
50
 
47
- def page
48
- return @request["page"]
49
- end
50
-
51
- def page=(page)
52
- @request["page"]=page
51
+ def posts
53
52
  fetch
54
53
  end
55
54
 
56
- def previous!
57
- @request["page"] -= 1
58
- fetch
55
+ def page
56
+ return @request[:page]
59
57
  end
60
58
 
61
- def to_json(a=nil,b=nil)
62
- posts = @posts.map{|o|o.to_json}
63
- {"name"=>@name,"queries"=>@queries,"posts"=>posts}.to_json
59
+ def page=(page)
60
+ @request[:page] = page
64
61
  end
62
+
65
63
  private
66
64
  def fetch
67
- @posts = @api.post("index",@request).map do |post|
68
- #Post.new(post) unless post["tags"].match(@bad_tags) || post["score"] <= 30
69
- Post.new(post) unless post["tags"].match(@bad_tags)
65
+ @api.post_list(@request).map do |post|
66
+ Post.new(post)
70
67
  end.compact
71
68
  end
69
+ def fetch_all
70
+ @worker = Thread.new do
71
+ until (posts = @api.post_list(@request)) == Array.new do
72
+ posts.each do |post|
73
+ post = Post.new(post)
74
+ @queue << post if post
75
+ end
76
+ next_page
77
+ end
78
+ end
79
+ end
72
80
  end
73
81
  end
data/lib/set.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -25,28 +25,25 @@ module E621
25
25
  @http = Net::HTTP.new(host,port)
26
26
  @http.use_ssl = true if port == 443
27
27
  end
28
- # Small wrapper function for post calls. This way a proper logging is
29
- # guaranteed.
30
- def post(url,request)
31
- return parse_body(@http.post(url,request))
32
- end
33
- # Small wrapper function for get calls. This way a proper logging is
34
- # guaranteed.
35
- def get(url,request)
36
- return parse_body(@http.get("#{url}?#{request}"))
37
- end
38
- # #geti gives an immediate response in the form of bytes downloaded. This
39
- # won't be included in the gems though.
40
- def geti(url,request)
41
- uri = "#{url}?#{request}"
42
- count = (@http.head(uri)["content-length"].to_i/1024.0).round(1)
43
- clength,message = count.to_s.length,String.new
44
- @http.get("#{url}?#{request}") do |body|
45
- message += body
46
- mlength = (message.length/1024.0).round(1)
47
- print "Downloaded #{" "*(clength-mlength.to_s.length)}#{mlength}/#{count}kB\r"
28
+ def method_missing(method, *args)
29
+ if [:get,:post].include?(method) then
30
+ tries,url,request = 0, *args
31
+ begin
32
+ response = case method
33
+ when :get then @http.get("#{url}?#{request}")
34
+ when :post then @http.post(url,request)
35
+ end
36
+ rescue
37
+ sleep 2**tries
38
+ tries += 1
39
+ raise if tries >= 8
40
+ retry
41
+ end
42
+ response = parse_body(response)
43
+ else
44
+ raise NoMethodError, "undefined method #{method} for #{self.class}"
48
45
  end
49
- return {"body"=>message}
46
+ return response
50
47
  end
51
48
  private
52
49
  # As every body gets processed the same, just put it into a seperate method.
@@ -64,18 +61,30 @@ module E621
64
61
  # Parse all code that returns not in JSON to actual JSON like hashes.
65
62
  def errorcode(code,url)
66
63
  body = {"success"=>false,"reason"=>""}
67
- body["reason"] = if code.match(/3\d{2}/) then "We got redirected!"
68
- elsif code.match(/403/) then "Access denied!"
69
- elsif code.match(/404/) then "File not found!"
70
- elsif code.match(/420/) then "Record could not be saved!"
71
- elsif code.match(/421/) then "User it throttled, try again later!"
72
- elsif code.match(/422/) then "The resource is locked!"
73
- elsif code.match(/423/) then "Resource already exists!"
74
- elsif code.match(/4\d{2}/) then "We made a bad request!"
75
- elsif code.match(/500/) then "Internal server error!"
76
- elsif code.match(/503/) then "Server has too much load!"
77
- elsif code.match(/5\d{2}/) then "Some unkown server side error!"
78
- else "HTTP response code out of range!!"
64
+ body["reason"] = if code.match(/3\d{2}/) then
65
+ "We got redirected!"
66
+ elsif code.match(/403/) then
67
+ "Access denied!"
68
+ elsif code.match(/404/) then
69
+ "File not found!"
70
+ elsif code.match(/420/) then
71
+ "Record could not be saved!"
72
+ elsif code.match(/421/) then
73
+ "User is throttled, try again later!"
74
+ elsif code.match(/422/) then
75
+ "The resource is locked!"
76
+ elsif code.match(/423/) then
77
+ "Resource already exists!"
78
+ elsif code.match(/4\d{2}/) then
79
+ "We made a bad request!"
80
+ elsif code.match(/500/) then
81
+ "Internal server error!"
82
+ elsif code.match(/503/) then
83
+ "Server has too much load!"
84
+ elsif code.match(/5\d{2}/) then
85
+ "Some unkown server side error!"
86
+ else
87
+ "HTTP response code out of range!!"
79
88
  end
80
89
  return body
81
90
  end
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -18,6 +18,7 @@
18
18
  =end
19
19
 
20
20
  require "json"
21
+ require "cgi"
21
22
 
22
23
  class String
23
24
  # Make json parsing a lot easier and less to write.
@@ -27,6 +28,9 @@ class String
27
28
  def to_ascii
28
29
  self.gsub("_"," ").encode("us-ascii", :invalid => :replace, :undef => :replace, :replace => "")
29
30
  end
31
+ def url_encode
32
+ CGI.escape(self)
33
+ end
30
34
  # Remove all tags inside of a string.
31
35
  def clean
32
36
  self.gsub(/<.+?>/,"")
@@ -1,5 +1,5 @@
1
1
  =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
2
+ Copyright 2014, 2015 Maxine Red <maxine@furfind.net>
3
3
 
4
4
  This file is part of rubyhexagon.
5
5
 
@@ -19,7 +19,7 @@
19
19
  class Time
20
20
  # A general to_s function for Time objects.
21
21
  def to_s
22
- self.strftime("%b %e,%Y %I:%M %p")
22
+ self.strftime("%b %e,%Y %I:%M:%S %p")
23
23
  end
24
24
  # This methods takes a block and returns how long it took to execute that block.
25
25
  def self.measure
metadata CHANGED
@@ -1,64 +1,84 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rubyhexagon
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
5
- prerelease:
4
+ version: 0.4.1
6
5
  platform: ruby
7
6
  authors:
8
7
  - Maxine Red
9
8
  autorequire:
10
9
  bindir: bin
11
10
  cert_chain: []
12
- date: 2015-06-11 00:00:00.000000000 Z
13
- dependencies: []
11
+ date: 2016-01-09 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pg
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.18'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.18'
14
41
  description: Rubyhexagon provides Ruby bindings for the e621 [dot] net API.
15
- email: maxine_red1@yahoo.com
16
- executables:
17
- - e621
42
+ email: maxine@furfind.net
43
+ executables: []
18
44
  extensions: []
19
45
  extra_rdoc_files: []
20
46
  files:
21
- - lib/set.rb
22
- - lib/examples/sync.rb
23
- - lib/examples/add.rb
24
- - lib/examples/db.rb
47
+ - lib/api.rb
48
+ - lib/container.rb
25
49
  - lib/pool.rb
26
50
  - lib/post.rb
27
- - lib/standard/time.rb
28
- - lib/standard/http.rb
51
+ - lib/rubyhexagon.rb
52
+ - lib/search.rb
53
+ - lib/set.rb
29
54
  - lib/standard/error.rb
30
- - lib/standard/int.rb
31
55
  - lib/standard/hash.rb
56
+ - lib/standard/http.rb
57
+ - lib/standard/int.rb
32
58
  - lib/standard/string.rb
33
- - lib/config.rb
34
- - lib/container.rb
35
- - lib/rubyhexagon.rb
36
- - lib/api.rb
37
- - lib/search.rb
38
- - bin/e621
59
+ - lib/standard/time.rb
39
60
  homepage: https://github.com/maxine-red/rubyhexagon
40
61
  licenses:
41
62
  - GPL-3.0
63
+ metadata: {}
42
64
  post_install_message:
43
65
  rdoc_options: []
44
66
  require_paths:
45
67
  - lib
46
68
  required_ruby_version: !ruby/object:Gem::Requirement
47
- none: false
48
69
  requirements:
49
- - - ! '>='
70
+ - - ">="
50
71
  - !ruby/object:Gem::Version
51
72
  version: 1.9.2
52
73
  required_rubygems_version: !ruby/object:Gem::Requirement
53
- none: false
54
74
  requirements:
55
- - - ! '>='
75
+ - - ">="
56
76
  - !ruby/object:Gem::Version
57
77
  version: '0'
58
78
  requirements: []
59
79
  rubyforge_project:
60
- rubygems_version: 1.8.23
80
+ rubygems_version: 2.2.2
61
81
  signing_key:
62
- specification_version: 3
82
+ specification_version: 4
63
83
  summary: Rubyhexagon, Ruby bindings for e621[dot]net.
64
84
  test_files: []
data/bin/e621 DELETED
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env ruby
2
- =begin
3
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
4
-
5
- This file is part of rubyhexahon.
6
-
7
- rubyhexahon is free software: you can redistribute it and/or modify
8
- it under the terms of the GNU General Public License as published by
9
- the Free Software Foundation, either version 3 of the License, or
10
- (at your option) any later version.
11
-
12
- rubyhexahon is distributed in the hope that it will be useful,
13
- but WITHOUT ANY WARRANTY; without even the implied warranty of
14
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
- GNU General Public License for more details.
16
-
17
- You should have received a copy of the GNU General Public License
18
- along with rubyhexahon. If not, see <http://www.gnu.org/licenses/>.
19
- =end
20
-
21
- $:.unshift File.dirname(__FILE__)+"/../lib"
22
-
23
- require "rubyhexagon"
24
-
25
- $0 = "E621 #{ARGV[0]} #{ARGV[1]}"
26
-
27
- require "examples/#{ARGV.shift}"
@@ -1,46 +0,0 @@
1
- =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
3
-
4
- This file is part of rubyhexagon.
5
-
6
- rubyhexagon is free software: you can redistribute it and/or modify
7
- it under the terms of the GNU General Public License as published by
8
- the Free Software Foundation, either version 3 of the License, or
9
- (at your option) any later version.
10
-
11
- rubyhexagon is distributed in the hope that it will be useful,
12
- but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- GNU General Public License for more details.
15
-
16
- You should have received a copy of the GNU General Public License
17
- along with rubyhexagon. If not, see <http://www.gnu.org/licenses/>.
18
- =end
19
- module E621
20
- class Config
21
- def self.config
22
- return @@config
23
- end
24
-
25
- def self.paths
26
- return @@paths
27
- end
28
-
29
- def self.blacklist
30
- return @@blacklist
31
- end
32
-
33
- def self.config=(config)
34
- if File.exist?(config) then
35
- File.open(config) do |f|
36
- c = f.read.parse
37
- @@paths = c.delete("paths")
38
- @@blacklist = c.delete("blacklist")
39
- @@config = c
40
- end
41
- else
42
- raise ArgumentError, "No configuration file specified. Broken installation!"
43
- end
44
- end
45
- end
46
- end
@@ -1,57 +0,0 @@
1
- =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
3
-
4
- This file is part of rubyhexagon.
5
-
6
- rubyhexagon is free software: you can redistribute it and/or modify
7
- it under the terms of the GNU General Public License as published by
8
- the Free Software Foundation, either version 3 of the License, or
9
- (at your option) any later version.
10
-
11
- rubyhexagon is distributed in the hope that it will be useful,
12
- but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- GNU General Public License for more details.
15
-
16
- You should have received a copy of the GNU General Public License
17
- along with rubyhexagon. If not, see <http://www.gnu.org/licenses/>.
18
- =end
19
-
20
- require "json"
21
-
22
- include E621
23
-
24
- mod = ARGV.shift
25
-
26
- $0 = "E621 #{mod}"
27
-
28
- E621::Config.config = File.expand_path("~/.hexagon/conf.json")
29
- Dir.chdir(File.expand_path(E621::Config.paths["home"])) do
30
- pools, api = Array.new, API.new(mod)
31
- File.open(File.expand_path(E621::Config.paths["j#{mod}s"])) do |f|
32
- pools = JSON.parser.new(f.read).parse
33
- end
34
- puts "Old count: #{pools.length}"
35
- candidates = ARGV.map{|x|x.to_i}.reject{|x|x==0}
36
- puts "Adding candidates: #{candidates.length}"
37
- new = ARGV.map{|x|x.to_i}.reject{|x|x==0||pools.include?(x)}
38
- new.each do |id|
39
- pool = api.post("show",{"id"=>id})
40
- if pool["id"] && mod == "pool" then
41
- pools << id
42
- elsif pool["id"] && mod == "set" then
43
- pools << {"id"=>pool["id"],"owner"=>pool["user_id"]}
44
- end
45
- end
46
- puts "Adding: #{new.length}"
47
- if mod == "pool" then
48
- pools = pools.sort.uniq
49
- elsif mod == "set" then
50
- pools = pools.sort{|k1,k2|k1["id"]<=>k2["id"]}.uniq
51
- end
52
- puts "New count: #{pools.length}"
53
- pools = pools.to_json
54
- File.open(File.expand_path(E621::Config.paths["j#{mod}s"]),"w") do |f|
55
- f.print pools
56
- end
57
- end
@@ -1,143 +0,0 @@
1
- =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
3
-
4
- This file is part of rubyhexagon.
5
-
6
- rubyhexagon is free software: you can redistribute it and/or modify
7
- it under the terms of the GNU General Public License as published by
8
- the Free Software Foundation, either version 3 of the License, or
9
- (at your option) any later version.
10
-
11
- rubyhexagon is distributed in the hope that it will be useful,
12
- but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- GNU General Public License for more details.
15
-
16
- You should have received a copy of the GNU General Public License
17
- along with rubyhexagon. If not, see <http://www.gnu.org/licenses/>.
18
- =end
19
-
20
- require "logger"
21
- require "thread"
22
- require "pg"
23
-
24
- include E621
25
-
26
- $form = "%b %e, %Y %I:%M:%S %p"
27
-
28
- require "search"
29
- require "pool"
30
- require "set"
31
-
32
- Thread.abort_on_exception = true
33
-
34
- class DB
35
- def initialize(database)
36
- @pg = PG.connect(dbname:database)
37
- @mt = Mutex.new
38
- end
39
- def exec(query)
40
- @mt.synchronize do
41
- result = @pg.exec(query)
42
- return result
43
- end
44
- end
45
- def escape_string(str)
46
- @pg.escape_string(str)
47
- end
48
- end
49
-
50
- class Sync
51
- def initialize
52
- E621::Config.config = File.expand_path("~/.hexagon/conf.json")
53
- if $stdout.tty? then
54
- @log = Logger.new($stdout)
55
- @log.level = Logger::DEBUG
56
- else
57
- @log = Logger.new(File.expand_path(E621::Config.paths["logging"]))
58
- @log.level = Logger::INFO
59
- end
60
- @log.formatter = proc do |sev,dat,prog,msg|
61
- "#{Time.now.strftime($form)} [#{sev.pad(5)}] #{msg}#$/"
62
- end
63
- api = API.new("user") # Used for login
64
- @log.info("Program #$0 started.")
65
- @uid = api.get("index",{"name"=>api.user}).first["id"]
66
- @max_wait = 1
67
- Dir.chdir(File.expand_path(E621::Config.paths["files"]))
68
- @db = DB.new("danbooru")
69
- @queue = Queue.new
70
- @mt = Mutex.new
71
- end
72
-
73
- def threads
74
- return E621::Config.threads
75
- end
76
-
77
- def worker(mod)
78
- if mod == "post" then
79
- fetch_posts
80
- end
81
- end
82
-
83
- def fetch_posts
84
- lid = @db.exec("SELECT id FROM e621.posts ORDER BY id DESC LIMIT 1;").first
85
- if lid then
86
- lid = lid["id"].to_i
87
- else
88
- lid = 0
89
- end
90
- posts = Search.new(["order:id", "id:#{lid}.."])
91
- @cols = ["id","tags","artist","created_at","score","md5","file_ext","description","rating","width","height","has_children","parent_id","file_size"]
92
- posts.each_post do |post|
93
- @queue << [post,"#{post.md5[0,2]}/#{post.md5[2,2]}/#{post.md5}.#{post.file_ext}"]
94
- end
95
- end
96
- def download
97
- Thread.current.exit if @queue.empty?
98
- post,path = @queue.pop
99
- m = String.new
100
- path.split("/")[0,2].each do |d|
101
- m += "#{d}/"
102
- Dir.mkdir(m) unless File.exists?(m)
103
- end
104
- tries = 0
105
- begin
106
- post.download(path)
107
- rescue
108
- raise if tries > 8
109
- tries += 1
110
- sleep 2
111
- retry
112
- end
113
- data = @cols.map do |x|
114
- y=post.__send__(x.to_sym)
115
- y = y.to_i if x == "created_at"
116
- y = y.join(" ") if x == "artist"
117
- y != nil ? @db.escape_string(y.to_s) : "NULL"
118
- end
119
- if !@db.exec("SELECT id FROM e621.posts WHERE id = #{post.id};").first then
120
- query = "INSERT INTO e621.posts (#{@cols.join(",")}) VALUES ('#{data.join("','")}');".gsub("'NULL'","NULL")
121
- @db.exec(query)
122
- @mt.synchronize do
123
- @log.info("Added post ##{post.id.pad(6," ")} to library.")
124
- end
125
- end
126
- end
127
- end
128
- sync = Sync.new
129
- posts = Thread.new do
130
- sync.worker("post")
131
- end
132
- sleep 4
133
- downloads = Array.new
134
- 10.times do
135
- downloads << Thread.new do
136
- loop do
137
- sync.download
138
- end
139
- end
140
- end
141
-
142
- posts.join
143
- downloads.each{|d|d.join}
@@ -1,187 +0,0 @@
1
- =begin
2
- Copyright 2014 Maxine Red <maxine_red1@yahoo.com>
3
-
4
- This file is part of rubyhexagon.
5
-
6
- rubyhexagon is free software: you can redistribute it and/or modify
7
- it under the terms of the GNU General Public License as published by
8
- the Free Software Foundation, either version 3 of the License, or
9
- (at your option) any later version.
10
-
11
- rubyhexagon is distributed in the hope that it will be useful,
12
- but WITHOUT ANY WARRANTY; without even the implied warranty of
13
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
- GNU General Public License for more details.
15
-
16
- You should have received a copy of the GNU General Public License
17
- along with rubyhexagon. If not, see <http://www.gnu.org/licenses/>.
18
- =end
19
-
20
- require "logger"
21
-
22
- include E621
23
-
24
- mod = ARGV.shift
25
-
26
- $form = "%b %e, %Y %I:%M:%S %p"
27
-
28
- require mod
29
-
30
- class Sync
31
- def initialize
32
- E621::Config.config = File.expand_path("~/.hexagon/conf.json")
33
- if $stdout.tty? then
34
- @log = Logger.new($stdout)
35
- @log.level = Logger::DEBUG
36
- else
37
- @log = Logger.new(File.expand_path(E621::Config.paths["logging"]))
38
- @log.level = Logger::INFO
39
- end
40
- @log.formatter = proc do |sev,dat,prog,msg|
41
- "#{Time.now.strftime($form)} [#{sev.pad(5)}] #{msg}#$/"
42
- end
43
- api = API.new("user") # Used for login
44
- @log.info("Program #$0 started.")
45
- @uid = api.get("index",{"name"=>api.user}).first["id"]
46
- @max_wait = 1
47
- end
48
-
49
- def worker(mod)
50
- @mod = mod
51
- Dir.chdir(File.expand_path("~/Pictures/e621/#{mod}s")) do
52
- sets = Array.new
53
- File.open(File.expand_path(E621::Config.paths["j#{mod}s"])) do |f|
54
- sets = f.read.parse
55
- end
56
- @set_files = File.expand_path(E621::Config.paths["#{mod}s"])
57
- api = API.new(mod)
58
- sets.each do |oset|
59
- id = mod == "set" ? oset["id"] : oset
60
- if mod == "set" then
61
- set = Set.new(id)
62
- elsif mod == "pool" then
63
- set = Pool.new(id)
64
- end
65
- begin
66
- set.name = set.name.gsub("_"," ")
67
- rescue
68
- @log.error("Error on #{mod} #{id}")
69
- net = Net::HTTP.new("e621.net",443)
70
- net.use_ssl = true
71
- body = net.get("/#{mod}/show/#{id}").body
72
- File.open("/tmp/e621.html","w"){|f|f.print body}
73
- if body.match("ActiveRecord::RecordNotFound")
74
- pools_f, pools = File.expand_path("~/.hexagon/#{mod}s.json"), Array.new
75
- File.open(pools_f) do |f|
76
- pools = f.read.parse
77
- end
78
- pools.delete(id)
79
- File.open(pools_f,"w") do |f|
80
- f.print pools.to_json
81
- end
82
- @log.info("Removed: #{mod} ##{id} doesn't exist anymore.")
83
- puts "Removed: #{mod} ##{id} doesn't exist anymore."
84
- end
85
- next
86
- end
87
- @log.info("Fetching #{mod} \"#{set.name}\"")
88
- set.name = "Love_Can_Be_Different" if set.name == "Love_can_be_different"
89
- set.name = set.name.gsub("_"," ").encode("us-ascii", :invalid => :replace, :undef => :replace, :replace => "")
90
- set.name = set.name.gsub(/[^0-9, ,_,a-z,\-]/i,"").sub(/\s+$/,"")
91
- name = mod == "set" ? set.shortname : id.pad(5)
92
- if !File.exists?(@set_files+"/#{name}.json") then
93
- File.open(@set_files+"/#{name}.json","w"){|f|f.print "{\"downloads\":[],\"updated_at\":0}"}
94
- end
95
- File.open(@set_files+"/#{name}.json"){|f|@posts = f.read.parse}
96
- @posts.store("updated_at",0) if !@posts.has_key?("updated_at")
97
- @posts.store("downloads",@posts["posts"]) if @posts["posts"]
98
- next if set.updated_at.to_i <= @posts["updated_at"].to_i
99
- dname = mod == "pool" ? set.name : name
100
- Dir.mkdir(dname) unless File.exist?(dname)
101
- i = 0
102
- set.each_post do |post|
103
- i += 1
104
- next if @posts["downloads"].include?(post.id)
105
- download(post,dname,set,i)
106
- end
107
- advanced_set(oset,name,set) if mod == "set"
108
- @posts["updated_at"] = set.updated_at.to_i
109
- jposts = @posts.to_json
110
- File.open(@set_files+"/#{name}.json","w"){|f|f.print jposts}
111
- end
112
- end
113
- end
114
-
115
- def advanced_set(set,name,s)
116
- sid, owner, query = set["id"], set["owner"], set["search"]
117
- if owner == @uid then
118
- Search.new("fav:maxine_red #{query} order:id".split(" ")).each_post do |post|
119
- next if @posts["downloads"].include?(post.id)
120
- download(post,name,s)
121
- end
122
- api = API.new("set")
123
- set = api.get("show", {"id"=>sid})
124
- posts = set["posts"].map{|post|Post.new(post).id}
125
- name = set["shortname"]
126
- local = Dir["#{name}/*"].reject{|x|x.match(/db$/)}.map{|x|x.split(".")[1].to_i}.sort.uniq
127
- (posts-local).each do |id|
128
- post = Post.new({"id"=>id})
129
- f = post.remove_from_set(sid)
130
- if f["success"] then
131
- s = "Removed Post #{" "*(6-post.id.to_s.length)}#{post.id} from \"#{set["name"]}\""
132
- puts "#{Time.now.strftime($form)}: \e[1;33m#{s}\e[0m"
133
- @log.info(s)
134
- else
135
- s = "#{f["reason"].to_s.gsub(/<.+?>/,"")}."
136
- puts "#{Time.now.strftime($form)}: \e[1;31m#{s}\e[0m"
137
- @log.info(s)
138
- end
139
- end
140
- (local-posts).each do |id|
141
- next if id == 0
142
- post = Post.new({"id"=>id})
143
- f = post.add_to_set(sid)
144
- if f["success"] then
145
- s = "Added Post #{" "*(6-post.id.to_s.length)}#{post.id} to \"#{set["name"]}\"."
146
- puts "#{Time.now.strftime($form)}: #{s}"
147
- @log.info(s)
148
- else
149
- s = "#{f["reason"].gsub(/<.+?>/,"")}. Removing local file."
150
- puts "#{Time.now.strftime($form)}: #{s}"
151
- @log.info(s)
152
- File.unlink(Dir["#{set["shortname"]}/*.#{"0"*(8-post.id.to_s.length)}#{post.id}.*"].first)
153
- end
154
- end
155
- end
156
- end
157
- def download(post,name,set,id=0)
158
- artist = post.artist.first
159
- if @mod == "set" then
160
- string = [post.created_at.to_i,post.id.pad(8),artist+"_"+name,post.file_ext]
161
- elsif @mod == "pool" then
162
- string = [set.id.pad(5),id.pad(4),post.id.pad(8),artist,name.downcase.gsub(/\s/,"_"),post.file_ext]
163
- end
164
- string = string.join(".")
165
- tries = 0
166
- begin
167
- post.download("#{name}/#{string}")
168
- rescue
169
- raise if tries > 4
170
- tries += 1
171
- puts "#{Time.now.strftime($form)}: #$!"
172
- @log.info("#$!")
173
- sleep 1
174
- retry
175
- end
176
- File.utime(post.created_at,post.created_at,"#{name}/#{string}")
177
- @posts["downloads"] << post.id
178
- jposts = @posts.to_json
179
- File.open(@set_files+"/#{name}.json","w"){|f|f.print jposts}
180
- s = "Downloaded post #{" "*(6-post.id.to_s.length)}#{post.id} from \"#{set.name}\"."
181
- puts "#{Time.now.strftime($form)}: #{s}"
182
- sleep(rand*@max_wait)
183
- @log.info(s)
184
- end
185
- end
186
- sync = Sync.new
187
- sync.worker(mod)