5 "github.com/Kovensky/go-anidb/http"
6 "github.com/Kovensky/go-anidb/misc"
7 "github.com/Kovensky/go-anidb/udp"
8 "github.com/Kovensky/go-fscache"
15 var _ cacheable = &Anime{}
17 func (a *Anime) setCachedTS(ts time.Time) {
21 func (a *Anime) IsStale() bool {
26 diff := now.Sub(a.Cached)
28 return diff > AnimeIncompleteCacheDuration
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
35 return diff > AnimeCacheDuration
38 // Unique Anime IDentifier.
41 // Returns a cached Anime. Returns nil if there is no cached Anime with this AID.
42 func (aid AID) Anime() *Anime {
44 if CacheGet(&a, "aid", aid) == nil {
50 type httpAnimeResponse struct {
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)
66 ic := make(chan notification, 1)
67 go func() { ch <- (<-ic).(*Anime); close(ch) }()
68 if intentMap.Intent(ic, key...) {
72 if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
73 intentMap.NotifyClose((*Anime)(nil), key...)
79 intentMap.NotifyClose(anime, key...)
84 httpChan := make(chan httpAnimeResponse, 1)
86 adb.Logger.Printf("HTTP>>> Anime %d", aid)
87 a, err := httpapi.GetAnime(int(aid))
88 httpChan <- httpAnimeResponse{anime: a, err: err}
90 udpChan := adb.udp.SendRecv("ANIME",
96 timeout := time.After(adb.Timeout)
99 anime = &Anime{AID: aid}
101 anime.Incomplete = true
106 for i := 0; i < 2; i++ {
111 adb.Logger.Printf("HTTP<<< Timeout")
114 case resp := <-httpChan:
116 adb.Logger.Printf("HTTP<<< %v", resp.err)
121 if resp.anime.Error != "" {
122 adb.Logger.Printf("HTTP<<< Error %q", resp.anime.Error)
125 if anime.populateFromHTTP(resp.anime) {
126 adb.Logger.Printf("HTTP<<< Anime %q", anime.PrimaryTitle)
128 // HTTP ok but parsing not ok
129 if anime.PrimaryTitle == "" {
130 Cache.SetInvalid(key...)
133 switch resp.anime.Error {
134 case "Anime not found", "aid Missing or Invalid":
144 case reply := <-udpChan:
145 if reply.Code() == 330 {
146 Cache.SetInvalid(key...)
153 anime.Incomplete = !anime.populateFromUDP(reply)
158 if anime.PrimaryTitle != "" {
160 CacheSet(anime, key...)
162 intentMap.NotifyClose(anime, key...)
164 intentMap.NotifyClose((*Anime)(nil), key...)
170 func (a *Anime) populateFromHTTP(reply httpapi.Anime) bool {
171 if reply.Error != "" {
175 if a.AID != AID(reply.ID) {
176 panic(fmt.Sprintf("Requested AID %d different from received AID %d", a.AID, reply.ID))
180 a.Type = AnimeType(reply.Type)
181 // skip episode count since it's unreliable; UDP API handles that
183 // UDP API has more precise versions
185 if st, err := time.Parse(httpapi.DateFormat, reply.StartDate); err == nil {
188 if et, err := time.Parse(httpapi.DateFormat, reply.EndDate); err == nil {
193 for _, title := range reply.Titles {
196 a.PrimaryTitle = title.Title
198 if a.OfficialTitles == nil {
199 a.OfficialTitles = make(UniqueTitleMap)
201 a.OfficialTitles[Language(title.Lang)] = title.Title
203 if a.ShortTitles == nil {
204 a.ShortTitles = make(TitleMap)
206 a.ShortTitles[Language(title.Lang)] = append(a.ShortTitles[Language(title.Lang)], title.Title)
208 if a.Synonyms == nil {
209 a.Synonyms = make(TitleMap)
211 a.Synonyms[Language(title.Lang)] = append(a.Synonyms[Language(title.Lang)], title.Title)
215 a.OfficialURL = reply.URL
216 if reply.Picture != "" {
217 a.Picture = httpapi.AniDBImageBaseURL + reply.Picture
220 a.Description = reply.Description
223 Rating: reply.Ratings.Permanent.Rating,
224 VoteCount: reply.Ratings.Permanent.Count,
226 a.TemporaryVotes = Rating{
227 Rating: reply.Ratings.Temporary.Rating,
228 VoteCount: reply.Ratings.Temporary.Count,
231 Rating: reply.Ratings.Review.Rating,
232 VoteCount: reply.Ratings.Review.Count,
235 a.populateResources(reply.Resources)
237 counts := map[misc.EpisodeType]int{}
239 sort.Sort(reply.Episodes)
240 for _, ep := range reply.Episodes {
241 ad, _ := time.Parse(httpapi.DateFormat, ep.AirDate)
243 titles := make(UniqueTitleMap)
244 for _, title := range ep.Titles {
245 titles[Language(title.Lang)] = title.Title
252 Episode: *misc.ParseEpisode(ep.EpNo.EpNo),
254 Length: time.Duration(ep.Length) * time.Minute,
258 Rating: ep.Rating.Rating,
259 VoteCount: ep.Rating.Votes,
266 a.Episodes = append(a.Episodes, e)
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],
279 if !a.EndDate.IsZero() {
280 a.TotalEpisodes = a.EpisodeCount.RegularCount
287 func (a *Anime) populateResources(list []httpapi.Resource) {
288 a.Resources.AniDB = Resource{fmt.Sprintf("http://anidb.net/a%v", a.AID)}
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]
301 for i := range res.ExternalEntity {
303 append(a.Resources.ANN, fmt.Sprintf(httpapi.ANNFormat, args[i]...))
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]...))
311 for i := range res.ExternalEntity {
312 a.Resources.AnimeNfo =
313 append(a.Resources.AnimeNfo, fmt.Sprintf(httpapi.AnimeNfoFormat, args[i]...))
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)
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)
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]...))
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]...))
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]...))
343 for i := range res.ExternalEntity {
344 a.Resources.AllCinema =
345 append(a.Resources.AllCinema, fmt.Sprintf(httpapi.AllCinemaFormat, args[i]...))
348 for i := range res.ExternalEntity {
350 append(a.Resources.Anison, fmt.Sprintf(httpapi.AnisonFormat, args[i]...))
353 for i := range res.ExternalEntity {
355 append(a.Resources.VNDB, fmt.Sprintf(httpapi.VNDBFormat, args[i]...))
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]...))
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"
371 func (a *Anime) populateFromUDP(reply udpapi.APIReply) bool {
372 if reply != nil && reply.Error() == nil {
373 parts := strings.Split(reply.Lines()[1], "|")
375 ints := make([]int64, len(parts))
376 for i, p := range parts {
377 ints[i], _ = strconv.ParseInt(p, 10, 32)
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
386 if len(parts[3]) > 0 {
390 // 0 does not actually mean the Epoch here...