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