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