1 // Low-level AniDB UDP API client library
3 // Implements the commands essential to setting up and tearing down an API connection,
4 // as well as an asynchronous layer. Throttles sends internally according to API spec.
6 // This library doesn't implement caching; beware since aggressive caching is an
7 // implementation requirement. Not doing so can get you banned.
23 AniDBUDPServer = "api.anidb.net"
27 type AniDBUDP struct {
28 // Interval between keep-alive packets; only sent when PUSH notifications are enabled (default: 20 minutes)
29 KeepAliveInterval time.Duration
31 // The time to wait before a packet is considered lost (default: 45 seconds)
34 // Channel where PUSH notifications are sent to
35 Notifications chan APIReply
45 tagRouter map[string]chan APIReply
46 routerLock sync.RWMutex
53 // notifyState *notifyState
57 // Creates and initializes the AniDBUDP struct
58 func NewAniDBUDP() *AniDBUDP {
60 KeepAliveInterval: 20 * time.Minute,
61 Timeout: 45 * time.Second,
62 Notifications: make(chan APIReply, 5),
63 tagRouter: make(map[string]chan APIReply),
68 // Key-value list of parameters.
69 type ParamMap map[string]interface{}
71 // Returns a query-like string representation of the ParamMap
72 func (m ParamMap) String() string {
73 keys := make([]string, 0, len(m))
75 keys = append(keys, k)
79 parts := make([]string, 0, len(m))
80 for _, k := range keys {
81 parts = append(parts, strings.Join([]string{k, fmt.Sprint((m)[k])}, "="))
83 return strings.Join(parts, "&")
86 // Sends the requested query to the AniDB UDP API server.
88 // Returns a channel through which the eventual reply is sent.
90 // See http://wiki.anidb.net/w/UDP_API_Definition for the defined commands.
91 func (a *AniDBUDP) SendRecv(command string, args ParamMap) <-chan APIReply {
93 tag := fmt.Sprintf("T%d", a.counter)
101 for k, v := range args {
103 s = strings.Replace(s, "\n", "<br/>", -1)
104 args[k] = strings.Replace(s, "&", "&", -1)
107 ch := make(chan APIReply, 1)
109 if err := a.dial(); err != nil {
110 ch <- newErrorWrapper(err)
116 a.tagRouter[tag] = ch
117 a.routerLock.Unlock()
119 reply := make(chan APIReply, 1)
121 <-a.send(command, args)
122 timeout := time.After(a.Timeout)
127 delete(a.tagRouter, tag)
128 a.routerLock.Unlock()
131 reply <- newErrorWrapper(TimeoutError)
134 log.Println("!!! Timeout")
137 delete(a.tagRouter, tag)
138 a.routerLock.Unlock()
148 var laddr, _ = net.ResolveUDPAddr("udp4", "0.0.0.0:0")
150 func (a *AniDBUDP) dial() (err error) {
155 srv := fmt.Sprintf("%s:%d", AniDBUDPServer, AniDBUDPPort)
156 if raddr, err := net.ResolveUDPAddr("udp4", srv); err != nil {
159 a.conn, err = net.DialUDP("udp4", laddr, raddr)
161 if a.breakSend != nil {
165 a.breakSend = make(chan bool)
167 a.sendCh = make(chan packet, 10)
170 if a.breakRecv != nil {
174 a.breakRecv = make(chan bool)
181 func (a *AniDBUDP) send(command string, args ParamMap) chan bool {
185 str = strings.Join([]string{command, arg}, " ")
187 log.Println(">>>", str)
189 p := makePacket([]byte(str), a.ecb)
191 return sendPacket(p, a.sendCh)
194 func (a *AniDBUDP) sendLoop() {
200 case pkt := <-a.sendCh:
203 // send twice: once for confirming with the queue,
204 // again for timeout calculations
205 for i := 0; i < 2; i++ {
212 func (a *AniDBUDP) recvLoop() {
213 pkt := make(chan packet, 1)
214 brk := make(chan bool)
222 b, err := a.getPacket()
223 pkt <- packet{b: b, err: err}
228 var pingTimer <-chan time.Time
231 if a.pingTimer != nil {
232 pingTimer = a.pingTimer.C
243 if a.KeepAliveInterval >= 30*time.Minute {
244 if (<-a.Uptime()).Error() != nil {
247 } else if (<-a.Ping()).Error() != nil {
250 a.pingTimer.Reset(a.KeepAliveInterval)
255 if err != nil && err != io.EOF && err != zlib.ErrChecksum {
256 // can UDP recv even raise other errors?
257 panic("UDP recv: " + err.Error())
260 if r := newGenericReply(b); r != nil {
261 if a.pingTimer != nil {
262 a.pingTimer.Reset(a.KeepAliveInterval)
265 if err == zlib.ErrChecksum {
270 if ch, ok := a.tagRouter[r.Tag()]; ok {
271 log.Println("<<<", string(b))
275 if c >= 720 && c < 799 {
276 // notices that need PUSHACK
277 id := strings.Fields(r.Text())[0]
278 a.send("PUSHACK", ParamMap{"nid": id})
282 // notice that doesn't need PUSHACK
286 if a.pingTimer == nil {
287 a.pingTimer = time.NewTimer(a.KeepAliveInterval)
292 } else if c == 701 || c == 702 {
293 // PUSHACK ACK, no need to route
294 } else if c == 281 || c == 282 || c == 381 || c == 382 {
295 // NOTIFYACK reply, ignore
297 // untagged error, broadcast to all
298 log.Println("<!<", string(b))
299 for _, ch := range a.tagRouter {
304 a.routerLock.RUnlock()