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