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