app/api/api_v1.rb
class API_v1 < Grape::API
version 'v1', using: :path, format: :json, vendor: 'hummingbird'
formatter :json, lambda {|object, env| MultiJson.dump(object) }
rescue_from ActiveRecord::RecordNotFound
helpers do
def current_user
return env['current_user'] if env.has_key?('current_user')
if params[:auth_token] || cookies[:auth_token]
token = Token.new(params[:auth_token] || cookies[:auth_token])
error! "Invalid authentication token", 401 if token.invalid?
env['current_user'] = token.user
else
env['current_user'] = nil
end
env['current_user']
end
def user_signed_in?
!current_user.nil?
end
def authenticate_user!
if user_signed_in?
return true
else
error!("401 Unauthenticated", 401)
end
end
def current_ability
@current_ability ||= Ability.new(current_user)
end
def find_user(id)
begin
if id == "me" and user_signed_in?
current_user
else
User.find(id)
end
rescue
error!("404 Not Found", 404)
end
end
def save_favorite(key, value)
item = value
favorite = Favorite.find(item.id)
if favorite.user == current_user
favorite.fav_rank = item.fav_rank
favorite.save
else
error!("Unauthorized", 403)
end
end
def present_library_entry(e, rating_type, title_language_preference)
{
id: e.id,
episodes_watched: e.episodes_watched,
last_watched: e.last_watched || e.updated_at,
updated_at: e.updated_at,
rewatched_times: e.rewatch_count,
notes: e.notes,
notes_present: (e.notes and e.notes.strip.length > 0),
status: e.status.downcase.gsub(' ', '-'),
private: e.private,
rewatching: e.rewatching,
anime: present_anime(e.anime, title_language_preference),
rating: {
type: rating_type,
value: e.rating
}
}
end
def present_miniuser(user)
{
name: user.name,
url: "https://hummingbird.me/users/#{user.name}",
avatar: user.avatar.url(:thumb),
avatar_small: user.avatar.url(:thumb_small),
nb: user.ninja_banned?
}
end
def present_anime(anime, title_language_preference)
if anime
{
id: anime.id,
mal_id: anime.mal_id,
slug: anime.slug,
status: anime.status,
url: "https://hummingbird.me/anime/#{anime.slug}",
title: anime.canonical_title(title_language_preference),
alternate_title: anime.alternate_title(title_language_preference),
episode_count: anime.episode_count,
episode_length: anime.episode_length,
cover_image: anime.poster_image.url(:large),
synopsis: anime.synopsis,
show_type: anime.show_type,
started_airing: anime.started_airing_date,
finished_airing: anime.finished_airing_date,
community_rating: anime.bayesian_rating,
age_rating: anime.age_rating.blank? ? nil : anime.age_rating,
genres: anime.genres.map {|x| {name: x.name} }
}
else
{}
end
end
def present_favorite_anime(favorite, title_language_preference)
if favorite
json = present_anime(favorite.item, title_language_preference)
json[:fav_rank] = favorite.fav_rank
json[:fav_id] = favorite.id
json
else
{}
end
end
def present_favorite(favorite)
if favorite
json = {
id: favorite.id,
item_type: favorite.item_type,
fav_rank: favorite.fav_rank
}
else
{}
end
end
def present_story(story, current_user, title_language_preference)
json = {
id: story.id,
story_type: story.story_type,
user: present_miniuser(story.user),
updated_at: story.updated_at,
}
if story.story_type == "comment"
json[:self_post] = (story.target == story.user)
json[:poster] = present_miniuser(story.target)
elsif story.story_type == "media_story"
json[:media] = present_anime(story.target, title_language_preference)
end
substories = story.substories
json[:substories_count] = substories.length
json[:substories] = substories.map do |substory|
subjson = {
id: substory.id,
substory_type: substory.substory_type,
created_at: substory.created_at
}
if substory.substory_type == "followed"
subjson[:followed_user] = present_miniuser(substory.target)
elsif substory.substory_type == "watchlist_status_update"
subjson[:new_status] = (substory.data["new_status"] || "Currently Watching").downcase.gsub(' ', '_')
elsif substory.substory_type == "watched_episode"
subjson[:episode_number] = substory.data["episode_number"]
subjson[:service] = substory.data["service"]
elsif substory.substory_type == "comment"
if substory.data["formatted_comment"].nil?
data = substory.data.dup
data["formatted_comment"] = MessageFormatter.format_message substory.data["comment"]
substory.data = data
substory.save
end
subjson[:comment] = substory.data["formatted_comment"]
end
if current_user and ((substory.user_id == current_user.id) or (story.user_id == current_user.id) or current_user.admin?)
subjson[:permissions] = {destroy: true}
else
subjson[:permissions] = {}
end
subjson
end
json
end
end
desc "Return the user's timeline"
params do
optional :page, type: Integer
end
get '/timeline' do
if user_signed_in?
NewsFeed.new(current_user).fetch(params[:page]).map {|x| present_story(x, current_user, current_user.title_language_preference) }
else
[]
end
end
resource :users do
desc "Return authentication code"
params do
optional :username, type: String
optional :email, type: String
requires :password, type: String
end
post '/authenticate' do
user = nil
if params[:username]
user = User.where("LOWER(name) = ?", params[:username].downcase).first
elsif params[:email]
user = User.where("LOWER(email) = ?", params[:email].downcase).first
end
if user.nil? || !user.valid_password?(params[:password])
error! "Invalid credentials", 401
end
Token.new(user.id, scope: ['all']).encode
end
desc "Return the current user."
params do
requires :username, type: String
end
get ':username' do
user = find_user(params[:username])
json = {
name: user.name,
waifu: user.waifu,
waifu_or_husbando: user.waifu_or_husbando,
waifu_slug: user.waifu_slug,
waifu_char_id: user.waifu_char_id,
location: user.location,
website: user.website,
avatar: user.avatar.url(:thumb),
cover_image: user.cover_image.url(:thumb),
about: user.about,
bio: user.bio,
karma: 0,
life_spent_on_anime: user.life_spent_on_anime,
show_adult_content: !user.sfw_filter?,
title_language_preference: user.title_language_preference,
last_library_update: user.last_library_update,
following: (user_signed_in? and user.followers.include?(current_user)),
favorites: user.favorites
}
if user == current_user
json["email"] = user.email
end
json
end
desc "Return the entries in a user's library under a specific status."
params do
requires :user_id, type: String
optional :status, type: String
optional :page, type: Integer
optional :title_language_preference, type: String
end
get ':user_id/library' do
if params[:page] and params[:page] > 1
return []
end
user = find_user(params[:user_id])
status = LibraryEntry::SNAKE_STATUSES[params[:status]]
entries = user.library_entries
entries = entries.where(status: status) if status
entries = entries.where(private: false) if user != current_user
title_language_preference = params[:title_language_preference]
if title_language_preference.nil? and current_user
title_language_preference = current_user.title_language_preference
end
title_language_preference ||= "canonical"
rating_type = user.star_rating? ? "advanced" : "simple"
entries.map {|e| present_library_entry(e, rating_type, title_language_preference) }
end
desc "Returns the user's favorite Animes"
params do
requires :user_id, type: String
optional :page, type: Integer
end
get ":user_id/favorite_anime" do
user = find_user(params[:user_id])
favorite_anime = user.favorites.where(item_type: "Anime").includes(:item, item: :genres).order('fav_rank')
favorite_anime.map {|a| present_favorite_anime(a, user.try(:title_language_preference) || "canonical")}
end
desc "Updates User's Favorite ranks"
params do
requires :data
end
post ":user_id/favorite_anime/update" do
data = params[:data]
data.each {|key,value| save_favorite(key,value)}
data
end
desc "Returns the user's feed."
params do
requires :user_id, type: String
optional :page, type: Integer
end
get ":user_id/feed" do
user = find_user(params[:user_id])
# Find stories to display.
stories = user.stories.for_user(current_user)
.includes(:substories, :user, :target)
.order('updated_at DESC').page(params[:page]).per(20)
# fetche anime stories and preload genres association
# WORKAROUND: See rails/rails#17479
anime_stories = stories.group_by(&:target_type).fetch("Anime",[])
ActiveRecord::Associations::Preloader.new.preload(anime_stories, target: [:genres])
stories.map {|x| present_story(x, current_user, current_user.try(:title_language_preference) || "canonical") }
end
desc "Returns the user's list of favorites"
params do
requires :user_id, type: String
end
get ":user_id/favorites" do
@user = User.find(params[:user_id])
@favorites = @user.favorites.order('fav_rank')
@favorites.map {|f| present_favorite(f)}
end
desc "Delete a substory from the user's feed."
params do
requires :user_id, type: String
requires :story_id, type: Integer
end
post ":user_id/feed/remove" do
begin
story = Story.find params[:story_id]
rescue
return true
end
if story.can_be_deleted_by?(current_user)
story.destroy!
return true
else
return false
end
end
end
resource :libraries do
desc "Remove an entry"
params do
requires :anime_slug, type: String
end
post ':anime_slug/remove' do
authenticate_user!
anime = Anime.find(params["anime_slug"])
library_entry = LibraryEntry.where(user_id: current_user.id, anime_id: anime.id).first
library_entry.destroy
true
end
desc "Get recently updated Entries"
get '/recent' do
entries = LibraryEntry.where(user_id: current_user).order("updated_at DESC").limit(12)
{library_entries: entries.to_a}
end
desc "Update a specific anime's details in a user's library."
params do
requires :anime_slug, type: String
optional :increment_episodes, type: String
optional :rewatching, type: String
optional :include_mal_id, type: String
optional :title_language_preference, type: String
end
post ':anime_slug' do
authenticate_user!
anime = Anime.find(params["anime_slug"])
library_entry = LibraryEntry.where(user_id: current_user.id, anime_id: anime.id).first_or_initialize
if params[:status]
t = {
"currently-watching" => "Currently Watching",
"plan-to-watch" => "Plan to Watch",
"completed" => "Completed",
"on-hold" => "On Hold",
"dropped" => "Dropped"
}
library_entry.status = t[params[:status]]
if library_entry.status == "Completed" and anime.episode_count
# Mark all episodes as viewed when the show is "Completed".
library_entry.episodes_watched = anime.episode_count
end
end
# Update privacy.
if params[:privacy]
if params[:privacy] == "private"
library_entry.private = true
elsif params[:privacy] == "public"
library_entry.private = false
end
end
# Update rewatched_times.
if params[:rewatched_times]
library_entry.rewatch_count = params[:rewatched_times]
end
# Update notes.
if params[:notes]
library_entry.notes = params[:notes]
end
# Update episode count.
if params[:episodes_watched]
library_entry.episodes_watched = params[:episodes_watched]
end
# Update "rewatching" status.
if params[:rewatching]
library_entry.rewatching = (params[:rewatching] == "true")
end
if params[:increment_episodes] and params[:increment_episodes] == "true"
library_entry.status = "Currently Watching"
library_entry.episodes_watched += 1
end
title_language_preference = params[:title_language_preference]
if title_language_preference.nil? and current_user
title_language_preference = current_user.title_language_preference
end
title_language_preference ||= "canonical"
rating_type = current_user.star_rating? ? "advanced" : "simple"
# Update rating.
if params[:rating]
rating = params[:rating].to_f
if library_entry.rating == rating or rating == 0
library_entry.rating = nil
else
library_entry.rating = [ [0, rating].max, 5].min
end
result = result and library_entry.save
end
#Update rating without the weird behaviour.
if params[:sane_rating_update]
rating = params[:sane_rating_update].to_f
if rating == 0
library_entry.rating = nil
else
library_entry.rating = [ [0, rating].max, 5].min
end
result = result and library_entry.save
end
if library_entry.episodes_watched_changed? and library_entry.episodes_watched == library_entry.anime.episode_count
library_entry.status = "Completed"
end
Action.from_library_entry(library_entry)
if library_entry.save
wl = present_library_entry(library_entry.reload, rating_type, title_language_preference)
wl = wl.merge(mal_id: anime.mal_id) if params[:include_mal_id] == "true"
wl
else
return false
end
end
end
resource :anime do
desc "Return an anime"
params do
requires :id, type: String, desc: "anime ID"
optional :title_language_preference, type: String
end
get ':id' do
anime = Anime.find(params[:id])
title_language_preference = params[:title_language_preference]
if title_language_preference.nil? and current_user
title_language_preference = current_user.title_language_preference
end
title_language_preference ||= "canonical"
present_anime(anime, title_language_preference)
end
desc "Return an Anime Slug Searched From Character Id"
params do
requires :id, type: String, desc: "character ID"
end
get 'casting/:id' do
casting = Casting.where(character_id: params[:id]).first.anime_id
anime = Anime.find(casting)
json = {anime_slug: anime.slug }
json
end
desc "Returns similar anime."
params do
requires :id, type: String, desc: "anime ID"
optional :limit, type: Integer, desc: "number of results (max/default 20)"
end
get ':id/similar' do
anime = Anime.find(params[:id])
similar_anime = []
similar_json = JSON.load(open("http://app.vikhyat.net/anime_graph/related/#{anime.id}")).sort_by {|x| -x["sim"] }
similar_json.each do |similar|
sim = Anime.find_by_id(similar["id"])
similar_anime.push(sim) if sim and similar_anime.length < (params[:limit] || 20)
end
similar_anime.map {|x| {id: x.slug, title: x.canonical_title, alternate_title: x.alternate_title, genres: x.genres.map {|x| {name: x.name} }, cover_image: x.poster_image.url(:large), url: anime_url(x)} }
end
end
desc "Anime search API endpoint"
params do
requires :query, type: String, desc: "query string"
optional :title_language_preference, type: String
end
get '/search/anime' do
anime = Anime.accessible_by(current_ability).includes(:genres)
results = anime.instant_search(params[:query]).limit(20)
if results.length == 0
results = anime.full_search(params[:query]).limit(20)
end
title_language_preference = params[:title_language_preference]
if title_language_preference.nil? and current_user
title_language_preference = current_user.title_language_preference
end
title_language_preference ||= "canonical"
results.map {|x| present_anime(x, title_language_preference) }
end
end