2018-02-24 12:19:11 +00:00
|
|
|
package recommended
|
|
|
|
|
|
|
|
import (
|
|
|
|
"net/http"
|
|
|
|
"sort"
|
|
|
|
|
|
|
|
"github.com/aerogo/aero"
|
2019-06-03 09:32:43 +00:00
|
|
|
"github.com/animenotifier/notify.moe/arn"
|
2018-02-24 12:19:11 +00:00
|
|
|
"github.com/animenotifier/notify.moe/components"
|
|
|
|
)
|
|
|
|
|
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
|
|
|
)
|
2018-02-24 12:19:11 +00:00
|
|
|
|
|
|
|
// Anime shows a list of recommended anime.
|
2019-06-01 04:55:49 +00:00
|
|
|
func Anime(ctx aero.Context) error {
|
2019-11-17 07:59:34 +00:00
|
|
|
user := arn.GetUserFromContext(ctx)
|
2018-02-24 12:19:11 +00:00
|
|
|
nick := ctx.Get("nick")
|
2018-02-24 14:35:34 +00:00
|
|
|
viewUser, err := arn.GetUserByNick(nick)
|
2018-02-24 12:19:11 +00:00
|
|
|
|
|
|
|
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
|
2018-11-15 11:19:40 +00:00
|
|
|
bestGenres := animeList.TopGenres(bestGenreCount)
|
2018-03-27 08:13:27 +00:00
|
|
|
|
2018-02-24 12:19:11 +00:00
|
|
|
// Get all anime
|
2018-03-23 22:48:15 +00:00
|
|
|
var tv []*arn.Anime
|
|
|
|
var movies []*arn.Anime
|
|
|
|
allAnime := arn.AllAnime()
|
2018-02-24 12:19:11 +00:00
|
|
|
|
|
|
|
// 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
|
2018-03-23 22:48:15 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2019-06-07 01:03:16 +00:00
|
|
|
switch anime.Type {
|
|
|
|
case "tv":
|
2018-03-23 22:48:15 +00:00
|
|
|
tv = append(tv, anime)
|
2019-06-07 01:03:16 +00:00
|
|
|
case "movie":
|
2018-03-23 22:48:15 +00:00
|
|
|
movies = append(movies, anime)
|
2019-06-07 01:03:16 +00:00
|
|
|
default:
|
2018-04-26 18:31:15 +00:00
|
|
|
continue
|
2018-03-23 22:48:15 +00:00
|
|
|
}
|
|
|
|
|
2018-02-24 12:19:11 +00:00
|
|
|
// Skip anime from my list (except planned anime)
|
|
|
|
existing := animeList.Find(anime.ID)
|
|
|
|
|
|
|
|
if existing != nil && existing.Status != arn.AnimeListStatusPlanned {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
2018-04-26 18:31:15 +00:00
|
|
|
affinity[anime.ID] = getAnimeAffinity(anime, existing, completed, bestGenres)
|
2018-02-24 12:19:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Sort
|
2018-03-23 22:48:15 +00:00
|
|
|
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 {
|
2018-03-23 22:48:15 +00:00
|
|
|
return movies[i].Title.Canonical < movies[j].Title.Canonical
|
2018-02-24 13:51:47 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return affinityA > affinityB
|
2018-02-24 12:19:11 +00:00
|
|
|
})
|
|
|
|
|
|
|
|
// Take the top 10
|
2018-03-23 22:48:15 +00:00
|
|
|
if len(tv) > maxRecommendations {
|
|
|
|
tv = tv[:maxRecommendations]
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(movies) > maxRecommendations {
|
|
|
|
movies = movies[:maxRecommendations]
|
2018-02-24 12:19:11 +00:00
|
|
|
}
|
|
|
|
|
2018-03-23 22:48:15 +00:00
|
|
|
return ctx.HTML(components.RecommendedAnime(tv, movies, viewUser, user))
|
2018-02-24 12:19:11 +00:00
|
|
|
}
|