/lib/smartlib/app/api.rb
Ruby | 724 lines | 625 code | 74 blank | 25 comment | 83 complexity | 98e210ed90443a43d26f03e9c2b04f8c MD5 | raw file
- # encoding:utf-8
- require 'smartlib/common'
- require 'open-uri'
- require 'rest_client'
- require 'nokogiri'
- require 'rest_client'
- require 'isbn'
- require 'faraday'
- module Smartlib
- module App
- class Api < Common
- set :views, 'lib/smartlib/views/email', 'lib/smartlib/views/api'
- ISSUES_URL = 'https://api.bitbucket.org/1.0/repositories/thrabal/sma' +
- 'rtlib/issues'
- URL_ALEPH = 'https://aleph.muni.cz/'
- COPY_URL = URL_ALEPH +
- 'F?func=find-b&find_code=SYS&local_base=MUB01&request=##&format=999'
- GOOGLE = 'http://books.google.com/books?bibkeys=ISBN' +
- '##&jscmd=viewapi'
- before do
- headers 'StatusCode' => '200'
- end
- helpers do
- def protected!
- return true if session.include?('session_api') &&
- self.class::get_person_by_session(session['session_api'])
- throw(unauthorized)
- end
- end
- # Search books
- get '/books/search' do
- #limits
- output = []
- limit = params.include?('limit') ? params['limit'].to_i : 20
- offset = params.include?('offset')? params['offset'].to_i : 0
- select, join, where = '', '', ''
- simple_error unless params.include?('query') &&
- params['query'] && !params['query'].empty?
- now = Time.now
- begin
- is = ISBN.valid?(params['query'].gsub('-', ''))
- raise StandardError, 'f' if is == false
- isbn = params['query'].gsub('-', '')
- add_books(
- DB[:books].filter(:isbn => ISBN.ten(isbn)).or(:isbn => ISBN.thirteen(isbn)), output
- )
- rescue StandardError => bang
- query = params['query'].tr(
- 'áäčďéěíĺľňóôőöŕšťúůűüýřžÁÄČĎÉĚÍĹĽŇÓÔŐÖŔŠŤÚŮŰÜÝŘŽ',
- 'aacdeeillnoooorstuuuuyrzAACDEEILLNOOOORSTUUUUYRZ'
- ).downcase
- again = true
- attempt = 0
- while again
- mode = "NATURAL LANGUAGE MODE"
- if attempt > 0
- mode = "BOOLEAN MODE"
- query = "*#{query}*" unless query =~ /\s+/
- query = "#{query}*" if query =~ /\s+/
- end
- attempt += 1
- # params contains specific libraries
- if params.include?('library')
- library = params['library'].gsub(/-[a-z]+,?/i, ',')
- select = ""
- join =
- " INNER JOIN `copies` ON (`copies`.`book_id` = `books`.`id`) " +
- " INNER JOIN `libraries` ON (`copies`.`library_id` = `libraries`.`id`)"
- library , where, counter = library.split(','), " AND (", 0
- library.each do |lib|
- or_ = counter == 0 ? '' : 'OR'
- counter += 1
- where += " #{or_} `libraries`.`library` LIKE '#{lib.strip}'"
- end
- where += ")"
- end
- # title search
- if !params.include?('type') || params['type'] && params['type'].eql?('title')
- output = add_books(
- DB["SELECT DISTINCT(`books`.`id`), `books`.`title`, `bo" +
- "oks`.`full_title`, `books`.`sysno`, `books`.`isbn`, `b" +
- "ooks`.`average_rating`, `books`.`cover_url`, `books`.`" +
- "preview_url` #{select} FROM `books` INNER JOIN `books_search`" +
- " ON (`books`.`id` = `books_search`.`book_id`) #{join}" +
- " WHERE MATCH (`books_search`.`search_title`) AGAINST (" +
- " '#{query}' IN #{mode} ) #{where} " +
- " LIMIT #{limit} OFFSET #{offset}"],
- output
- )
- end
- # complete search
- if (!params.include?('type') && (output.count < limit)) || (
- params.include?('type') && params['type'] && params['type'].eql?('author')
- )
- if (!params.include?('type') ||
- (params.include?('type') && !params['type'])) &&
- offset > 20
- count = DB["SELECT DISTINCT(COUNT(`id`)) as count FROM " +
- "`books_search` WHERE MATCH (`authors_search`." +
- "`search_name`) AGAINST ('#{query}' IN #{mode})"]
- offset = offset - count.first[:count]
- end
- books = DB["SELECT DISTINCT `books`.`id`,`books`.`title`, " +
- "`books`.`full_title`, `books`.`sysno`, `books`.`isbn`," +
- " `books`.`average_rating`, `books`.`cover_url`, `books" +
- "`.`preview_url` #{select} FROM `books` INNER JOIN `books_authors" +
- "` ON (`books`.`id` = `books_authors`.`book_id`) INNER " +
- "JOIN `authors_search` ON (`books_authors`.`author_id` " +
- "= `authors_search`.`author_id`) #{join} WHERE MATCH (" +
- "`authors_search`.`search_name`) AGAINST ('#{query}'" +
- " IN #{mode}) #{where}" +
- " LIMIT #{limit - output.count} OFFSET #{offset}"]
- output = add_books(books, output)
- end
- again = false if attempt > 1 || !output.empty?
- end
- end
- p "MySQL: #{query} #{(Time.now - now)}"
- my_json output
- end
- def add_books books, output
- books.each do |book|
- b = {
- 'sysno' => book[:sysno],
- 'title' => book[:title].gsub(' :', ': '),
- 'fullTitle' => book[:full_title] ? book[:full_title].gsub(' :', ': ') : '',
- 'authors' => get_authors(book[:id]),
- 'averageRating' => book[:average_rating],
- 'ratingCount' => get_rating_count(book[:id])
- }
- b.merge!(download_image(book))
- output << b
- end if books
- output
- end
- # Load book info
- get '/books' do
- # get book detail
- search = {}
- if params.include?('sysno') || params.include?('isbn')
- search[:sysno] = params['sysno'] if params.include? 'sysno'
- search[:isbn] = params['isbn'] if params.include? 'isbn'
- result = DB[:books].select(
- :books__id, :books__sysno, :books__full_title, :books__page_desc,
- :books__pages, :books__preview_url, :books__cover_url, :books__title,
- :publishers__publisher, :years__year, :languages__language,
- :page_types__page_type, :books__isbn
- ).join_table(:left, :publishers, :id => :books__publisher_id
- ).join_table(:left, :years, :id => :books__year_id
- ).join_table(:left, :page_types, :id => :books__page_type_id
- ).join_table(:left, :languages, :id => :books__language_id
- ).filter(search).first
- simple_error if search.empty?
- elsif params.include?('barcode')
- result = DB[:books].select(
- :books__id, :books__sysno, :books__full_title, :books__page_desc,
- :books__pages, :books__preview_url, :books__cover_url, :books__title,
- :publishers__publisher, :years__year, :languages__language,
- :page_types__page_type, :books__isbn
- ).join_table(:inner, :copies, :copies__book_id => :id
- ).join_table(:left, :publishers, :id => :books__publisher_id
- ).join_table(:left, :years, :id => :books__year_id
- ).join_table(:left, :page_types, :id => :books__page_type_id
- ).join_table(:left, :languages, :id => :books__language_id
- ).filter(:copies__barcode => params['barcode']).first
- end
- if result
- rslt = {
- 'sysno' => result[:sysno],
- 'isbn' => result[:isbn],
- 'title' => result[:title],
- 'fullTitle' => result[:full_title],
- 'publisher' => result[:publisher],
- 'publishedDate' => result[:year],
- 'language' => result[:language],
- 'pageCount' => result[:pages],
- 'pageType' => result[:page_type],
- 'pageDesc' => result[:page_desc],
- 'authors' => get_authors(result[:id]),
- }
- rslt.merge!(download_image(result))
- my_json rslt
- else
- not_found
- end
- end
- def get_authors id
- rslt = []
- authors = DB[:authors].select(:name, :author_type
- ).join_table(:inner, :books_authors, :author_id => :id
- ).join_table(:left, :author_types, :id => :author_type_id
- ).filter(
- :books_authors__book_id => id
- )
- authors.each do |author|
- rslt << {'type' => author[:author_type], 'name' => author[:name] }
- end if authors
- rslt
- end
- # Post new book review
- post '/books/:sysno/reviews' do
- simple_error unless
- params.include?('rating') || params.include?('text')
- params['rating'] = 5 if params['rating'].to_i > 5
- params['rating'] = 0 if params['rating'].to_i < 0
- person, device, temp = nil, nil, {}
- # user search
- if session[:session_api]
- person = self.class::get_person_by_session session[:session_api]
- elsif params.include?('device_id')
- device = DB[:devices].filter(:name => params['device_id']).first
- unless device
- device = DB[:devices].insert(:name => params['device_id'])
- device = DB[:devices].filter(:id => device).first
- end
- end
- simple_error unless person || device
- book = DB[:books].filter(:sysno => params['sysno']).first
- simple_error unless book
- rating = DB[:ratings].filter(:book_id => book[:id])
- rating = rating.and(:person_id => person[:id]) if person
- rating = rating.and(:device_id => device[:id]) if device
- rating = rating.first
- temp[:rating] = params['rating'] if params.include?('rating')
- temp[:review] = params['text'] if params.include?('text')
- temp[:person_id] = person[:id] if person && person[:id]
- temp[:device_id] = device[:id] if device && device[:id]
- temp[:time] = my_now
- if rating
- DB[:ratings].filter(:id => rating[:id]).update(temp)
- else
- temp[:book_id] = book[:id]
- DB[:ratings].insert(temp)
- end
- DB[:books].filter(:id => book[:id]).update(
- :average_rating => compute_rating(book)
- )
- ok
- end
- # Get actual rating
- get '/books/:sysno/reviews' do
- not_found unless params.include?('sysno') && params['sysno'] &&
- params['sysno'] =~ /\d+/
- limit = params.include?('limit') ? params['limit'].to_i : 20
- offset = params.include?('offset')? params['offset'].to_i : 0
- ratings = DB[:ratings].join_table(:inner, :books, :id => :book_id
- ).join_table(:inner, :people, :id => :ratings__person_id
- ).filter(:sysno => params['sysno']).limit(limit, offset)
- output = []
- ratings.each do |rating|
- output << {
- 'user' => get_person(rating),
- 'text' => rating[:review],
- 'rating' => rating[:rating],
- 'date' => rating[:time],
- }
- end
- my_json(output) if output
- end
- get '/books/:sysno/ratings' do
- ratings = DB[:ratings].select_more{:rating}.group_and_count(:rating).join_table(
- :inner, :books, :id => :book_id
- ).filter(:sysno => params['sysno'])
- output = {}
- ['0','0.5','1.0','1.5','2.5','2.5','3.5','3.5','4.5','4.5','5.0'].each{
- |i| output[i] = 0
- }
- ratings.each do |rating|
- output[rating[:rating]] = rating[:count]
- end
- return my_json output
- end
- # return json person
- def get_person person
- {
- 'firstName' => person[:first_name],
- 'lastName' => person[:surname],
- 'uco' => person[:username]
- } if person
- end
- post '/report' do
- simple_error unless
- params.include?('uco') &&
- params.include?('text') &&
- params.include?('type')
- params['type'] = 'bug' if params['type'].eql?('error')
- params['type'] = 'task' if params['type'].eql?('issue')
- backtrace = params.include?('backtrace') ?
- params['backtrace'] : ''
- begin
- RestClient::Request.new(
- 'method' => :post,
- 'url' => ISSUES_URL,
- 'user' => $CONFIG[:bitbucket_login],
- 'password' => $CONFIG[:bitbucket_passwd],
- 'accept' => :json,
- 'payload' => {
- 'title' => 'New issue: ' + params['uco'],
- 'kind' => params['type'],
- 'content' =>
- "User: #{params['uco']}
- Text:
- #{params['text']}" +
- "#{backtrace}"
- }).execute
- rescue => e
- p e.message
- simple_error
- end
- ok
- end
- get '/books/:sysno/details' do
- self.class::simple_error unless params.include? 'sysno'
- book = DB[:books].select(
- :books__id, :books__sysno, :books__pages, :books__isbn,
- :publishers__publisher, :years__year, :languages__language,
- :page_types__page_type, :books__isbn, :books__cover_url,
- :books__preview_url
- ).join_table(:left, :publishers, :id => :books__publisher_id
- ).join_table(:left, :years, :id => :books__year_id
- ).join_table(:left, :page_types, :id => :books__page_type_id
- ).join_table(:left, :languages, :id => :books__language_id
- ).filter(:sysno => params['sysno']).first
- return not_found unless book
- b = {
- 'publisher' => book[:publisher],
- 'publishedDate' => book[:year],
- 'isbn' => book[:isbn],
- 'language' => book[:language],
- 'pageType' => book[:page_type],
- 'pageCount' => book[:pages],
- }
- b.merge!(download_image(book))
- my_json b
- end
- get '/books/:sysno/copies' do
- simple_error unless params.include?('sysno') && params['sysno'] &&
- params['sysno'] =~ /\d+/
- copies, rslt = DB[:copies].select(
- :copies__signature, :copies__barcode, :copies__status,
- :copies__last_check, :books__id, :libraries__library,
- :cols__col, :copies__availability
- ).join_table(:inner, :books, :id => :book_id
- ).join_table(:left, :libraries, :id => :copies__library_id
- ).join_table(:left, :cols, :id => :copies__col_id
- ).filter(:books__sysno => params[:sysno]), []
- not_found unless copies
- copies.each{ |copy| rslt << get_copy(copy) }
- # no information or information older then 1 day
- if rslt.count > 0 && !rslt[0]['last_check'] || Time.parse(rslt[0]['last_check']).to_i+86400 < Time.now.to_i
- rslt = check_availability params['sysno'], rslt.map{|i| i['barcode'] }
- end
- my_json rslt
- end
- # get top or new ratings
- get '/books/list' do
- limit = params.include?('limit')? params['limit'].to_i : 20
- offset = params.include?('offset')? params['offset'].to_i : 0
- if params.include?('category')
- params['category'] = 'new' unless
- params[:category].eql?('new') || params[:category].eql?('top')
- else
- params['category'] = 'new'
- end
- if params['category'].eql?('new')
- books = DB[:books].select(:books__id, :books__sysno, :books__title, :books__full_title,
- :books__average_rating, :books__isbn, :books__cover_url, :books__preview_url
- ).select_more{count(:ratings__id)}.join_table(:inner, :ratings, :book_id => :id
- ).group(:books__id).reverse(:ratings__time).limit(limit, offset)
- elsif params['category'].eql?('top')
- books = DB[:books].select(:books__id, :books__sysno, :books__title, :books__full_title,
- :books__average_rating, :books__isbn, :books__cover_url, :books__preview_url
- ).select_more{count(:ratings__id)}.join_table(:inner, :ratings, :book_id => :id
- ).group(:books__id).reverse(:books__average_rating, :ratings__time).limit(limit, offset)
- end
- not_found unless books
- output = []
- books.each do |book|
- b = {
- 'sysno' => book[:sysno],
- 'title' => book[:title],
- 'fullTitle' => book[:full_title],
- 'authors' => get_authors(book[:id]),
- 'averageRating' => book[:average_rating],
- 'ratingCount' => book[:"count(`ratings`.`id`)"]
- }
- b.merge!(download_image(book))
- output << b
- end
- my_json output
- end
- def download_image book
- # download info from google books
- return {
- 'coverUrl' => nil,
- 'previewUrl' => nil,
- } if !book[:isbn] || (book[:cover_url] && book[:cover_url].eql?(''))
- new_book = {}
- unless book[:cover_url]
- # first try => google books
- new_book[:cover_url] = '' # default set
- result = download_preview_google book
- if result && result.count > 0
- key = result[result.keys[0]]
- new_book[:cover_url] = key['thumbnail_url'].gsub('zoom=5', 'zoom=1') if
- key.include?('thumbnail_url')
- new_book[:preview_url] = "#{key['preview_url']}#v=onepage&q&f=true" if
- key.include?('preview_url') && key['preview_url'] =~ /printsec=frontcover/i
- end
- if new_book[:cover_url].eql?('')
- request =
- "http://www.obalkyknih.cz/api/books?books=#{URI.encode(
- "[{ \"permalink\":\"test\", \"bibinfo\": {\"isbn\":\"#{book[:isbn]}\"}}]")}"
- result = RestClient.get request
- if result
- result = result =~ /\[(.+)\]/i ? Oj.load($1) : nil
- if result
- new_book[:cover_url] = result.include?('cover_medium_url') ?
- result['cover_medium_url'] : ""
- end
- end
- end
- DB[:books].filter(:id => book[:id]).update(new_book)
- book = DB[:books].filter(:id => book[:id]).first
- end
- {
- 'coverUrl' => book[:cover_url],
- 'previewUrl' => book[:preview_url],
- }
- end
- def download_preview_google book
- result = RestClient.get(GOOGLE.sub('##', URI.encode(book[:isbn])))
- result =~ /(\{.*\})/ ? Oj.load($1) : nil
- end
- get '/spec' do
- books = Book.where('cover_url' => "false")
- books.each do |book|
- book.cover_url = ""
- book.save
- end
- end
- # return rating by json
- def compute_rating book
- rating, counter = 0, 0
- books = DB[:ratings].filter(:book_id => book[:id])
- books.each do |book|
- if book[:rating]
- counter += 1
- rating = rating + book[:rating]
- end
- end
- if counter > 0
- rating = rating / counter
- rating.round(1)
- else
- rating = nil
- end
- if rating
- r_ = rating % 1
- rating += 1 - r_ if (r_ >= 0.75 && r_ < 1)
- rating += 0.5 - r_ if (r_ >= 0.25 && r_ < 0.5)
- rating -= r_ - 0.5 if (r_ > 0.5 && r_ < 0.75)
- rating -= r_ if (r_ > 0 && r_ < 0.25)
- end
- rating
- end
- # ---------------- USER FUNCTIONS -------------------
- # logout
- post '/user/logout' do
- protected!
- session['session_api'] = nil
- ok
- end
- # register new user
- post '/user/registration' do
- not_found unless params.include?('firstName') &&
- params.include?('lastName') &&
- params.include?('uco')
- simple_error 409 if
- DB[:people].filter(:username => params['uco'], :active => true).count > 0
- password = self.class::get_random_text 10
- person = DB[:people].insert(
- {
- 'first_name' => params['firstName'],
- 'surname' => params['lastName'],
- 'username' => params['uco'],
- 'email' => "#{params['uco']}@mail.muni.cz",
- 'password' => self.class::md5(password),
- 'active' => true,
- 'admin' => false
- }
- )
- person = DB[:people].filter(:id => person).first
- text = erb :registration, :locals => {
- :username => params['uco'],
- :password => password
- }
- self.class::send_email person[:email], 'SmartLib - registrace', text
- ok
- end
- # register new user
- get '/user/lostpassword' do
- not_found unless params.include?('uco')
- person = DB[:people].filter(
- :username => params['uco'], :active => true
- ).first
- simple_error 409 unless person
- password = self.class::get_random_text 10
- DB[:people].filter(:id => person[:id]).update(
- :temp_password => self.class::md5(password),
- :temp_password_time => my_now)
- text = erb :lost_password, :locals => {
- :username => params['uco'],
- :password => password
- }
- self.class::send_email person[:email], 'SmartLib - dočasné heslo', text
- ok
- end
- # change password
- post '/user/changepassword' do
- self.class::simple_error unless params.include?('uco') &&
- params.include?('oldPassword') && params.include?('newPassword')
- person = self.class::authenticate params['uco'], params['oldPassword']
- unauthorized unless person
- DB[:people].filter(:id => person[:id]
- ).update(:password => self.class::md5(params['newPassword']))
- ok
- end
- # authenticate!
- post '/user/authentication' do
- if session.include?('session_api')
- return self.class::get_person_by_session(session['session_api']) ?
- ok : unauthorized
- end
- unauthorized
- end
- # log person in
- post '/user/login' do
- simple_error unless params.include?('uco') && params.include?('password')
- if person = self.class::authenticate(params[:uco], params[:password])
- session_api = self.class::get_random_text 50
- DB[:people].filter(:id => person[:id]).update(:session_api => session_api)
- session['session_api'] = session_api
- return ok
- end
- session[:session_api] = nil
- unauthorized
- end
- get '/libraries' do
- libraries = DB[:libraries]
- output = []
- libraries.each do |library|
- output << {
- 'library' => library[:library]
- }
- end
- my_json output
- end
- def get_rating_count id
- DB[:ratings].filter(:book_id => id).count
- end
- get '/books/:sysno/check' do
- not_found unless params.include?('sysno') && params['sysno'] &&
- params['sysno'] =~ /\d+/
- book = DB[:books].filter(:sysno => params['sysno']).first
- not_found unless book && params.include?('barcodes')
- otpt, barcodes = [], params['barcodes'].split(',')
- my_json(check_availability(params['sysno'], barcodes))
- end
- def check_availability sysno, barcodes
- begin
- url, otpt = COPY_URL.sub('##', sysno), []
- doc = Nokogiri::HTML(RestClient.get(url))
- not_found unless doc
- url = doc.at_xpath('//a[contains(text(),"Všechny jednotky")]/@href') or not_found
- url = URL_ALEPH + url.content
- doc = Nokogiri::HTML(RestClient.get(url))
- not_found unless doc
- last = my_now
- barcodes.each do |barcode|
- copy = DB[:copies].filter(:barcode => barcode).first
- next unless copy
- find = doc.at_xpath('//span[contains(text(),"' +barcode+'")]')
- unless find
- otpt << {
- 'barcode' => barcode,
- 'status' => 404,
- 'last_check' => output_time(Time.now)
- }
- next
- end
- status = find.at_xpath('./../preceding-sibling::td[6]') || ''
- back = find.at_xpath('./../preceding-sibling::td[5]')
- back = back ? back.content : ''
- type = copy[:status] ? 'prs' : 'abs'
- if back =~ /\d+\-\d+\-\d+/
- DB[:copies].filter(:id => copy[:id]).update(
- :availability => false, :last_check => last
- )
- otpt << {
- 'barcode' => barcode,
- 'status' => false,
- 'type' => type,
- 'back' => back,
- 'last_check' => output_time(Time.parse(last))
- }
- else
- DB[:copies].filter(:id => copy[:id]).update(
- :availability => true, :last_check => last
- )
- otpt << {
- 'barcode' => barcode,
- 'status' => true,
- 'back' => back,
- 'type' => type,
- 'last_check' => output_time(Time.parse(last))
- }
- end
- end
- otpt
- rescue StandardError => bang
- p bang.message
- p bang.backtrace
- not_found
- end
- end
- def my_now
- Time.now.strftime('%Y-%m-%d %H:%M:%S')
- end
- def output_time time
- time.getutc.iso8601(0)
- end
- def get_copy copy
- last_check = copy[:last_check] ?
- output_time(copy[:last_check]) : nil
- {
- 'signature' => copy[:signature],
- 'barcode' => copy[:barcode],
- 'last_check' => last_check,
- 'status' => copy[:availability] ? true : false,
- 'col' => copy[:col],
- 'panel' => nil,
- 'library' => copy[:library],
- 'type' => copy[:status] && copy[:status].eql?('1') ?
- 'prs' : 'abs'
- }
- end
- end
- end
- end