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