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 f = adb.parseFileResponse(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 f = adb.parseFileResponse(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 = "77da7fe8"
188 var fileAmask = "00008000"
191 fileStateCRCOK = 1 << iota
201 func sanitizeCodec(codec string) string {
205 case "WMV9 (also WMV3)":
215 func (adb *AniDB) parseFileResponse(reply udpapi.APIReply, calledFromFIDsByGID bool) *File {
216 if reply.Error() != nil {
219 if reply.Truncated() {
223 parts := strings.Split(reply.Lines()[1], "|")
224 ints := make([]int64, len(parts))
225 for i, p := range parts {
226 ints[i], _ = strconv.ParseInt(p, 10, 64)
231 rels := strings.Split(parts[4], "'")
232 relList := make([]EID, 0, len(parts[4]))
233 related := make(RelatedEpisodes, len(parts[4]))
234 for _, rel := range rels {
235 r := strings.Split(rel, ",")
240 eid, _ := strconv.ParseInt(r[0], 10, 32)
241 pct, _ := strconv.ParseInt(r[1], 10, 32)
242 relList = append(relList, EID(eid))
243 related[EID(eid)] = float32(pct) / 100
250 epno := misc.ParseEpisodeList(parts[23])
256 if !epno[0].Start.ContainsEpisodes(epno[0].End) || len(epno) > 1 || len(relList) > 0 {
257 // epno is broken -- we need to sanitize it
258 thisEp := <-adb.EpisodeByID(eid)
261 parts := make([]string, 1, len(relList)+1)
262 parts[0] = thisEp.Episode.String()
264 // everything after this SHOULD be cache hits now, unless this is somehow
265 // linked with an EID from a different anime (*stares at Haruhi*).
266 // We don't want to use eps from different AIDs anyway, so that makes
269 // We check if the related episodes are all in sequence from this one.
270 // If they are, we build a new epno with the sequence. Otherwise,
271 // our epno will only have the primary episode.
273 // gather the episode numbers
274 for _, eid := range relList {
275 if ep := eid.Episode(); ep != nil && ep.AID == thisEp.AID {
276 parts = append(parts, ep.Episode.String())
283 test := misc.EpisodeList{}
284 // only if we didn't break the loop
286 test = misc.ParseEpisodeList(strings.Join(parts, ","))
290 if calledFromFIDsByGID {
292 log.Printf("UDP!!! FID %d is only part of episode %s with no complementary files", fid, epno)
293 } else if len(test) == 1 && test[0].Start.Number == test[0].End.Number {
296 for fid := range adb.FIDsByGID(thisEp, gid) {
297 fids = append(fids, int(fid))
299 if len(fids) >= 1 && fids[0] == 0 {
301 // Only entry was API error
306 sort.Sort(sort.IntSlice(fids))
307 idx := sort.SearchInts(fids, int(fid))
308 if idx == len(fids) {
309 panic(fmt.Sprintf("FID %d couldn't locate itself", fid))
315 epno[0].End = epno[0].Start
317 epno[0].Start.Parts = len(fids)
318 epno[0].Start.Part = idx
320 panic(fmt.Sprintf("Don't know what to do with partial episode %s (EID %d)", test, eid))
323 // if they're all in sequence, then we'll only have a single range in the list
327 // use only the primary epno then
328 epno = misc.ParseEpisodeList(thisEp.Episode.String())
334 version := FileVersion(1)
335 switch i := ints[6]; {
336 case i&fileStateV5 != 0:
338 case i&fileStateV4 != 0:
340 case i&fileStateV3 != 0:
342 case i&fileStateV2 != 0:
346 // codecs (parts[13]), bitrates (ints[14]), langs (parts[19])
347 codecs := strings.Split(parts[13], "'")
348 bitrates := strings.Split(parts[14], "'")
349 alangs := strings.Split(parts[19], "'")
350 streams := make([]AudioStream, len(codecs))
351 for i := range streams {
352 br, _ := strconv.ParseInt(bitrates[i], 10, 32)
353 streams[i] = AudioStream{
355 Codec: sanitizeCodec(codecs[i]),
356 Language: Language(alangs[i]),
360 sl := strings.Split(parts[20], "'")
361 slangs := make([]Language, len(sl))
363 slangs[i] = Language(sl[i])
366 depth := int(ints[11])
370 res := strings.Split(parts[17], "x")
371 width, _ := strconv.ParseInt(res[0], 10, 32)
372 height, _ := strconv.ParseInt(res[1], 10, 32)
374 Bitrate: int(ints[16]),
375 Codec: sanitizeCodec(parts[15]),
377 Resolution: image.Rect(0, 0, int(width), int(height)),
389 RelatedEpisodes: related,
390 Deprecated: ints[5] != 0,
392 CRCMatch: ints[6]&fileStateCRCOK != 0,
393 BadCRC: ints[6]&fileStateCRCERR != 0,
395 Uncensored: ints[6]&fileStateUncensored != 0,
396 Censored: ints[6]&fileStateCensored != 0,
398 Incomplete: video.Resolution.Empty(),
405 Source: FileSource(parts[12]),
407 AudioStreams: streams,
408 SubtitleLanguages: slangs,
410 FileExtension: parts[18],
412 Length: time.Duration(ints[21]) * time.Second,
413 AirDate: time.Unix(ints[22], 0),