lib/pixiv/client.rb
module Pixiv
class Client
# A new agent
# @return [Mechanize::HTTP::Agent]
def self.new_agent
agent = Mechanize.new
agent.max_history = 1
agent.pluggable_parser['image/gif'] = Mechanize::Download
agent.pluggable_parser['image/jpeg'] = Mechanize::Download
agent.pluggable_parser['image/png'] = Mechanize::Download
agent
end
# @return [Mechanize::HTTP::Agent]
attr_reader :agent
# @return [Integer]
attr_reader :member_id
# A new instance of Client, logged in with the given credentials
# @overload initialize(pixiv_id, password)
# @param [String] pixiv_id
# @param [String] password
# @yield [agent] (optional) gives a chance to customize the +agent+ before logging in
# @overload initialize(agent)
# @param [Mechanize::HTTP::Agent] agent
# @return [Pixiv::Client]
def initialize(*args)
if args.size < 2
@agent = args.first || self.class.new_agent
yield @agent if block_given?
ensure_logged_in
else
pixiv_id, password = *args
@agent = self.class.new_agent
yield @agent if block_given?
login(pixiv_id, password)
end
end
# Log in to Pixiv
# @param [String] pixiv_id
# @param [String] password
def login(pixiv_id, password)
doc = agent.get("https://accounts.pixiv.net/login?lang=ja&source=pc&view_type=page")
return if doc && doc.body =~ /logout/
form = doc.forms_with(action: '/login').first
puts doc.body and raise Error::LoginFailed, 'login form is not available' unless form
form.pixiv_id = pixiv_id
form.password = password
doc = agent.submit(form)
raise Error::LoginFailed unless doc && doc.body =~ /logout/
@member_id = member_id_from_mypage(doc)
end
# @param [Integer] member_id
# @return [Pixiv::Member] member bound to +self+
def member(member_id = member_id)
attrs = {member_id: member_id}
member = Member.lazy_new(attrs) { agent.get(Member.url(member_id)) }
member.bind(self)
end
# @param [Integer] illust_id
# @return [Pixiv::Illust] illust bound to +self+
def illust(illust_id)
attrs = {illust_id: illust_id}
illust = Illust.lazy_new(attrs) { agent.get(Illust.url(illust_id)) }
illust.bind(self)
end
# @param [Pixiv::Member, Integer] member_or_id
# @param [Integer] page
# @return [Pixiv::WorkList] work list bound to +self+
def work_list(member_or_id = member_id, page = 1)
list = illust_list_with_class(WorkList, member_or_id, page)
# Cheat; member_name will not found on your own work list.
list.send(:set_attrs!, member_name: member.name) if list.owner_id == member_id
list
end
# @param [Pixiv::Member, Integer] member_or_id
# @param [Integer] page
# @return [Pixiv::BookmarkList] bookmark list bound to +self+
def bookmark_list(member_or_id = member_id, page = 1)
illust_list_with_class(BookmarkList, member_or_id, page)
end
# @param [Pixiv::Member, Integer] member_or_id
# @param [Integer] page
# @return [Pixiv::PrivateBookmarkList] private bookmark list bound to +self+
def private_bookmark_list(member_or_id = member_id, page = 1)
illust_list_with_class(PrivateBookmarkList, member_or_id, page)
end
def search_result_list(query, opts = {})
attrs = {query: query, search_opts: opts}
SearchResultList.lazy_new(attrs) {
agent.get(SearchResultList.url(query, opts))
}.bind(self)
end
# @param [Pixiv::IllustList] list
# @!macro [new] opts_and_return
# @param [Hash] opts
# @option opts [Boolean] :include_deleted (false)
# whether the returning enumerator yields deleted illust as +nil+ or not
# @return [Pixiv::PageCollection::Enumerator]
def illusts(list, opts = {})
PageCollection::Enumerator.new(self, list, !!opts[:include_deleted])
end
# @param [Pixiv::Member, Integer] member_or_id
# @param [Integer] page
# @!macro opts_and_return
def works(member_or_id = member_id, page = 1, opts = {})
illusts(work_list(member_or_id, page), opts)
end
# @param [Pixiv::Member, Integer] member_or_id
# @param [Integer] page
# @!macro opts_and_return
def bookmarks(member_or_id = member_id, page = 1, opts = {})
illusts(bookmark_list(member_or_id, page), opts)
end
# @param [Integer] page
# @!macro opts_and_return
def private_bookmarks(page = 1, opts = {})
illusts(private_bookmark_list(member_id, page), opts)
end
# (see {SearchResultList.url})
def search(query, opts = {})
illusts(search_result_list(query, opts))
end
# Downloads the image to +io_or_filename+
# @param [Pixiv::Illust] illust
# @param [#write, String, Array<String, Symbol, #call>] io_or_filename io or filename or pattern for {#filename_from_pattern}
# @param [Symbol] size image size (+:small+, +:medium+, or +:original+)
def download_illust(illust, io_or_filename, size = :original)
size = {:s => :small, :m => :medium, :o => :original}[size] || size
url = illust.__send__("#{size}_image_url")
referer = case size
when :small then nil
when :medium then illust.url
when :original then illust.url
else raise ArgumentError, "unknown size `#{size}`"
end
save_to = io_or_filename
if save_to.is_a?(Array)
save_to = filename_from_pattern(save_to, illust, url)
end
FileUtils.mkdir_p(File.dirname(save_to)) unless save_to.respond_to?(:write)
@agent.download(url, save_to, [], referer)
end
# Downloads the images to +pattern+
# @param [Pixiv::Illust] illust the manga to download
# @param [Array<String, Symbol, #call>] pattern pattern for {#filename_from_pattern}
# @note +illust#manga?+ must be +true+
# @todo Document +&block+
def download_manga(illust, pattern, &block)
action = DownloadActionRegistry.new(&block)
illust.original_image_urls.each_with_index do |url, n|
begin
action.before_each.call(url, n) if action.before_each
filename = filename_from_pattern(pattern, illust, url)
FileUtils.mkdir_p(File.dirname(filename))
@agent.download(url, filename, [], illust.original_image_referer)
action.after_each.call(url, n) if action.after_each
rescue
action.on_error ? action.on_error.call($!) : raise
end
end
end
# Generate filename from +pattern+ in context of +illust+ and +url+
#
# @api private
# @param [Array<String, Symbol, #call>] pattern
# @param [Pixiv::Illust] illust
# @param [String] url
# @return [String] filename
#
# The +pattern+ is an array of string, symbol, or object that responds to +#call+.
# Each component of the +pattern+ is replaced by the following rules and then
# the +pattern+ is concatenated as the returning +filename+.
#
# * +:image_name+ in the +pattern+ is replaced with the base name of the +url+
# * Any other symbol is replaced with the value of +illust.send(the_symbol)+
# * +#call+-able object is replaced with the value of +the_object.call(illust)+
# * String is left as-is
def filename_from_pattern(pattern, illust, url)
pattern.map {|i|
if i == :image_name
name = File.basename(url)
if name =~ /\.(\w+)\?\d+$/
name += '.' + $1
end
name
elsif i.is_a?(Symbol) then illust.send(i)
elsif i.respond_to?(:call) then i.call(illust)
else i
end
}.join('')
end
protected
def illust_list_with_class(list_class, member_or_id, page)
it = member_or_id
id = it.is_a?(Member) ? it.member_id : it.to_i
attrs = {member_id: id} # Don't set page or it will prevent checking bounds.
list_class.lazy_new(attrs) {
agent.get(list_class.url(id, page))
}.bind(self)
end
def ensure_logged_in
doc = agent.get("#{ROOT_URL}/mypage.php")
raise Error::LoginFailed unless doc.body =~ /logout/
@member_id = member_id_from_mypage(doc)
end
def member_id_from_mypage(doc)
doc.at('a.user-name')['href'][/\d+$/].to_i
end
end
# @private
class DownloadActionRegistry
def initialize(&block)
instance_eval(&block) if block
end
def before_each(&block)
block ? (@before_each = block) : @before_each
end
def after_each(&block)
block ? (@after_each = block) : @after_each
end
def on_error(&block)
block ? (@on_error = block) : @on_error
end
end
end