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