6 "github.com/Kovensky/go-anidb/http"
7 "github.com/Kovensky/go-anidb/misc"
8 "github.com/Kovensky/go-anidb/udp"
16 gob.RegisterName("*github.com/Kovensky/go-anidb.Anime", &Anime{})
17 gob.RegisterName("github.com/Kovensky/go-anidb.AID", AID(0))
20 func (a *Anime) Touch() {
24 func (a *Anime) IsStale() bool {
29 return time.Now().Sub(a.Cached) > AnimeIncompleteCacheDuration
31 return time.Now().Sub(a.Cached) > AnimeCacheDuration
34 // Unique Anime IDentifier.
39 func (e AID) Touch() {}
40 func (e AID) IsStale() bool { return false }
42 // Returns a cached Anime. Returns nil if there is no cached Anime with this AID.
43 func (aid AID) Anime() *Anime {
45 if cache.Get(&a, "aid", aid) == nil {
51 type httpAnimeResponse struct {
56 // Retrieves an Anime from the cache if possible. If it isn't cached,
57 // or if the cache is stale, queries both the UDP and HTTP APIs
60 // Note: This can take at least 4 seconds during heavy traffic.
61 func (adb *AniDB) AnimeByID(aid AID) <-chan *Anime {
62 keys := []cacheKey{"aid", aid}
63 ch := make(chan *Anime, 1)
65 ic := make(chan Cacheable, 1)
66 go func() { ch <- (<-ic).(*Anime); close(ch) }()
67 if intentMap.Intent(ic, keys...) {
71 if !cache.CheckValid(keys...) {
72 intentMap.Notify((*Anime)(nil), keys...)
78 intentMap.Notify(anime, keys...)
83 httpChan := make(chan httpAnimeResponse, 1)
85 a, err := httpapi.GetAnime(int(aid))
86 httpChan <- httpAnimeResponse{anime: a, err: err}
88 udpChan := adb.udp.SendRecv("ANIME",
94 timeout := time.After(adb.Timeout)
97 anime = &Anime{AID: aid}
99 anime.Incomplete = true
104 for i := 0; i < 2; i++ {
108 case resp := <-httpChan:
113 if a := anime.populateFromHTTP(resp.anime); a == nil {
114 // HTTP ok but parsing not ok
115 if anime.PrimaryTitle == "" {
116 cache.MarkInvalid(keys...)
125 case reply := <-udpChan:
126 if reply.Code() == 330 {
127 cache.MarkInvalid(keys...)
129 anime.Incomplete = !anime.populateFromUDP(reply)
134 if anime.PrimaryTitle != "" {
136 cache.Set(anime, keys...)
138 intentMap.Notify(anime, keys...)
140 intentMap.Notify((*Anime)(nil), keys...)
146 func (a *Anime) populateFromHTTP(reply httpapi.Anime) *Anime {
147 if reply.Error != "" {
151 if a.AID != AID(reply.ID) {
152 panic(fmt.Sprintf("Requested AID %d different from received AID %d", a.AID, reply.ID))
156 a.Type = AnimeType(reply.Type)
157 // skip episode count since it's unreliable; UDP API handles that
159 // UDP API has more precise versions
161 if st, err := time.Parse(httpapi.DateFormat, reply.StartDate); err == nil {
164 if et, err := time.Parse(httpapi.DateFormat, reply.EndDate); err == nil {
169 for _, title := range reply.Titles {
172 a.PrimaryTitle = title.Title
174 if a.OfficialTitles == nil {
175 a.OfficialTitles = make(UniqueTitleMap)
177 a.OfficialTitles[Language(title.Lang)] = title.Title
179 if a.ShortTitles == nil {
180 a.ShortTitles = make(TitleMap)
182 a.ShortTitles[Language(title.Lang)] = append(a.ShortTitles[Language(title.Lang)], title.Title)
184 if a.Synonyms == nil {
185 a.Synonyms = make(TitleMap)
187 a.Synonyms[Language(title.Lang)] = append(a.Synonyms[Language(title.Lang)], title.Title)
191 a.OfficialURL = reply.URL
192 if reply.Picture != "" {
193 a.Picture = httpapi.AniDBImageBaseURL + reply.Picture
196 a.Description = reply.Description
199 Rating: reply.Ratings.Permanent.Rating,
200 VoteCount: reply.Ratings.Permanent.Count,
202 a.TemporaryVotes = Rating{
203 Rating: reply.Ratings.Temporary.Rating,
204 VoteCount: reply.Ratings.Temporary.Count,
207 Rating: reply.Ratings.Review.Rating,
208 VoteCount: reply.Ratings.Review.Count,
211 a.populateResources(reply.Resources)
213 counts := map[misc.EpisodeType]int{}
215 sort.Sort(reply.Episodes)
216 for _, ep := range reply.Episodes {
217 ad, _ := time.Parse(httpapi.DateFormat, ep.AirDate)
219 titles := make(UniqueTitleMap)
220 for _, title := range ep.Titles {
221 titles[Language(title.Lang)] = title.Title
228 Episode: *misc.ParseEpisode(ep.EpNo.EpNo),
230 Length: time.Duration(ep.Length) * time.Minute,
234 Rating: ep.Rating.Rating,
235 VoteCount: ep.Rating.Votes,
242 a.Episodes = append(a.Episodes, e)
245 a.EpisodeCount = EpisodeCount{
246 RegularCount: counts[misc.EpisodeTypeRegular],
247 SpecialCount: counts[misc.EpisodeTypeSpecial],
248 CreditsCount: counts[misc.EpisodeTypeCredits],
249 OtherCount: counts[misc.EpisodeTypeOther],
250 TrailerCount: counts[misc.EpisodeTypeTrailer],
251 ParodyCount: counts[misc.EpisodeTypeParody],
255 if !a.EndDate.IsZero() {
256 a.TotalEpisodes = a.EpisodeCount.RegularCount
263 func (a *Anime) populateResources(list []httpapi.Resource) {
264 a.Resources.AniDB = Resource{fmt.Sprintf("http://anidb.net/a%v", a.AID)}
266 for _, res := range list {
267 args := make([][]interface{}, len(res.ExternalEntity))
268 for i, e := range res.ExternalEntity {
269 args[i] = make([]interface{}, len(e.Identifiers))
270 for j := range args[i] {
271 args[i][j] = e.Identifiers[j]
277 for i := range res.ExternalEntity {
279 append(a.Resources.ANN, fmt.Sprintf(httpapi.ANNFormat, args[i]...))
281 case 2: // MyAnimeList
282 for i := range res.ExternalEntity {
283 a.Resources.MyAnimeList =
284 append(a.Resources.MyAnimeList, fmt.Sprintf(httpapi.MyAnimeListFormat, args[i]...))
287 for i := range res.ExternalEntity {
288 a.Resources.AnimeNfo =
289 append(a.Resources.AnimeNfo, fmt.Sprintf(httpapi.AnimeNfoFormat, args[i]...))
291 case 4: // OfficialJapanese
292 for _, e := range res.ExternalEntity {
293 for _, url := range e.URL {
294 a.Resources.OfficialJapanese = append(a.Resources.OfficialJapanese, url)
297 case 5: // OfficialEnglish
298 for _, e := range res.ExternalEntity {
299 for _, url := range e.URL {
300 a.Resources.OfficialEnglish = append(a.Resources.OfficialEnglish, url)
303 case 6: // WikipediaEnglish
304 for i := range res.ExternalEntity {
305 a.Resources.WikipediaEnglish =
306 append(a.Resources.WikipediaEnglish, fmt.Sprintf(httpapi.WikiEnglishFormat, args[i]...))
308 case 7: // WikipediaJapanese
309 for i := range res.ExternalEntity {
310 a.Resources.WikipediaJapanese =
311 append(a.Resources.WikipediaJapanese, fmt.Sprintf(httpapi.WikiJapaneseFormat, args[i]...))
313 case 8: // SyoboiSchedule
314 for i := range res.ExternalEntity {
315 a.Resources.SyoboiSchedule =
316 append(a.Resources.SyoboiSchedule, fmt.Sprintf(httpapi.SyoboiFormat, args[i]...))
319 for i := range res.ExternalEntity {
320 a.Resources.AllCinema =
321 append(a.Resources.AllCinema, fmt.Sprintf(httpapi.AllCinemaFormat, args[i]...))
324 for i := range res.ExternalEntity {
326 append(a.Resources.Anison, fmt.Sprintf(httpapi.AnisonFormat, args[i]...))
329 for i := range res.ExternalEntity {
331 append(a.Resources.VNDB, fmt.Sprintf(httpapi.VNDBFormat, args[i]...))
333 case 15: // MaruMegane
334 for i := range res.ExternalEntity {
335 a.Resources.MaruMegane =
336 append(a.Resources.MaruMegane, fmt.Sprintf(httpapi.MaruMeganeFormat, args[i]...))
342 // http://wiki.anidb.info/w/UDP_API_Definition#ANIME:_Retrieve_Anime_Data
343 // Everything that we can't easily get through the HTTP API, or that has more accuracy:
344 // episodes, air date, end date, award list, update date,
345 const animeAMask = "0000980201"
347 func (a *Anime) populateFromUDP(reply udpapi.APIReply) bool {
348 if reply != nil && reply.Error() == nil {
349 parts := strings.Split(reply.Lines()[1], "|")
351 ints := make([]int64, len(parts))
352 for i, p := range parts {
353 ints[i], _ = strconv.ParseInt(p, 10, 32)
356 a.TotalEpisodes = int(ints[0]) // episodes
357 st := time.Unix(ints[1], 0) // air date
358 et := time.Unix(ints[2], 0) // end date
359 aw := strings.Split(parts[3], "'") // award list
360 ut := time.Unix(ints[4], 0) // update date
362 if len(parts[3]) > 0 {
366 // 0 does not actually mean the Epoch here...