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