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{})
19 func (a *Anime) Touch() {
23 func (a *Anime) IsStale() bool {
27 return time.Now().Sub(a.Cached) > AnimeCacheDuration
30 // Unique Anime IDentifier.
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)
39 type httpAnimeResponse struct {
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
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)
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) {
67 httpChan := make(chan httpAnimeResponse, 1)
69 a, err := httpapi.GetAnime(int(aid))
70 httpChan <- httpAnimeResponse{anime: a, err: err}
72 udpChan := adb.udp.SendRecv("ANIME",
78 timeout := time.After(adb.Timeout)
81 anime = &Anime{AID: aid}
83 anime.Incomplete = true
88 for i := 0; i < 2; i++ {
92 case resp := <-httpChan:
97 if a := anime.populateFromHTTP(resp.anime); a == nil {
104 case reply := <-udpChan:
105 anime.Incomplete = !anime.populateFromUDP(reply)
109 if anime.PrimaryTitle != "" {
111 ac.Set(int(aid), anime)
113 ac.Flush(int(aid), anime)
116 ac.Set(int(aid), (*Anime)(nil))
122 func (a *Anime) populateFromHTTP(reply httpapi.Anime) *Anime {
123 if reply.Error != "" {
127 if a.AID != AID(reply.ID) {
128 panic(fmt.Sprintf("Requested AID %d different from received AID %d", a.AID, reply.ID))
132 a.Type = AnimeType(reply.Type)
133 // skip episode count since it's unreliable; UDP API handles that
135 // UDP API has more precise versions
137 if st, err := time.Parse(httpapi.DateFormat, reply.StartDate); err == nil {
140 if et, err := time.Parse(httpapi.DateFormat, reply.EndDate); err == nil {
145 for _, title := range reply.Titles {
148 if a.PrimaryTitle != "" {
149 // We assume there's only ever one "main" title
151 fmt.Sprintf("PrimaryTitle %q already set, new PrimaryTitle %q received!",
152 a.PrimaryTitle, title.Title))
154 a.PrimaryTitle = title.Title
156 if a.OfficialTitles == nil {
157 a.OfficialTitles = make(UniqueTitleMap)
159 a.OfficialTitles[Language(title.Lang)] = title.Title
161 if a.ShortTitles == nil {
162 a.ShortTitles = make(TitleMap)
164 a.ShortTitles[Language(title.Lang)] = append(a.ShortTitles[Language(title.Lang)], title.Title)
166 if a.Synonyms == nil {
167 a.Synonyms = make(TitleMap)
169 a.Synonyms[Language(title.Lang)] = append(a.Synonyms[Language(title.Lang)], title.Title)
173 a.OfficialURL = reply.URL
174 if reply.Picture != "" {
175 a.Picture = httpapi.AniDBImageBaseURL + reply.Picture
178 a.Description = reply.Description
181 Rating: reply.Ratings.Permanent.Rating,
182 VoteCount: reply.Ratings.Permanent.Count,
184 a.TemporaryVotes = Rating{
185 Rating: reply.Ratings.Temporary.Rating,
186 VoteCount: reply.Ratings.Temporary.Count,
189 Rating: reply.Ratings.Review.Rating,
190 VoteCount: reply.Ratings.Review.Count,
193 a.populateResources(reply.Resources)
195 counts := map[misc.EpisodeType]int{}
197 sort.Sort(reply.Episodes)
198 for _, ep := range reply.Episodes {
199 ad, _ := time.Parse(httpapi.DateFormat, ep.AirDate)
201 titles := make(UniqueTitleMap)
202 for _, title := range ep.Titles {
203 titles[Language(title.Lang)] = title.Title
210 Episode: *misc.ParseEpisode(ep.EpNo.EpNo),
212 Length: time.Duration(ep.Length) * time.Minute,
216 Rating: ep.Rating.Rating,
217 VoteCount: ep.Rating.Votes,
224 a.Episodes = append(a.Episodes, e)
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],
237 if !a.EndDate.IsZero() {
238 a.TotalEpisodes = a.EpisodeCount.RegularCount
245 func (a *Anime) populateResources(list []httpapi.Resource) {
246 a.Resources.AniDB = Resource{fmt.Sprintf("http://anidb.net/a%v", a.AID)}
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]
259 for i := range res.ExternalEntity {
261 append(a.Resources.ANN, fmt.Sprintf(httpapi.ANNFormat, args[i]...))
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]...))
269 for i := range res.ExternalEntity {
270 a.Resources.AnimeNfo =
271 append(a.Resources.AnimeNfo, fmt.Sprintf(httpapi.AnimeNfoFormat, args[i]...))
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)
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)
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]...))
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]...))
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]...))
301 for i := range res.ExternalEntity {
302 a.Resources.AllCinema =
303 append(a.Resources.AllCinema, fmt.Sprintf(httpapi.AllCinemaFormat, args[i]...))
306 for i := range res.ExternalEntity {
308 append(a.Resources.Anison, fmt.Sprintf(httpapi.AnisonFormat, args[i]...))
311 for i := range res.ExternalEntity {
313 append(a.Resources.VNDB, fmt.Sprintf(httpapi.VNDBFormat, args[i]...))
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]...))
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"
329 func (a *Anime) populateFromUDP(reply udpapi.APIReply) bool {
330 if reply != nil && reply.Error() == nil {
331 parts := strings.Split(reply.Lines()[1], "|")
333 ints := make([]int64, len(parts))
334 for i, p := range parts {
335 ints[i], _ = strconv.ParseInt(p, 10, 32)
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
344 if len(parts[3]) > 0 {
348 // 0 does not actually mean the Epoch here...