6 "github.com/Kovensky/go-anidb/misc"
7 "github.com/Kovensky/go-anidb/udp"
18 gob.RegisterName("*github.com/Kovensky/go-anidb.File", &File{})
19 gob.RegisterName("*github.com/Kovensky/go-anidb.fidCache", &fidCache{})
20 gob.RegisterName("github.com/Kovensky/go-anidb.FID", FID(0))
23 func (f *File) Touch() {
27 func (f *File) IsStale() bool {
32 return time.Now().Sub(f.Cached) > FileIncompleteCacheDuration
34 return time.Now().Sub(f.Cached) > FileCacheDuration
41 func (e FID) Touch() {}
42 func (e FID) IsStale() bool { return false }
44 func (fid FID) File() *File {
46 if cache.Get(&f, "fid", fid) == nil {
52 type fidCache struct {
57 func (c *fidCache) Touch() {
61 func (c *fidCache) IsStale() bool {
62 return time.Now().Sub(c.Time) > FileCacheDuration
65 // Prefetches the Anime, Episode and Group that this
66 // file is linked to using the given AniDB instance.
68 // Returns a channel where this file will be sent to
69 // when the prefetching is done; if the file is nil,
70 // the channel will return nil.
71 func (f *File) Prefetch(adb *AniDB) <-chan *File {
72 ch := make(chan *File, 1)
75 a := adb.AnimeByID(f.AID)
76 g := adb.GroupByID(f.GID)
86 // Retrieves a File by its FID. Uses the UDP API.
87 func (adb *AniDB) FileByID(fid FID) <-chan *File {
88 keys := []cacheKey{"fid", fid}
90 ch := make(chan *File, 1)
98 ic := make(chan Cacheable, 1)
99 go func() { ch <- (<-ic).(*File); close(ch) }()
100 if intentMap.Intent(ic, keys...) {
104 if !cache.CheckValid(keys...) {
105 intentMap.NotifyClose((*File)(nil), keys...)
111 intentMap.NotifyClose(f, keys...)
116 reply := <-adb.udp.SendRecv("FILE",
123 if reply.Error() == nil {
124 f = adb.parseFileResponse(reply, false)
126 cache.Set(&fidCache{FID: f.FID}, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
127 cache.Set(f, keys...)
128 } else if reply.Code() == 320 {
129 cache.MarkInvalid(keys...)
132 intentMap.NotifyClose(f, keys...)
137 var validEd2kHash = regexp.MustCompile(`\A[:xdigit:]{32}\z`)
139 // Retrieves a File by its Ed2kHash + Filesize combination. Uses the UDP API.
140 func (adb *AniDB) FileByEd2kSize(ed2k string, size int64) <-chan *File {
141 keys := []cacheKey{"fid", "by-ed2k", ed2k, size}
143 ch := make(chan *File, 1)
145 if size < 1 || !validEd2kHash.MatchString(ed2k) {
150 // AniDB always uses lower case hashes
151 ed2k = strings.ToLower(ed2k)
153 ic := make(chan Cacheable, 1)
157 ch <- <-adb.FileByID(fid)
161 if intentMap.Intent(ic, keys...) {
165 if !cache.CheckValid(keys...) {
166 intentMap.NotifyClose(FID(0), keys...)
173 if cache.Get(&ec, keys...) == nil && !ec.IsStale() {
174 intentMap.NotifyClose(ec.FID, keys...)
180 reply := <-adb.udp.SendRecv("FILE",
189 if reply.Error() == nil {
190 f = adb.parseFileResponse(reply, false)
194 cache.Set(&fidCache{FID: fid}, keys...)
195 cache.Set(f, "fid", fid)
196 } else if reply.Code() == 320 { // file not found
197 cache.MarkInvalid(keys...)
198 } else if reply.Code() == 322 { // multiple files found
199 panic("Don't know what to do with " + strings.Join(reply.Lines(), "\n"))
202 intentMap.NotifyClose(fid, keys...)
207 var fileFmask = "77da7fe8"
208 var fileAmask = "00008000"
211 fileStateCRCOK = 1 << iota
221 func sanitizeCodec(codec string) string {
225 case "WMV9 (also WMV3)":
235 func (adb *AniDB) parseFileResponse(reply udpapi.APIReply, calledFromFIDsByGID bool) *File {
236 if reply.Error() != nil {
239 if reply.Truncated() {
243 parts := strings.Split(reply.Lines()[1], "|")
244 ints := make([]int64, len(parts))
245 for i, p := range parts {
246 ints[i], _ = strconv.ParseInt(p, 10, 64)
251 rels := strings.Split(parts[4], "'")
252 relList := make([]EID, 0, len(parts[4]))
253 related := make(RelatedEpisodes, len(parts[4]))
254 for _, rel := range rels {
255 r := strings.Split(rel, ",")
260 eid, _ := strconv.ParseInt(r[0], 10, 32)
261 pct, _ := strconv.ParseInt(r[1], 10, 32)
262 relList = append(relList, EID(eid))
263 related[EID(eid)] = float32(pct) / 100
270 epno := misc.ParseEpisodeList(parts[23])
276 if !epno[0].Start.ContainsEpisodes(epno[0].End) || len(epno) > 1 || len(relList) > 0 {
277 // epno is broken -- we need to sanitize it
278 thisEp := <-adb.EpisodeByID(eid)
281 parts := make([]string, 1, len(relList)+1)
282 parts[0] = thisEp.Episode.String()
284 // everything after this SHOULD be cache hits now, unless this is somehow
285 // linked with an EID from a different anime (*stares at Haruhi*).
286 // We don't want to use eps from different AIDs anyway, so that makes
289 // We check if the related episodes are all in sequence from this one.
290 // If they are, we build a new epno with the sequence. Otherwise,
291 // our epno will only have the primary episode.
293 // gather the episode numbers
294 for _, eid := range relList {
295 if ep := eid.Episode(); ep != nil && ep.AID == thisEp.AID {
296 parts = append(parts, ep.Episode.String())
303 test := misc.EpisodeList{}
304 // only if we didn't break the loop
306 test = misc.ParseEpisodeList(strings.Join(parts, ","))
310 if calledFromFIDsByGID {
312 log.Printf("UDP!!! FID %d is only part of episode %s with no complementary files", fid, epno)
313 } else if len(test) == 1 && test[0].Start.Number == test[0].End.Number {
316 for fid := range adb.FIDsByGID(thisEp, gid) {
317 fids = append(fids, int(fid))
319 if len(fids) >= 1 && fids[0] == 0 {
321 // Only entry was API error
326 sort.Sort(sort.IntSlice(fids))
327 idx := sort.SearchInts(fids, int(fid))
328 if idx == len(fids) {
329 panic(fmt.Sprintf("FID %d couldn't locate itself", fid))
335 epno[0].End = epno[0].Start
337 epno[0].Start.Parts = len(fids)
338 epno[0].Start.Part = idx
340 panic(fmt.Sprintf("Don't know what to do with partial episode %s (EID %d)", test, eid))
343 // if they're all in sequence, then we'll only have a single range in the list
347 // use only the primary epno then
348 epno = misc.ParseEpisodeList(thisEp.Episode.String())
354 version := FileVersion(1)
355 switch i := ints[6]; {
356 case i&fileStateV5 != 0:
358 case i&fileStateV4 != 0:
360 case i&fileStateV3 != 0:
362 case i&fileStateV2 != 0:
366 // codecs (parts[13]), bitrates (ints[14]), langs (parts[19])
367 codecs := strings.Split(parts[13], "'")
368 bitrates := strings.Split(parts[14], "'")
369 alangs := strings.Split(parts[19], "'")
370 streams := make([]AudioStream, len(codecs))
371 for i := range streams {
372 br, _ := strconv.ParseInt(bitrates[i], 10, 32)
373 streams[i] = AudioStream{
375 Codec: sanitizeCodec(codecs[i]),
376 Language: Language(alangs[i]),
380 sl := strings.Split(parts[20], "'")
381 slangs := make([]Language, len(sl))
383 slangs[i] = Language(sl[i])
386 depth := int(ints[11])
390 res := strings.Split(parts[17], "x")
391 width, _ := strconv.ParseInt(res[0], 10, 32)
392 height, _ := strconv.ParseInt(res[1], 10, 32)
394 Bitrate: int(ints[16]),
395 Codec: sanitizeCodec(parts[15]),
397 Resolution: image.Rect(0, 0, int(width), int(height)),
409 RelatedEpisodes: related,
410 Deprecated: ints[5] != 0,
412 CRCMatch: ints[6]&fileStateCRCOK != 0,
413 BadCRC: ints[6]&fileStateCRCERR != 0,
415 Uncensored: ints[6]&fileStateUncensored != 0,
416 Censored: ints[6]&fileStateCensored != 0,
418 Incomplete: video.Resolution.Empty(),
425 Source: FileSource(parts[12]),
427 AudioStreams: streams,
428 SubtitleLanguages: slangs,
430 FileExtension: parts[18],
432 Length: time.Duration(ints[21]) * time.Second,
433 AirDate: time.Unix(ints[22], 0),