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