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