5 "github.com/EliasFleckenstein03/go-anidb/misc"
6 "github.com/EliasFleckenstein03/go-anidb/udp"
7 "github.com/EliasFleckenstein03/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.AID, "aid", "by-eid", f.EID)
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 var opedRE = regexp.MustCompile(`\A(Opening|Ending)(?: (\d+))?\z`)
217 func (adb *AniDB) parseFileResponse(f **File, reply udpapi.APIReply, calledFromFIDsByGID bool) bool {
218 if reply.Error() != nil {
221 if reply.Truncated() {
225 uidChan := make(chan UID, 1)
226 if adb.udp.credentials != nil {
227 go func() { uidChan <- <-adb.GetUserUID(decrypt(adb.udp.credentials.username)) }()
233 parts := strings.Split(reply.Lines()[1], "|")
234 ints := make([]int64, len(parts))
235 for i, p := range parts {
236 ints[i], _ = strconv.ParseInt(p, 10, 64)
241 rels := strings.Split(parts[5], "'")
242 relList := make([]EID, 0, len(parts[5]))
243 related := make(RelatedEpisodes, len(parts[5]))
244 for _, rel := range rels {
245 r := strings.Split(rel, ",")
250 eid, _ := strconv.ParseInt(r[0], 10, 32)
251 pct, _ := strconv.ParseInt(r[1], 10, 32)
252 relList = append(relList, EID(eid))
253 related[EID(eid)] = float32(pct) / 100
260 epno := misc.ParseEpisodeList(parts[24])
267 if !epno[0].Start.ContainsEpisodes(epno[0].End) || len(epno) > 1 || len(relList) > 0 {
268 // epno is broken -- we need to sanitize it
269 thisEp := <-adb.EpisodeByID(eid)
272 parts := make([]string, 1, len(relList)+1)
273 parts[0] = thisEp.Episode.String()
275 // everything after this SHOULD be cache hits now, unless this is somehow
276 // linked with an EID from a different anime (*stares at Haruhi*).
277 // We don't want to use eps from different AIDs anyway, so that makes
280 // We check if the related episodes are all in sequence from this one.
281 // If they are, we build a new epno with the sequence. Otherwise,
282 // our epno will only have the primary episode.
284 // gather the episode numbers
285 for _, eid := range relList {
286 if ep := eid.Episode(); ep != nil && ep.AID == thisEp.AID {
287 parts = append(parts, ep.Episode.String())
294 test := misc.EpisodeList{}
295 // only if we didn't break the loop
297 test = misc.ParseEpisodeList(strings.Join(parts, ","))
301 if calledFromFIDsByGID {
303 adb.Logger.Printf("UDP!!! FID %d is only part of episode %s with no complementary files", fid, epno)
304 } else if len(test) == 1 && test[0].Start.Number == test[0].End.Number {
307 for fid := range adb.FIDsByGID(thisEp, gid) {
308 fids = append(fids, int(fid))
310 if len(fids) >= 1 && fids[0] == 0 {
312 // Only entry was API error
317 sort.Sort(sort.IntSlice(fids))
318 idx := sort.SearchInts(fids, int(fid))
319 if idx == len(fids) {
320 panic(fmt.Sprintf("FID %d couldn't locate itself", fid))
326 epno[0].End = epno[0].Start
328 epno[0].Start.Parts = len(fids)
329 epno[0].Start.Part = idx
331 panic(fmt.Sprintf("Don't know what to do with partial episode %s (EID %d)", test, eid))
334 // if they're all in sequence, then we'll only have a single range in the list
338 // use only the primary epno then
339 epno = misc.ParseEpisodeList(thisEp.Episode.String())
345 epstr := epno.String()
346 if len(epno) == 1 && epno[0].Type == misc.EpisodeTypeCredits && epno[0].Len() == 1 {
350 if ep := <-adb.EpisodeByID(eid); ep == nil {
351 } else if m := opedRE.FindStringSubmatch(ep.Titles["en"]); len(m) > 2 {
352 num, err := strconv.ParseInt(m[2], 10, 32)
360 gobi := fmt.Sprintf("%d", n)
373 version := FileVersion(1)
374 switch i := ints[7]; {
385 codecs := strings.Split(parts[14], "'")
386 bitrates := strings.Split(parts[15], "'")
387 alangs := strings.Split(parts[20], "'")
388 streams := make([]AudioStream, len(codecs))
389 for i := range streams {
390 br, _ := strconv.ParseInt(bitrates[i], 10, 32)
391 streams[i] = AudioStream{
393 Codec: sanitizeCodec(codecs[i]),
394 Language: Language(alangs[i]),
398 sl := strings.Split(parts[21], "'")
399 slangs := make([]Language, len(sl))
401 slangs[i] = Language(sl[i])
404 depth := int(ints[12])
408 res := strings.Split(parts[18], "x")
409 width, _ := strconv.ParseInt(res[0], 10, 32)
410 height, _ := strconv.ParseInt(res[1], 10, 32)
412 Bitrate: int(ints[17]),
413 Codec: sanitizeCodec(parts[16]),
415 Resolution: image.Rect(0, 0, int(width), int(height)),
436 EpisodeString: epstr,
439 RelatedEpisodes: related,
440 Deprecated: ints[6] != 0,
442 CRCMatch: ints[7]&stateCRCOK != 0,
443 BadCRC: ints[7]&stateCRCERR != 0,
445 Uncensored: ints[7]&stateUncensored != 0,
446 Censored: ints[7]&stateCensored != 0,
448 Incomplete: video.Resolution.Empty(),
455 Source: FileSource(parts[13]),
457 AudioStreams: streams,
458 SubtitleLanguages: slangs,
460 FileExtension: parts[19],
462 Length: time.Duration(ints[22]) * time.Second,
463 AirDate: time.Unix(ints[23], 0),