package anidb
import (
- "encoding/gob"
"fmt"
"github.com/Kovensky/go-anidb/http"
"github.com/Kovensky/go-anidb/misc"
"github.com/Kovensky/go-anidb/udp"
+ "github.com/Kovensky/go-fscache"
+ "log"
"sort"
"strconv"
"strings"
"time"
)
-func init() {
- gob.RegisterName("*github.com/Kovensky/go-anidb.Anime", &Anime{})
-}
+var _ cacheable = &Anime{}
-func (a *Anime) Touch() {
- a.Cached = time.Now()
+func (a *Anime) setCachedTS(ts time.Time) {
+ a.Cached = ts
}
func (a *Anime) IsStale() bool {
if a == nil {
return true
}
- return time.Now().Sub(a.Cached) > AnimeCacheDuration
+ now := time.Now()
+ diff := now.Sub(a.Cached)
+ if a.Incomplete {
+ return diff > AnimeIncompleteCacheDuration
+ }
+
+ // If the anime ended, and more than AnimeCacheDuration time ago at that
+ if !a.EndDate.IsZero() && now.After(a.EndDate.Add(AnimeCacheDuration)) {
+ return diff > FinishedAnimeCacheDuration
+ }
+ return diff > AnimeCacheDuration
}
// Unique Anime IDentifier.
// Returns a cached Anime. Returns nil if there is no cached Anime with this AID.
func (aid AID) Anime() *Anime {
- a, _ := caches.Get(animeCache).Get(int(aid)).(*Anime)
- return a
+ var a Anime
+ if CacheGet(&a, "aid", aid) == nil {
+ return &a
+ }
+ return nil
}
type httpAnimeResponse struct {
err error
}
-// Retrieves an Anime from the cache if possible. If it isn't cached,
-// or if the cache is stale, queries both the UDP and HTTP APIs
-// for data.
-//
-// Note: This can take at least 4 seconds during heavy traffic.
+// Retrieves an Anime by its AID. Uses both the HTTP and UDP APIs,
+// but can work without the UDP API.
func (adb *AniDB) AnimeByID(aid AID) <-chan *Anime {
+ key := []fscache.CacheKey{"aid", aid}
ch := make(chan *Anime, 1)
- anime := aid.Anime()
- if !anime.IsStale() {
- ch <- anime
+ if aid < 1 {
+ ch <- nil
close(ch)
- return ch
}
- ac := caches.Get(animeCache)
- ic := make(chan Cacheable, 1)
+ ic := make(chan notification, 1)
go func() { ch <- (<-ic).(*Anime); close(ch) }()
- if ac.Intent(int(aid), ic) {
+ if intentMap.Intent(ic, key...) {
+ return ch
+ }
+
+ if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
+ intentMap.NotifyClose((*Anime)(nil), key...)
+ return ch
+ }
+
+ anime := aid.Anime()
+ if !anime.IsStale() {
+ intentMap.NotifyClose(anime, key...)
return ch
}
go func() {
httpChan := make(chan httpAnimeResponse, 1)
go func() {
+ log.Printf("HTTP>>> Anime %d", aid)
a, err := httpapi.GetAnime(int(aid))
httpChan <- httpAnimeResponse{anime: a, err: err}
}()
for i := 0; i < 2; i++ {
select {
case <-timeout:
- ok = false
+ // HTTP API timeout
+ if httpChan != nil {
+ log.Printf("HTTP<<< Timeout")
+ close(httpChan)
+ }
case resp := <-httpChan:
if resp.err != nil {
+ log.Printf("HTTP<<< %v", resp.err)
ok = false
break Loop
}
- if a := anime.populateFromHTTP(resp.anime); a == nil {
+
+ if resp.anime.Error != "" {
+ log.Printf("HTTP<<< Error %q", resp.anime.Error)
+ }
+
+ if anime.populateFromHTTP(resp.anime) {
+ log.Printf("HTTP<<< Anime %q", anime.PrimaryTitle)
+ } else {
+ // HTTP ok but parsing not ok
+ if anime.PrimaryTitle == "" {
+ Cache.SetInvalid(key...)
+ }
+
+ switch resp.anime.Error {
+ case "Anime not found", "aid Missing or Invalid":
+ // deleted AID?
+ Cache.Delete(key...)
+ }
+
ok = false
break Loop
- } else {
- anime = a
}
+
httpChan = nil
case reply := <-udpChan:
- anime.Incomplete = !anime.populateFromUDP(reply)
+ if reply.Code() == 330 {
+ Cache.SetInvalid(key...)
+ // deleted AID?
+ Cache.Delete(key...)
+
+ ok = false
+ break Loop
+ } else {
+ anime.Incomplete = !anime.populateFromUDP(reply)
+ }
udpChan = nil
}
}
if anime.PrimaryTitle != "" {
if ok {
- ac.Set(int(aid), anime)
- } else {
- ac.Flush(int(aid), anime)
+ CacheSet(anime, key...)
}
+ intentMap.NotifyClose(anime, key...)
} else {
- ac.Set(int(aid), (*Anime)(nil))
+ intentMap.NotifyClose((*Anime)(nil), key...)
}
}()
return ch
}
-func (a *Anime) populateFromHTTP(reply httpapi.Anime) *Anime {
+func (a *Anime) populateFromHTTP(reply httpapi.Anime) bool {
if reply.Error != "" {
- return (*Anime)(nil)
+ return false
}
if a.AID != AID(reply.ID) {
for _, title := range reply.Titles {
switch title.Type {
case "main":
- if a.PrimaryTitle != "" {
- // We assume there's only ever one "main" title
- panic(
- fmt.Sprintf("PrimaryTitle %q already set, new PrimaryTitle %q received!",
- a.PrimaryTitle, title.Title))
- }
a.PrimaryTitle = title.Title
case "official":
if a.OfficialTitles == nil {
titles[Language(title.Lang)] = title.Title
}
- e := Episode{
+ e := &Episode{
EID: EID(ep.ID),
AID: a.AID,
Titles: titles,
}
counts[e.Type]++
- cacheEpisode(&e)
+ cacheEpisode(e)
a.Episodes = append(a.Episodes, e)
}
- a.EpisodeCount = EpisodeCount{
+ a.EpisodeCount = misc.EpisodeCount{
RegularCount: counts[misc.EpisodeTypeRegular],
SpecialCount: counts[misc.EpisodeTypeSpecial],
CreditsCount: counts[misc.EpisodeTypeCredits],
}
}
- return a
+ return true
}
func (a *Anime) populateResources(list []httpapi.Resource) {