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