5 "github.com/Kovensky/go-anidb/misc"
6 "github.com/Kovensky/go-anidb/udp"
7 "github.com/Kovensky/go-fscache"
17 var _ cacheable = &File{}
19 func (f *File) setCachedTS(ts time.Time) {
23 func (f *File) IsStale() bool {
28 return time.Now().Sub(f.Cached) > FileIncompleteCacheDuration
30 return time.Now().Sub(f.Cached) > FileCacheDuration
33 func cacheFile(f *File) {
34 CacheSet(f.FID, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
35 CacheSet(f, "fid", f.FID)
40 func (fid FID) File() *File {
42 if CacheGet(&f, "fid", fid) == nil {
48 // Prefetches the Anime, Episode and Group that this
49 // file is linked to using the given AniDB instance.
51 // Returns a channel where this file will be sent to
52 // when the prefetching is done; if the file is nil,
53 // the channel will return nil.
54 func (f *File) Prefetch(adb *AniDB) <-chan *File {
55 ch := make(chan *File, 1)
58 a := adb.AnimeByID(f.AID)
59 g := adb.GroupByID(f.GID)
69 // Retrieves a File by its FID. Uses the UDP API.
70 func (adb *AniDB) FileByID(fid FID) <-chan *File {
71 key := []fscache.CacheKey{"fid", fid}
73 ch := make(chan *File, 1)
81 ic := make(chan notification, 1)
82 go func() { ch <- (<-ic).(*File); close(ch) }()
83 if intentMap.Intent(ic, key...) {
87 if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
88 intentMap.NotifyClose((*File)(nil), key...)
94 intentMap.NotifyClose(f, key...)
99 reply := <-adb.udp.SendRecv("FILE",
106 if reply.Error() == nil {
107 adb.parseFileResponse(&f, reply, false)
110 } else if reply.Code() == 320 {
111 Cache.SetInvalid(key...)
114 intentMap.NotifyClose(f, key...)
119 var validEd2kHash = regexp.MustCompile(`\A[:xdigit:]{32}\z`)
121 // Retrieves a File by its Ed2kHash + Filesize combination. Uses the UDP API.
122 func (adb *AniDB) FileByEd2kSize(ed2k string, size int64) <-chan *File {
123 key := []fscache.CacheKey{"fid", "by-ed2k", ed2k, size}
125 ch := make(chan *File, 1)
127 if size < 1 || !validEd2kHash.MatchString(ed2k) {
132 // AniDB always uses lower case hashes
133 ed2k = strings.ToLower(ed2k)
135 ic := make(chan notification, 1)
139 ch <- <-adb.FileByID(fid)
143 if intentMap.Intent(ic, key...) {
147 if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
148 intentMap.NotifyClose(FID(0), key...)
154 switch ts, err := Cache.Get(&fid, key...); {
155 case err != nil && time.Now().Sub(ts) < FileCacheDuration:
156 intentMap.NotifyClose(fid, key...)
161 reply := <-adb.udp.SendRecv("FILE",
170 if reply.Error() == nil {
171 adb.parseFileResponse(&f, reply, false)
176 } else if reply.Code() == 320 { // file not found
177 Cache.SetInvalid(key...)
178 } else if reply.Code() == 322 { // multiple files found
179 panic("Don't know what to do with " + strings.Join(reply.Lines(), "\n"))
182 intentMap.NotifyClose(fid, key...)
187 var fileFmask = "7fda7fe8"
188 var fileAmask = "00008000"
191 stateCRCOK = 1 << iota
201 func sanitizeCodec(codec string) string {
205 case "WMV9 (also WMV3)":
215 func (adb *AniDB) parseFileResponse(f **File, reply udpapi.APIReply, calledFromFIDsByGID bool) bool {
216 if reply.Error() != nil {
219 if reply.Truncated() {
223 uidChan := make(chan UID, 1)
224 if adb.udp.credentials != nil {
225 go func() { uidChan <- <-adb.GetUserUID(decrypt(adb.udp.credentials.username)) }()
231 parts := strings.Split(reply.Lines()[1], "|")
232 ints := make([]int64, len(parts))
233 for i, p := range parts {
234 ints[i], _ = strconv.ParseInt(p, 10, 64)
239 rels := strings.Split(parts[5], "'")
240 relList := make([]EID, 0, len(parts[5]))
241 related := make(RelatedEpisodes, len(parts[5]))
242 for _, rel := range rels {
243 r := strings.Split(rel, ",")
248 eid, _ := strconv.ParseInt(r[0], 10, 32)
249 pct, _ := strconv.ParseInt(r[1], 10, 32)
250 relList = append(relList, EID(eid))
251 related[EID(eid)] = float32(pct) / 100
258 epno := misc.ParseEpisodeList(parts[24])
265 if !epno[0].Start.ContainsEpisodes(epno[0].End) || len(epno) > 1 || len(relList) > 0 {
266 // epno is broken -- we need to sanitize it
267 thisEp := <-adb.EpisodeByID(eid)
270 parts := make([]string, 1, len(relList)+1)
271 parts[0] = thisEp.Episode.String()
273 // everything after this SHOULD be cache hits now, unless this is somehow
274 // linked with an EID from a different anime (*stares at Haruhi*).
275 // We don't want to use eps from different AIDs anyway, so that makes
278 // We check if the related episodes are all in sequence from this one.
279 // If they are, we build a new epno with the sequence. Otherwise,
280 // our epno will only have the primary episode.
282 // gather the episode numbers
283 for _, eid := range relList {
284 if ep := eid.Episode(); ep != nil && ep.AID == thisEp.AID {
285 parts = append(parts, ep.Episode.String())
292 test := misc.EpisodeList{}
293 // only if we didn't break the loop
295 test = misc.ParseEpisodeList(strings.Join(parts, ","))
299 if calledFromFIDsByGID {
301 log.Printf("UDP!!! FID %d is only part of episode %s with no complementary files", fid, epno)
302 } else if len(test) == 1 && test[0].Start.Number == test[0].End.Number {
305 for fid := range adb.FIDsByGID(thisEp, gid) {
306 fids = append(fids, int(fid))
308 if len(fids) >= 1 && fids[0] == 0 {
310 // Only entry was API error
315 sort.Sort(sort.IntSlice(fids))
316 idx := sort.SearchInts(fids, int(fid))
317 if idx == len(fids) {
318 panic(fmt.Sprintf("FID %d couldn't locate itself", fid))
324 epno[0].End = epno[0].Start
326 epno[0].Start.Parts = len(fids)
327 epno[0].Start.Part = idx
329 panic(fmt.Sprintf("Don't know what to do with partial episode %s (EID %d)", test, eid))
332 // if they're all in sequence, then we'll only have a single range in the list
336 // use only the primary epno then
337 epno = misc.ParseEpisodeList(thisEp.Episode.String())
343 version := FileVersion(1)
344 switch i := ints[7]; {
355 codecs := strings.Split(parts[14], "'")
356 bitrates := strings.Split(parts[15], "'")
357 alangs := strings.Split(parts[20], "'")
358 streams := make([]AudioStream, len(codecs))
359 for i := range streams {
360 br, _ := strconv.ParseInt(bitrates[i], 10, 32)
361 streams[i] = AudioStream{
363 Codec: sanitizeCodec(codecs[i]),
364 Language: Language(alangs[i]),
368 sl := strings.Split(parts[21], "'")
369 slangs := make([]Language, len(sl))
371 slangs[i] = Language(sl[i])
374 depth := int(ints[12])
378 res := strings.Split(parts[18], "x")
379 width, _ := strconv.ParseInt(res[0], 10, 32)
380 height, _ := strconv.ParseInt(res[1], 10, 32)
382 Bitrate: int(ints[17]),
383 Codec: sanitizeCodec(parts[16]),
385 Resolution: image.Rect(0, 0, int(width), int(height)),
408 RelatedEpisodes: related,
409 Deprecated: ints[6] != 0,
411 CRCMatch: ints[7]&stateCRCOK != 0,
412 BadCRC: ints[7]&stateCRCERR != 0,
414 Uncensored: ints[7]&stateUncensored != 0,
415 Censored: ints[7]&stateCensored != 0,
417 Incomplete: video.Resolution.Empty(),
424 Source: FileSource(parts[13]),
426 AudioStreams: streams,
427 SubtitleLanguages: slangs,
429 FileExtension: parts[19],
431 Length: time.Duration(ints[22]) * time.Second,
432 AirDate: time.Unix(ints[23], 0),