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