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