5 "github.com/Kovensky/go-anidb/misc"
6 "github.com/Kovensky/go-anidb/udp"
7 "github.com/Kovensky/go-fscache"
16 var _ cacheable = &File{}
18 func (f *File) setCachedTS(ts time.Time) {
22 func (f *File) IsStale() bool {
27 return time.Now().Sub(f.Cached) > FileIncompleteCacheDuration
29 return time.Now().Sub(f.Cached) > FileCacheDuration
32 func cacheFile(f *File) {
33 CacheSet(f.FID, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
34 CacheSet(f, "fid", f.FID)
39 func (fid FID) File() *File {
41 if CacheGet(&f, "fid", fid) == nil {
47 // Prefetches the Anime, Episode and Group that this
48 // file is linked to using the given AniDB instance.
50 // Returns a channel where this file will be sent to
51 // when the prefetching is done; if the file is nil,
52 // the channel will return nil.
53 func (f *File) Prefetch(adb *AniDB) <-chan *File {
54 ch := make(chan *File, 1)
57 a := adb.AnimeByID(f.AID)
58 g := adb.GroupByID(f.GID)
68 // Retrieves a File by its FID. Uses the UDP API.
69 func (adb *AniDB) FileByID(fid FID) <-chan *File {
70 key := []fscache.CacheKey{"fid", fid}
72 ch := make(chan *File, 1)
80 ic := make(chan notification, 1)
81 go func() { ch <- (<-ic).(*File); close(ch) }()
82 if intentMap.Intent(ic, key...) {
86 if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
87 intentMap.NotifyClose((*File)(nil), key...)
93 intentMap.NotifyClose(f, key...)
98 reply := <-adb.udp.SendRecv("FILE",
105 if reply.Error() == nil {
106 adb.parseFileResponse(&f, reply, false)
109 } else if reply.Code() == 320 {
110 Cache.SetInvalid(key...)
113 intentMap.NotifyClose(f, key...)
118 var validEd2kHash = regexp.MustCompile(`\A[[:xdigit:]]{32}\z`)
120 // Retrieves a File by its Ed2kHash + Filesize combination. Uses the UDP API.
121 func (adb *AniDB) FileByEd2kSize(ed2k string, size int64) <-chan *File {
122 key := []fscache.CacheKey{"fid", "by-ed2k", ed2k, size}
124 ch := make(chan *File, 1)
126 if size < 1 || !validEd2kHash.MatchString(ed2k) {
131 // AniDB always uses lower case hashes
132 ed2k = strings.ToLower(ed2k)
134 ic := make(chan notification, 1)
138 ch <- <-adb.FileByID(fid)
142 if intentMap.Intent(ic, key...) {
146 if !Cache.IsValid(InvalidKeyCacheDuration, key...) {
147 intentMap.NotifyClose(FID(0), key...)
153 switch ts, err := Cache.Get(&fid, key...); {
154 case err == nil && time.Now().Sub(ts) < FileCacheDuration:
155 intentMap.NotifyClose(fid, key...)
160 reply := <-adb.udp.SendRecv("FILE",
169 if reply.Error() == nil {
170 adb.parseFileResponse(&f, reply, false)
175 } else if reply.Code() == 320 { // file not found
176 Cache.SetInvalid(key...)
177 } else if reply.Code() == 322 { // multiple files found
178 panic("Don't know what to do with " + strings.Join(reply.Lines(), "\n"))
181 intentMap.NotifyClose(fid, key...)
186 var fileFmask = "7fda7fe8"
187 var fileAmask = "00008000"
190 stateCRCOK = 1 << iota
200 func sanitizeCodec(codec string) string {
204 case "WMV9 (also WMV3)":
214 var opedRE = regexp.MustCompile(`\A(Opening|Ending)(?: (\d+))?\z`)
216 func (adb *AniDB) parseFileResponse(f **File, reply udpapi.APIReply, calledFromFIDsByGID bool) bool {
217 if reply.Error() != nil {
220 if reply.Truncated() {
224 uidChan := make(chan UID, 1)
225 if adb.udp.credentials != nil {
226 go func() { uidChan <- <-adb.GetUserUID(decrypt(adb.udp.credentials.username)) }()
232 parts := strings.Split(reply.Lines()[1], "|")
233 ints := make([]int64, len(parts))
234 for i, p := range parts {
235 ints[i], _ = strconv.ParseInt(p, 10, 64)
240 rels := strings.Split(parts[5], "'")
241 relList := make([]EID, 0, len(parts[5]))
242 related := make(RelatedEpisodes, len(parts[5]))
243 for _, rel := range rels {
244 r := strings.Split(rel, ",")
249 eid, _ := strconv.ParseInt(r[0], 10, 32)
250 pct, _ := strconv.ParseInt(r[1], 10, 32)
251 relList = append(relList, EID(eid))
252 related[EID(eid)] = float32(pct) / 100
259 epno := misc.ParseEpisodeList(parts[24])
266 if !epno[0].Start.ContainsEpisodes(epno[0].End) || len(epno) > 1 || len(relList) > 0 {
267 // epno is broken -- we need to sanitize it
268 thisEp := <-adb.EpisodeByID(eid)
271 parts := make([]string, 1, len(relList)+1)
272 parts[0] = thisEp.Episode.String()
274 // everything after this SHOULD be cache hits now, unless this is somehow
275 // linked with an EID from a different anime (*stares at Haruhi*).
276 // We don't want to use eps from different AIDs anyway, so that makes
279 // We check if the related episodes are all in sequence from this one.
280 // If they are, we build a new epno with the sequence. Otherwise,
281 // our epno will only have the primary episode.
283 // gather the episode numbers
284 for _, eid := range relList {
285 if ep := eid.Episode(); ep != nil && ep.AID == thisEp.AID {
286 parts = append(parts, ep.Episode.String())
293 test := misc.EpisodeList{}
294 // only if we didn't break the loop
296 test = misc.ParseEpisodeList(strings.Join(parts, ","))
300 if calledFromFIDsByGID {
302 adb.Logger.Printf("UDP!!! FID %d is only part of episode %s with no complementary files", fid, epno)
303 } else if len(test) == 1 && test[0].Start.Number == test[0].End.Number {
306 for fid := range adb.FIDsByGID(thisEp, gid) {
307 fids = append(fids, int(fid))
309 if len(fids) >= 1 && fids[0] == 0 {
311 // Only entry was API error
316 sort.Sort(sort.IntSlice(fids))
317 idx := sort.SearchInts(fids, int(fid))
318 if idx == len(fids) {
319 panic(fmt.Sprintf("FID %d couldn't locate itself", fid))
325 epno[0].End = epno[0].Start
327 epno[0].Start.Parts = len(fids)
328 epno[0].Start.Part = idx
330 panic(fmt.Sprintf("Don't know what to do with partial episode %s (EID %d)", test, eid))
333 // if they're all in sequence, then we'll only have a single range in the list
337 // use only the primary epno then
338 epno = misc.ParseEpisodeList(thisEp.Episode.String())
344 epstr := epno.String()
345 if len(epno) == 1 && epno[0].Type == misc.EpisodeTypeCredits && epno[0].Len() == 1 {
349 if ep := <-adb.EpisodeByID(eid); ep == nil {
350 } else if m := opedRE.FindStringSubmatch(ep.Titles["en"]); len(m) > 2 {
351 num, err := strconv.ParseInt(m[2], 10, 32)
359 gobi := fmt.Sprintf("%d", n)
372 version := FileVersion(1)
373 switch i := ints[7]; {
384 codecs := strings.Split(parts[14], "'")
385 bitrates := strings.Split(parts[15], "'")
386 alangs := strings.Split(parts[20], "'")
387 streams := make([]AudioStream, len(codecs))
388 for i := range streams {
389 br, _ := strconv.ParseInt(bitrates[i], 10, 32)
390 streams[i] = AudioStream{
392 Codec: sanitizeCodec(codecs[i]),
393 Language: Language(alangs[i]),
397 sl := strings.Split(parts[21], "'")
398 slangs := make([]Language, len(sl))
400 slangs[i] = Language(sl[i])
403 depth := int(ints[12])
407 res := strings.Split(parts[18], "x")
408 width, _ := strconv.ParseInt(res[0], 10, 32)
409 height, _ := strconv.ParseInt(res[1], 10, 32)
411 Bitrate: int(ints[17]),
412 Codec: sanitizeCodec(parts[16]),
414 Resolution: image.Rect(0, 0, int(width), int(height)),
435 EpisodeString: epstr,
438 RelatedEpisodes: related,
439 Deprecated: ints[6] != 0,
441 CRCMatch: ints[7]&stateCRCOK != 0,
442 BadCRC: ints[7]&stateCRCERR != 0,
444 Uncensored: ints[7]&stateUncensored != 0,
445 Censored: ints[7]&stateCensored != 0,
447 Incomplete: video.Resolution.Empty(),
454 Source: FileSource(parts[13]),
456 AudioStreams: streams,
457 SubtitleLanguages: slangs,
459 FileExtension: parts[19],
461 Length: time.Duration(ints[22]) * time.Second,
462 AirDate: time.Unix(ints[23], 0),