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.
28 AniDBUDPServer = "api.anidb.net"
32 type AniDBUDP struct {
33 KeepAliveInterval time.Duration // Interval between keep-alive packets; only sent when PUSH notifications are enabled (default: 20 minutes)
34 Timeout time.Duration // The time to wait before a packet is considered lost (default: 45 seconds)
35 Notifications chan APIReply // Channel where PUSH notifications are sent to
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)
102 if err := a.dial(); err != nil {
103 ch <- newErrorWrapper(err)
108 ch := make(chan APIReply, 1)
111 a.tagRouter[tag] = ch
112 a.routerLock.Unlock()
114 reply := make(chan APIReply, 1)
116 <-a.send(command, args)
117 timeout := time.After(a.Timeout)
122 delete(a.tagRouter, tag)
123 a.routerLock.Unlock()
126 reply <- TimeoutError
129 log.Println("!!! Timeout")
132 delete(a.tagRouter, tag)
133 a.routerLock.Unlock()
143 var laddr, _ = net.ResolveUDPAddr("udp4", "0.0.0.0:0")
145 func (a *AniDBUDP) dial() (err error) {
150 srv := fmt.Sprintf("%s:%d", AniDBUDPServer, AniDBUDPPort)
151 if raddr, err := net.ResolveUDPAddr("udp4", srv); err != nil {
154 a.conn, err = net.DialUDP("udp4", laddr, raddr)
156 if a.breakSend != nil {
160 a.breakSend = make(chan bool)
162 a.sendCh = make(chan packet, 10)
165 if a.breakRecv != nil {
169 a.breakRecv = make(chan bool)
176 func (a *AniDBUDP) send(command string, args ParamMap) chan bool {
180 str = strings.Join([]string{command, arg}, " ")
182 log.Println(">>>", str)
184 p := makePacket([]byte(str), a.ecb)
186 sendPacket(p, a.sendCh)
195 func (a *AniDBUDP) sendLoop() {
201 case pkt := <-a.sendCh:
204 // send twice: once for confirming with the queue,
205 // again for timeout calculations
206 for i := 0; i < 2; i++ {
213 func (a *AniDBUDP) recvLoop() {
214 pkt := make(chan packet, 1)
215 brk := make(chan bool)
223 b, err := getPacket(a.conn, a.ecb)
224 pkt <- packet{b: b, err: err}
229 var pingTimer <-chan time.Time
232 if a.pingTimer != nil {
233 pingTimer = a.pingTimer.C
244 if a.KeepAliveInterval >= 30*time.Minute {
245 if (<-a.Uptime()).Error() != nil {
248 } else if (<-a.Ping()).Error() != nil {
251 a.pingTimer.Reset(a.KeepAliveInterval)
256 if err != nil && err != io.EOF && err != zlib.ErrChecksum {
257 // can UDP recv even raise other errors?
258 panic("UDP recv: " + err.Error())
261 if r := newGenericReply(b); r != nil {
262 if a.pingTimer != nil {
263 a.pingTimer.Reset(a.KeepAliveInterval)
266 if err == zlib.ErrChecksum {
271 if ch, ok := a.tagRouter[r.Tag()]; ok {
273 log.Println("<<<", string(b))
277 if c >= 720 && c < 799 {
278 // notices that need PUSHACK
279 id := strings.Fields(r.Text())[0]
280 a.send("PUSHACK", paramMap{"nid": id})
284 // notice that doesn't need PUSHACK
288 if a.pingTimer == nil {
289 a.pingTimer = time.NewTimer(a.KeepAliveInterval)
294 } else if c == 701 || c == 702 {
295 // PUSHACK ACK, no need to route
296 } else if c == 281 || c == 282 || c == 381 || c == 382 {
297 // NOTIFYACK reply, ignore
299 // untagged error, broadcast to all
300 log.Println("<!<", string(b))
301 for _, ch := range a.tagRouter {
306 a.routerLock.RUnlock()