6 "github.com/Kovensky/go-anidb/http"
7 "github.com/Kovensky/go-anidb/misc"
8 "github.com/Kovensky/go-anidb/udp"
17 gob.RegisterName("*github.com/Kovensky/go-anidb.Anime", &Anime{})
18 gob.RegisterName("github.com/Kovensky/go-anidb.AID", AID(0))
21 func (a *Anime) Touch() {
25 func (a *Anime) IsStale() bool {
30 diff := now.Sub(a.Cached)
32 return diff > AnimeIncompleteCacheDuration
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
39 return diff > AnimeCacheDuration
42 // Unique Anime IDentifier.
47 func (e AID) Touch() {}
48 func (e AID) IsStale() bool { return false }
50 // Returns a cached Anime. Returns nil if there is no cached Anime with this AID.
51 func (aid AID) Anime() *Anime {
53 if cache.Get(&a, "aid", aid) == nil {
59 type httpAnimeResponse struct {
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)
75 ic := make(chan Cacheable, 1)
76 go func() { ch <- (<-ic).(*Anime); close(ch) }()
77 if intentMap.Intent(ic, keys...) {
81 if !cache.CheckValid(keys...) {
82 intentMap.NotifyClose((*Anime)(nil), keys...)
88 intentMap.NotifyClose(anime, keys...)
93 httpChan := make(chan httpAnimeResponse, 1)
95 log.Printf("HTTP>>> Anime %d", aid)
96 a, err := httpapi.GetAnime(int(aid))
97 httpChan <- httpAnimeResponse{anime: a, err: err}
99 udpChan := adb.udp.SendRecv("ANIME",
105 timeout := time.After(adb.Timeout)
108 anime = &Anime{AID: aid}
110 anime.Incomplete = true
115 for i := 0; i < 2; i++ {
120 log.Printf("HTTP<<< Timeout")
123 case resp := <-httpChan:
125 log.Printf("HTTP<<< %v", resp.err)
130 if resp.anime.Error != "" {
131 log.Printf("HTTP<<< Error %q", resp.anime.Error)
134 if anime.populateFromHTTP(resp.anime) {
135 log.Printf("HTTP<<< Anime %q", anime.PrimaryTitle)
137 // HTTP ok but parsing not ok
138 if anime.PrimaryTitle == "" {
139 cache.MarkInvalid(keys...)
142 switch resp.anime.Error {
143 case "Anime not found", "aid Missing or Invalid":
145 cache.Delete(keys...)
153 case reply := <-udpChan:
154 if reply.Code() == 330 {
155 cache.MarkInvalid(keys...)
157 cache.Delete(keys...)
162 anime.Incomplete = !anime.populateFromUDP(reply)
167 if anime.PrimaryTitle != "" {
169 cache.Set(anime, keys...)
171 intentMap.NotifyClose(anime, keys...)
173 intentMap.NotifyClose((*Anime)(nil), keys...)
179 func (a *Anime) populateFromHTTP(reply httpapi.Anime) bool {
180 if reply.Error != "" {
184 if a.AID != AID(reply.ID) {
185 panic(fmt.Sprintf("Requested AID %d different from received AID %d", a.AID, reply.ID))
189 a.Type = AnimeType(reply.Type)
190 // skip episode count since it's unreliable; UDP API handles that
192 // UDP API has more precise versions
194 if st, err := time.Parse(httpapi.DateFormat, reply.StartDate); err == nil {
197 if et, err := time.Parse(httpapi.DateFormat, reply.EndDate); err == nil {
202 for _, title := range reply.Titles {
205 a.PrimaryTitle = title.Title
207 if a.OfficialTitles == nil {
208 a.OfficialTitles = make(UniqueTitleMap)
210 a.OfficialTitles[Language(title.Lang)] = title.Title
212 if a.ShortTitles == nil {
213 a.ShortTitles = make(TitleMap)
215 a.ShortTitles[Language(title.Lang)] = append(a.ShortTitles[Language(title.Lang)], title.Title)
217 if a.Synonyms == nil {
218 a.Synonyms = make(TitleMap)
220 a.Synonyms[Language(title.Lang)] = append(a.Synonyms[Language(title.Lang)], title.Title)
224 a.OfficialURL = reply.URL
225 if reply.Picture != "" {
226 a.Picture = httpapi.AniDBImageBaseURL + reply.Picture
229 a.Description = reply.Description
232 Rating: reply.Ratings.Permanent.Rating,
233 VoteCount: reply.Ratings.Permanent.Count,
235 a.TemporaryVotes = Rating{
236 Rating: reply.Ratings.Temporary.Rating,
237 VoteCount: reply.Ratings.Temporary.Count,
240 Rating: reply.Ratings.Review.Rating,
241 VoteCount: reply.Ratings.Review.Count,
244 a.populateResources(reply.Resources)
246 counts := map[misc.EpisodeType]int{}
248 sort.Sort(reply.Episodes)
249 for _, ep := range reply.Episodes {
250 ad, _ := time.Parse(httpapi.DateFormat, ep.AirDate)
252 titles := make(UniqueTitleMap)
253 for _, title := range ep.Titles {
254 titles[Language(title.Lang)] = title.Title
261 Episode: *misc.ParseEpisode(ep.EpNo.EpNo),
263 Length: time.Duration(ep.Length) * time.Minute,
267 Rating: ep.Rating.Rating,
268 VoteCount: ep.Rating.Votes,
275 a.Episodes = append(a.Episodes, e)
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],
288 if !a.EndDate.IsZero() {
289 a.TotalEpisodes = a.EpisodeCount.RegularCount
296 func (a *Anime) populateResources(list []httpapi.Resource) {
297 a.Resources.AniDB = Resource{fmt.Sprintf("http://anidb.net/a%v", a.AID)}
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]
310 for i := range res.ExternalEntity {
312 append(a.Resources.ANN, fmt.Sprintf(httpapi.ANNFormat, args[i]...))
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]...))
320 for i := range res.ExternalEntity {
321 a.Resources.AnimeNfo =
322 append(a.Resources.AnimeNfo, fmt.Sprintf(httpapi.AnimeNfoFormat, args[i]...))
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)
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)
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]...))
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]...))
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]...))
352 for i := range res.ExternalEntity {
353 a.Resources.AllCinema =
354 append(a.Resources.AllCinema, fmt.Sprintf(httpapi.AllCinemaFormat, args[i]...))
357 for i := range res.ExternalEntity {
359 append(a.Resources.Anison, fmt.Sprintf(httpapi.AnisonFormat, args[i]...))
362 for i := range res.ExternalEntity {
364 append(a.Resources.VNDB, fmt.Sprintf(httpapi.VNDBFormat, args[i]...))
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]...))
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"
380 func (a *Anime) populateFromUDP(reply udpapi.APIReply) bool {
381 if reply != nil && reply.Error() == nil {
382 parts := strings.Split(reply.Lines()[1], "|")
384 ints := make([]int64, len(parts))
385 for i, p := range parts {
386 ints[i], _ = strconv.ParseInt(p, 10, 32)
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
395 if len(parts[3]) > 0 {
399 // 0 does not actually mean the Epoch here...