]> git.lizzy.rs Git - go-anidb.git/blob - animecache.go
anidb: Wait longer after each UDP API timeout
[go-anidb.git] / animecache.go
1 package anidb
2
3 import (
4         "encoding/gob"
5         "fmt"
6         "github.com/Kovensky/go-anidb/http"
7         "github.com/Kovensky/go-anidb/misc"
8         "github.com/Kovensky/go-anidb/udp"
9         "sort"
10         "strconv"
11         "strings"
12         "time"
13 )
14
15 func init() {
16         gob.RegisterName("*github.com/Kovensky/go-anidb.Anime", &Anime{})
17         gob.RegisterName("github.com/Kovensky/go-anidb.AID", AID(0))
18 }
19
20 func (a *Anime) Touch() {
21         a.Cached = time.Now()
22 }
23
24 func (a *Anime) IsStale() bool {
25         if a == nil {
26                 return true
27         }
28         if a.Incomplete {
29                 return time.Now().Sub(a.Cached) > AnimeIncompleteCacheDuration
30         }
31         return time.Now().Sub(a.Cached) > AnimeCacheDuration
32 }
33
34 // Unique Anime IDentifier.
35 type AID int
36
37 // make AID Cacheable
38
39 func (e AID) Touch()        {}
40 func (e AID) IsStale() bool { return false }
41
42 // Returns a cached Anime. Returns nil if there is no cached Anime with this AID.
43 func (aid AID) Anime() *Anime {
44         var a Anime
45         if cache.Get(&a, "aid", aid) == nil {
46                 return &a
47         }
48         return nil
49 }
50
51 type httpAnimeResponse struct {
52         anime httpapi.Anime
53         err   error
54 }
55
56 // Retrieves an Anime from the cache if possible. If it isn't cached,
57 // or if the cache is stale, queries both the UDP and HTTP APIs
58 // for data.
59 //
60 // Note: This can take at least 4 seconds during heavy traffic.
61 func (adb *AniDB) AnimeByID(aid AID) <-chan *Anime {
62         keys := []cacheKey{"aid", aid}
63         ch := make(chan *Anime, 1)
64
65         ic := make(chan Cacheable, 1)
66         go func() { ch <- (<-ic).(*Anime); close(ch) }()
67         if intentMap.Intent(ic, keys...) {
68                 return ch
69         }
70
71         if !cache.CheckValid(keys...) {
72                 intentMap.Notify((*Anime)(nil), keys...)
73                 return ch
74         }
75
76         anime := aid.Anime()
77         if !anime.IsStale() {
78                 intentMap.Notify(anime, keys...)
79                 return ch
80         }
81
82         go func() {
83                 httpChan := make(chan httpAnimeResponse, 1)
84                 go func() {
85                         a, err := httpapi.GetAnime(int(aid))
86                         httpChan <- httpAnimeResponse{anime: a, err: err}
87                 }()
88                 udpChan := adb.udp.SendRecv("ANIME",
89                         paramMap{
90                                 "aid":   aid,
91                                 "amask": animeAMask,
92                         })
93
94                 timeout := time.After(adb.Timeout)
95
96                 if anime == nil {
97                         anime = &Anime{AID: aid}
98                 }
99                 anime.Incomplete = true
100
101                 ok := true
102
103         Loop:
104                 for i := 0; i < 2; i++ {
105                         select {
106                         case <-timeout:
107                                 ok = false
108                         case resp := <-httpChan:
109                                 if resp.err != nil {
110                                         ok = false
111                                         break Loop
112                                 }
113                                 if a := anime.populateFromHTTP(resp.anime); a == nil {
114                                         // HTTP ok but parsing not ok
115                                         if anime.PrimaryTitle == "" {
116                                                 cache.MarkInvalid(keys...)
117                                         }
118
119                                         ok = false
120                                         break Loop
121                                 } else {
122                                         anime = a
123                                 }
124                                 httpChan = nil
125                         case reply := <-udpChan:
126                                 if reply.Code() == 330 {
127                                         cache.MarkInvalid(keys...)
128                                 } else {
129                                         anime.Incomplete = !anime.populateFromUDP(reply)
130                                 }
131                                 udpChan = nil
132                         }
133                 }
134                 if anime.PrimaryTitle != "" {
135                         if ok {
136                                 cache.Set(anime, keys...)
137                         }
138                         intentMap.Notify(anime, keys...)
139                 } else {
140                         intentMap.Notify((*Anime)(nil), keys...)
141                 }
142         }()
143         return ch
144 }
145
146 func (a *Anime) populateFromHTTP(reply httpapi.Anime) *Anime {
147         if reply.Error != "" {
148                 return (*Anime)(nil)
149         }
150
151         if a.AID != AID(reply.ID) {
152                 panic(fmt.Sprintf("Requested AID %d different from received AID %d", a.AID, reply.ID))
153         }
154         a.R18 = reply.R18
155
156         a.Type = AnimeType(reply.Type)
157         // skip episode count since it's unreliable; UDP API handles that
158
159         // UDP API has more precise versions
160         if a.Incomplete {
161                 if st, err := time.Parse(httpapi.DateFormat, reply.StartDate); err == nil {
162                         a.StartDate = st
163                 }
164                 if et, err := time.Parse(httpapi.DateFormat, reply.EndDate); err == nil {
165                         a.EndDate = et
166                 }
167         }
168
169         for _, title := range reply.Titles {
170                 switch title.Type {
171                 case "main":
172                         a.PrimaryTitle = title.Title
173                 case "official":
174                         if a.OfficialTitles == nil {
175                                 a.OfficialTitles = make(UniqueTitleMap)
176                         }
177                         a.OfficialTitles[Language(title.Lang)] = title.Title
178                 case "short":
179                         if a.ShortTitles == nil {
180                                 a.ShortTitles = make(TitleMap)
181                         }
182                         a.ShortTitles[Language(title.Lang)] = append(a.ShortTitles[Language(title.Lang)], title.Title)
183                 case "synonym":
184                         if a.Synonyms == nil {
185                                 a.Synonyms = make(TitleMap)
186                         }
187                         a.Synonyms[Language(title.Lang)] = append(a.Synonyms[Language(title.Lang)], title.Title)
188                 }
189         }
190
191         a.OfficialURL = reply.URL
192         if reply.Picture != "" {
193                 a.Picture = httpapi.AniDBImageBaseURL + reply.Picture
194         }
195
196         a.Description = reply.Description
197
198         a.Votes = Rating{
199                 Rating:    reply.Ratings.Permanent.Rating,
200                 VoteCount: reply.Ratings.Permanent.Count,
201         }
202         a.TemporaryVotes = Rating{
203                 Rating:    reply.Ratings.Temporary.Rating,
204                 VoteCount: reply.Ratings.Temporary.Count,
205         }
206         a.Reviews = Rating{
207                 Rating:    reply.Ratings.Review.Rating,
208                 VoteCount: reply.Ratings.Review.Count,
209         }
210
211         a.populateResources(reply.Resources)
212
213         counts := map[misc.EpisodeType]int{}
214
215         sort.Sort(reply.Episodes)
216         for _, ep := range reply.Episodes {
217                 ad, _ := time.Parse(httpapi.DateFormat, ep.AirDate)
218
219                 titles := make(UniqueTitleMap)
220                 for _, title := range ep.Titles {
221                         titles[Language(title.Lang)] = title.Title
222                 }
223
224                 e := Episode{
225                         EID: EID(ep.ID),
226                         AID: a.AID,
227
228                         Episode: *misc.ParseEpisode(ep.EpNo.EpNo),
229
230                         Length:  time.Duration(ep.Length) * time.Minute,
231                         AirDate: &ad,
232
233                         Rating: Rating{
234                                 Rating:    ep.Rating.Rating,
235                                 VoteCount: ep.Rating.Votes,
236                         },
237                         Titles: titles,
238                 }
239                 counts[e.Type]++
240                 cacheEpisode(&e)
241
242                 a.Episodes = append(a.Episodes, e)
243         }
244
245         a.EpisodeCount = EpisodeCount{
246                 RegularCount: counts[misc.EpisodeTypeRegular],
247                 SpecialCount: counts[misc.EpisodeTypeSpecial],
248                 CreditsCount: counts[misc.EpisodeTypeCredits],
249                 OtherCount:   counts[misc.EpisodeTypeOther],
250                 TrailerCount: counts[misc.EpisodeTypeTrailer],
251                 ParodyCount:  counts[misc.EpisodeTypeParody],
252         }
253
254         if a.Incomplete {
255                 if !a.EndDate.IsZero() {
256                         a.TotalEpisodes = a.EpisodeCount.RegularCount
257                 }
258         }
259
260         return a
261 }
262
263 func (a *Anime) populateResources(list []httpapi.Resource) {
264         a.Resources.AniDB = Resource{fmt.Sprintf("http://anidb.net/a%v", a.AID)}
265
266         for _, res := range list {
267                 args := make([][]interface{}, len(res.ExternalEntity))
268                 for i, e := range res.ExternalEntity {
269                         args[i] = make([]interface{}, len(e.Identifiers))
270                         for j := range args[i] {
271                                 args[i][j] = e.Identifiers[j]
272                         }
273                 }
274
275                 switch res.Type {
276                 case 1: // ANN
277                         for i := range res.ExternalEntity {
278                                 a.Resources.ANN =
279                                         append(a.Resources.ANN, fmt.Sprintf(httpapi.ANNFormat, args[i]...))
280                         }
281                 case 2: // MyAnimeList
282                         for i := range res.ExternalEntity {
283                                 a.Resources.MyAnimeList =
284                                         append(a.Resources.MyAnimeList, fmt.Sprintf(httpapi.MyAnimeListFormat, args[i]...))
285                         }
286                 case 3: // AnimeNfo
287                         for i := range res.ExternalEntity {
288                                 a.Resources.AnimeNfo =
289                                         append(a.Resources.AnimeNfo, fmt.Sprintf(httpapi.AnimeNfoFormat, args[i]...))
290                         }
291                 case 4: // OfficialJapanese
292                         for _, e := range res.ExternalEntity {
293                                 for _, url := range e.URL {
294                                         a.Resources.OfficialJapanese = append(a.Resources.OfficialJapanese, url)
295                                 }
296                         }
297                 case 5: // OfficialEnglish
298                         for _, e := range res.ExternalEntity {
299                                 for _, url := range e.URL {
300                                         a.Resources.OfficialEnglish = append(a.Resources.OfficialEnglish, url)
301                                 }
302                         }
303                 case 6: // WikipediaEnglish
304                         for i := range res.ExternalEntity {
305                                 a.Resources.WikipediaEnglish =
306                                         append(a.Resources.WikipediaEnglish, fmt.Sprintf(httpapi.WikiEnglishFormat, args[i]...))
307                         }
308                 case 7: // WikipediaJapanese
309                         for i := range res.ExternalEntity {
310                                 a.Resources.WikipediaJapanese =
311                                         append(a.Resources.WikipediaJapanese, fmt.Sprintf(httpapi.WikiJapaneseFormat, args[i]...))
312                         }
313                 case 8: // SyoboiSchedule
314                         for i := range res.ExternalEntity {
315                                 a.Resources.SyoboiSchedule =
316                                         append(a.Resources.SyoboiSchedule, fmt.Sprintf(httpapi.SyoboiFormat, args[i]...))
317                         }
318                 case 9: // AllCinema
319                         for i := range res.ExternalEntity {
320                                 a.Resources.AllCinema =
321                                         append(a.Resources.AllCinema, fmt.Sprintf(httpapi.AllCinemaFormat, args[i]...))
322                         }
323                 case 10: // Anison
324                         for i := range res.ExternalEntity {
325                                 a.Resources.Anison =
326                                         append(a.Resources.Anison, fmt.Sprintf(httpapi.AnisonFormat, args[i]...))
327                         }
328                 case 14: // VNDB
329                         for i := range res.ExternalEntity {
330                                 a.Resources.VNDB =
331                                         append(a.Resources.VNDB, fmt.Sprintf(httpapi.VNDBFormat, args[i]...))
332                         }
333                 case 15: // MaruMegane
334                         for i := range res.ExternalEntity {
335                                 a.Resources.MaruMegane =
336                                         append(a.Resources.MaruMegane, fmt.Sprintf(httpapi.MaruMeganeFormat, args[i]...))
337                         }
338                 }
339         }
340 }
341
342 // http://wiki.anidb.info/w/UDP_API_Definition#ANIME:_Retrieve_Anime_Data
343 // Everything that we can't easily get through the HTTP API, or that has more accuracy:
344 // episodes, air date, end date, award list, update date,
345 const animeAMask = "0000980201"
346
347 func (a *Anime) populateFromUDP(reply udpapi.APIReply) bool {
348         if reply != nil && reply.Error() == nil {
349                 parts := strings.Split(reply.Lines()[1], "|")
350
351                 ints := make([]int64, len(parts))
352                 for i, p := range parts {
353                         ints[i], _ = strconv.ParseInt(p, 10, 32)
354                 }
355
356                 a.TotalEpisodes = int(ints[0])     // episodes
357                 st := time.Unix(ints[1], 0)        // air date
358                 et := time.Unix(ints[2], 0)        // end date
359                 aw := strings.Split(parts[3], "'") // award list
360                 ut := time.Unix(ints[4], 0)        // update date
361
362                 if len(parts[3]) > 0 {
363                         a.Awards = aw
364                 }
365
366                 // 0 does not actually mean the Epoch here...
367                 if ints[1] != 0 {
368                         a.StartDate = st
369                 }
370                 if ints[2] != 0 {
371                         a.EndDate = et
372                 }
373                 if ints[4] != 0 {
374                         a.Updated = ut
375                 }
376                 return true
377         }
378         return false
379 }