167 lines
3.7 KiB
Go
Raw Normal View History

package recommended
import (
2018-03-27 08:13:27 +00:00
"math"
"net/http"
"sort"
"github.com/aerogo/aero"
"github.com/animenotifier/arn"
"github.com/animenotifier/notify.moe/components"
2018-02-24 14:35:34 +00:00
"github.com/animenotifier/notify.moe/utils"
)
2018-02-24 13:51:47 +00:00
const (
2018-03-27 08:13:27 +00:00
maxRecommendations = 10
bestGenreCount = 3
genreBonusCompletedAnimeThreshold = 40
2018-02-24 13:51:47 +00:00
)
// Anime shows a list of recommended anime.
func Anime(ctx *aero.Context) string {
2018-02-24 14:35:34 +00:00
user := utils.GetUser(ctx)
nick := ctx.Get("nick")
2018-02-24 14:35:34 +00:00
viewUser, err := arn.GetUserByNick(nick)
if err != nil {
return ctx.Error(http.StatusUnauthorized, "Not logged in", err)
}
2018-02-24 14:35:34 +00:00
animeList := viewUser.AnimeList()
2018-03-27 08:13:27 +00:00
completed := animeList.FilterStatus(arn.AnimeListStatusCompleted)
// Genre affinity
genreItems := animeList.Genres()
genreAffinity := map[string]float64{}
bestGenres := []string{}
for genre, animeListItems := range genreItems {
affinity := 0.0
for _, item := range animeListItems {
if item.Status != arn.AnimeListStatusCompleted {
continue
}
if item.Rating.Overall != 0 {
affinity += item.Rating.Overall
} else {
affinity += 5.0
}
}
genreAffinity[genre] = affinity
bestGenres = append(bestGenres, genre)
}
sort.Slice(bestGenres, func(i, j int) bool {
return genreAffinity[bestGenres[i]] > genreAffinity[bestGenres[j]]
})
if len(bestGenres) > bestGenreCount {
bestGenres = bestGenres[:bestGenreCount]
}
// Get all anime
var tv []*arn.Anime
var movies []*arn.Anime
allAnime := arn.AllAnime()
// Affinity maps an anime ID to a number that indicates how likely a user is going to enjoy that anime.
affinity := map[string]float64{}
// Calculate affinity for each anime
for _, anime := range allAnime {
2018-03-04 00:02:30 +00:00
// Skip anime that are upcoming or tba
if anime.Status == "upcoming" || anime.Status == "tba" {
2018-02-24 13:51:47 +00:00
continue
}
if anime.Type == "tv" {
tv = append(tv, anime)
} else if anime.Type == "movie" {
movies = append(movies, anime)
}
// Skip anime from my list (except planned anime)
existing := animeList.Find(anime.ID)
if existing != nil && existing.Status != arn.AnimeListStatusPlanned {
continue
}
animeAffinity := anime.Score()
2018-02-24 13:51:47 +00:00
// Planned anime go higher
if existing != nil && existing.Status == arn.AnimeListStatusPlanned {
animeAffinity += 10.0
}
// Anime whose high-ranked prequel you did not see are lower ranked
prequels := anime.Prequels()
for _, prequel := range prequels {
2018-03-27 08:13:27 +00:00
item := completed.Find(prequel.ID)
// Filter out unimportant prequels
if prequel.Score() < anime.Score()/2 {
continue
}
2018-03-27 08:13:27 +00:00
if item == nil {
animeAffinity -= 20.0
}
2018-02-24 13:51:47 +00:00
}
2018-03-27 08:13:27 +00:00
// Give favorite genre bonus if we have enough completed anime
if len(completed.Items) >= genreBonusCompletedAnimeThreshold {
bestGenreCount := 0
for _, genre := range anime.Genres {
if arn.Contains(bestGenres, genre) {
bestGenreCount++
}
}
// Use square root to dampen the bonus of additional best genres
animeAffinity += math.Sqrt(float64(bestGenreCount)) * 7.0
}
2018-02-24 13:51:47 +00:00
affinity[anime.ID] = animeAffinity
}
// Sort
sort.Slice(tv, func(i, j int) bool {
affinityA := affinity[tv[i].ID]
affinityB := affinity[tv[j].ID]
if affinityA == affinityB {
return tv[i].Title.Canonical < tv[j].Title.Canonical
}
return affinityA > affinityB
})
sort.Slice(movies, func(i, j int) bool {
affinityA := affinity[movies[i].ID]
affinityB := affinity[movies[j].ID]
2018-02-24 13:51:47 +00:00
if affinityA == affinityB {
return movies[i].Title.Canonical < movies[j].Title.Canonical
2018-02-24 13:51:47 +00:00
}
return affinityA > affinityB
})
// Take the top 10
if len(tv) > maxRecommendations {
tv = tv[:maxRecommendations]
}
if len(movies) > maxRecommendations {
movies = movies[:maxRecommendations]
}
return ctx.HTML(components.RecommendedAnime(tv, movies, viewUser, user))
}