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