]> git.lizzy.rs Git - go-anidb.git/blob - filecache.go
anidb: Refactor the intent map
[go-anidb.git] / filecache.go
1 package anidb
2
3 import (
4         "encoding/gob"
5         "github.com/Kovensky/go-anidb/misc"
6         "github.com/Kovensky/go-anidb/udp"
7         "image"
8         "log"
9         "regexp"
10         "strconv"
11         "strings"
12         "time"
13 )
14
15 func init() {
16         gob.RegisterName("*github.com/Kovensky/go-anidb.File", &File{})
17         gob.RegisterName("*github.com/Kovensky/go-anidb.fidCache", &fidCache{})
18         gob.RegisterName("github.com/Kovensky/go-anidb.FID", FID(0))
19 }
20
21 func (f *File) Touch() {
22         f.Cached = time.Now()
23 }
24
25 func (f *File) IsStale() bool {
26         if f == nil {
27                 return true
28         }
29         if f.Incomplete {
30                 return time.Now().Sub(f.Cached) > FileIncompleteCacheDuration
31         }
32         return time.Now().Sub(f.Cached) > FileCacheDuration
33 }
34
35 type FID int
36
37 // make FID Cacheable
38
39 func (e FID) Touch()        {}
40 func (e FID) IsStale() bool { return false }
41
42 func (fid FID) File() *File {
43         var f File
44         if cache.Get(&f, "fid", fid) == nil {
45                 return &f
46         }
47         return nil
48 }
49
50 type fidCache struct {
51         FID
52         Time time.Time
53 }
54
55 func (c *fidCache) Touch() {
56         c.Time = time.Now()
57 }
58
59 func (c *fidCache) IsStale() bool {
60         return time.Now().Sub(c.Time) > FileCacheDuration
61 }
62
63 // Prefetches the Anime, Episode and Group that this
64 // file is linked to using the given AniDB instance.
65 //
66 // Returns a channel where this file will be sent to
67 // when the prefetching is done; if the file is nil,
68 // the channel will return nil.
69 func (f *File) Prefetch(adb *AniDB) <-chan *File {
70         ch := make(chan *File, 1)
71         go func() {
72                 if f != nil {
73                         a := adb.AnimeByID(f.AID)
74                         g := adb.GroupByID(f.GID)
75                         <-a
76                         <-g
77                         ch <- f
78                 }
79                 close(ch)
80         }()
81         return ch
82 }
83
84 // Retrieves a File by its FID. Uses the UDP API.
85 func (adb *AniDB) FileByID(fid FID) <-chan *File {
86         keys := []cacheKey{"fid", fid}
87
88         ch := make(chan *File, 1)
89
90         if fid < 1 {
91                 ch <- nil
92                 close(ch)
93                 return ch
94         }
95
96         ic := make(chan Cacheable, 1)
97         go func() { ch <- (<-ic).(*File); close(ch) }()
98         if intentMap.Intent(ic, keys...) {
99                 return ch
100         }
101
102         if !cache.CheckValid(keys...) {
103                 intentMap.NotifyClose((*File)(nil), keys...)
104                 return ch
105         }
106
107         f := fid.File()
108         if !f.IsStale() {
109                 intentMap.NotifyClose(f, keys...)
110                 return ch
111         }
112
113         go func() {
114                 reply := <-adb.udp.SendRecv("FILE",
115                         paramMap{
116                                 "fid":   fid,
117                                 "fmask": fileFmask,
118                                 "amask": fileAmask,
119                         })
120
121                 if reply.Error() == nil {
122                         f = parseFileResponse(reply)
123
124                         cache.Set(&fidCache{FID: f.FID}, "fid", "by-ed2k", f.Ed2kHash, f.Filesize)
125                         cache.Set(f, keys...)
126                 } else if reply.Code() == 320 {
127                         cache.MarkInvalid(keys...)
128                 }
129
130                 intentMap.NotifyClose(f, keys...)
131         }()
132         return ch
133 }
134
135 var validEd2kHash = regexp.MustCompile(`\A[:xdigit:]{32}\z`)
136
137 // Retrieves a File by its Ed2kHash + Filesize combination. Uses the UDP API.
138 func (adb *AniDB) FileByEd2kSize(ed2k string, size int64) <-chan *File {
139         keys := []cacheKey{"fid", "by-ed2k", ed2k, size}
140
141         ch := make(chan *File, 1)
142
143         if size < 1 || !validEd2kHash.MatchString(ed2k) {
144                 ch <- nil
145                 close(ch)
146                 return ch
147         }
148         // AniDB always uses lower case hashes
149         ed2k = strings.ToLower(ed2k)
150
151         ic := make(chan Cacheable, 1)
152         go func() {
153                 fid := (<-ic).(FID)
154                 if fid > 0 {
155                         ch <- <-adb.FileByID(fid)
156                 }
157                 close(ch)
158         }()
159         if intentMap.Intent(ic, keys...) {
160                 return ch
161         }
162
163         if !cache.CheckValid(keys...) {
164                 intentMap.NotifyClose(FID(0), keys...)
165                 return ch
166         }
167
168         fid := FID(0)
169
170         var ec fidCache
171         if cache.Get(&ec, keys...) == nil && !ec.IsStale() {
172                 intentMap.NotifyClose(ec.FID, keys...)
173                 return ch
174         }
175         fid = ec.FID
176
177         go func() {
178                 reply := <-adb.udp.SendRecv("FILE",
179                         paramMap{
180                                 "ed2k":  ed2k,
181                                 "size":  size,
182                                 "fmask": fileFmask,
183                                 "amask": fileAmask,
184                         })
185
186                 var f *File
187                 if reply.Error() == nil {
188                         f = parseFileResponse(reply)
189
190                         fid = f.FID
191
192                         cache.Set(&fidCache{FID: fid}, keys...)
193                         cache.Set(f, "fid", fid)
194                 } else if reply.Code() == 320 { // file not found
195                         cache.MarkInvalid(keys...)
196                 } else if reply.Code() == 322 { // multiple files found
197                         panic("Don't know what to do with " + strings.Join(reply.Lines(), "\n"))
198                 }
199
200                 intentMap.NotifyClose(fid, keys...)
201         }()
202         return ch
203 }
204
205 var fileFmask = "77da7fe8"
206 var fileAmask = "00008000"
207
208 const (
209         fileStateCRCOK = 1 << iota
210         fileStateCRCERR
211         fileStateV2
212         fileStateV3
213         fileStateV4
214         fileStateV5
215         fileStateUncensored
216         fileStateCensored
217 )
218
219 func sanitizeCodec(codec string) string {
220         switch codec {
221         case "MP3 CBR":
222                 return "MP3"
223         case "WMV9 (also WMV3)":
224                 return "WMV9"
225         case "Ogg (Vorbis)":
226                 return "Vorbis"
227         case "H264/AVC":
228                 return "H.264"
229         }
230         return codec
231 }
232
233 func parseFileResponse(reply udpapi.APIReply) *File {
234         if reply.Error() != nil {
235                 return nil
236         }
237         if reply.Truncated() {
238                 panic("Truncated")
239         }
240
241         parts := strings.Split(reply.Lines()[1], "|")
242         ints := make([]int64, len(parts))
243         for i, p := range parts {
244                 ints[i], _ = strconv.ParseInt(parts[i], 10, 64)
245                 log.Printf("#%d: %s\n", i, p)
246         }
247
248         // how does epno look like?
249         log.Println("epno: " + parts[23])
250
251         version := FileVersion(1)
252         switch i := ints[6]; {
253         case i&fileStateV5 != 0:
254                 version = 5
255         case i&fileStateV4 != 0:
256                 version = 4
257         case i&fileStateV3 != 0:
258                 version = 3
259         case i&fileStateV2 != 0:
260                 version = 2
261         }
262
263         // codecs (parts[13]), bitrates (ints[14]), langs (parts[19])
264         codecs := strings.Split(parts[13], "'")
265         bitrates := strings.Split(parts[14], "'")
266         alangs := strings.Split(parts[19], "'")
267         streams := make([]AudioStream, len(codecs))
268         for i := range streams {
269                 br, _ := strconv.ParseInt(bitrates[i], 10, 32)
270                 streams[i] = AudioStream{
271                         Bitrate:  int(br),
272                         Codec:    sanitizeCodec(codecs[i]),
273                         Language: Language(alangs[i]),
274                 }
275         }
276
277         sl := strings.Split(parts[20], "'")
278         slangs := make([]Language, len(sl))
279         for i := range sl {
280                 slangs[i] = Language(sl[i])
281         }
282
283         depth := int(ints[11])
284         if depth == 0 {
285                 depth = 8
286         }
287         res := strings.Split(parts[17], "x")
288         width, _ := strconv.ParseInt(res[0], 10, 32)
289         height, _ := strconv.ParseInt(res[1], 10, 32)
290         video := VideoInfo{
291                 Bitrate:    int(ints[16]),
292                 Codec:      sanitizeCodec(parts[15]),
293                 ColorDepth: depth,
294                 Resolution: image.Rect(0, 0, int(width), int(height)),
295         }
296
297         return &File{
298                 FID: FID(ints[0]),
299
300                 AID: AID(ints[1]),
301                 EID: EID(ints[2]),
302                 GID: GID(ints[3]),
303
304                 OtherEpisodes: misc.ParseEpisodeList(parts[4]).Simplify(),
305                 Deprecated:    ints[5] != 0,
306
307                 CRCMatch:   ints[6]&fileStateCRCOK != 0,
308                 BadCRC:     ints[6]&fileStateCRCERR != 0,
309                 Version:    version,
310                 Uncensored: ints[6]&fileStateUncensored != 0,
311                 Censored:   ints[6]&fileStateCensored != 0,
312
313                 Incomplete: video.Resolution.Empty(),
314
315                 Filesize: ints[7],
316                 Ed2kHash: parts[8],
317                 SHA1Hash: parts[9],
318                 CRC32:    parts[10],
319
320                 Source: FileSource(parts[12]),
321
322                 AudioStreams:      streams,
323                 SubtitleLanguages: slangs,
324                 VideoInfo:         video,
325                 FileExtension:     parts[18],
326
327                 Length:  time.Duration(ints[21]) * time.Second,
328                 AirDate: time.Unix(ints[22], 0),
329         }
330 }