Ticket #971: cddbp.py

File cddbp.py, 11.3 KB (added by rhymes, 9 years ago)
Line 
1"""CDDB protocol implementation.
2
3Future Plans:
4 - add server protocol
5 - add not readonly client commands
6"""
7
8from twisted.internet import reactor, protocol
9from twisted.protocols import basic
10
11TERMINATING_MARKER = "."
12MAX_PROTO = "6"
13
14# response codes
15OK                        = 200
16EXACT_MATCH               = OK
17CURRENT_PROTO_LEVEL       = OK
18OK_READ_ONLY              = 201
19OK_PROTO_LEVEL_CHANGED    = 201
20NO_MATCH_FOUND            = 202
21OK_FURTHER_DATA_FOLLOWS   = 210
22MULTIPLE_INEXACT_MATCHES  = 211
23OK_VERSION_INFO_FOLLOWS   = 211
24QUIT_OK_CLOSING           = 230
25NOT_AVAILABLE             = 401
26UNLINK_PERMISSION_DENIED  = 401
27FILE_ACCESS_FAILED        = 402
28SERVER_ERROR              = 402
29HANDSHAKE_ALREADY_DONE    = 402
30DB_ENTRY_CORRUPTED        = 403
31CGI_ENV_ERROR             = 408
32HANDSHAKE_NOT_DONE        = 409
33HANDSHAKE_FAILED          = 431
34CONNECT_PERMISSION_DENIED = 432
35USER_LIMIT_EXCEED         = 433
36SYSTEM_LOAD_TOO_HIGH      = 434
37SYNTAX_ERROR              = 500
38UNKOWN_COMMAND            = SYNTAX_ERROR
39UNIMPLEMENTED_COMMAND     = SYNTAX_ERROR
40INVALID_DATA              = 501
41SAME_PROTO_LEVEL          = 502
42QUIT_ERROR_CLOSING        = 530
43SERVER_TIMEOUT            = 530
44
45# proper codes for each supported command
46login_codes = [OK, OK_READ_ONLY, CONNECT_PERMISSION_DENIED, USER_LIMIT_EXCEED,
47                SYSTEM_LOAD_TOO_HIGH]
48hello_codes = [OK, HANDSHAKE_FAILED, HANDSHAKE_ALREADY_DONE]
49lscat_codes = [OK_FURTHER_DATA_FOLLOWS]
50query_codes = [EXACT_MATCH, MULTIPLE_INEXACT_MATCHES, NO_MATCH_FOUND,
51               DB_ENTRY_CORRUPTED, HANDSHAKE_NOT_DONE]
52read_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE, SERVER_ERROR,
53              DB_ENTRY_CORRUPTED, HANDSHAKE_NOT_DONE]
54discid_codes = [OK, SYNTAX_ERROR]
55help_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE]
56motd_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE]
57proto_codes = [CURRENT_PROTO_LEVEL, OK_PROTO_LEVEL_CHANGED,
58               INVALID_DATA, SAME_PROTO_LEVEL]
59quit_codes = [QUIT_OK_CLOSING, QUIT_ERROR_CLOSING]
60sites_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE]
61stat_codes = [OK_FURTHER_DATA_FOLLOWS]
62ver_codes = [OK, OK_FURTHER_DATA_FOLLOWS]
63misc_codes = [SERVER_ERROR, CGI_ENV_ERROR, SYNTAX_ERROR,
64              UNKOWN_COMMAND, UNIMPLEMENTED_COMMAND, SERVER_TIMEOUT]
65unlink_codes = [OK, UNLINK_PERMISSION_DENIED, FILE_ACCESS_FAILED, INVALID_DATA]
66
67# helper codes list
68successful_codes = [200, 201, 230]
69multi_line_codes = [210, 211]
70failure_error_codes = [431, 432, 433, 434]
71
72# dictionary to map codes
73RESPONSE_CODES = {
74    "login": login_codes,
75    "hello": hello_codes,
76    "lscat": lscat_codes,
77    "query": query_codes,
78    "read": read_codes,
79    "discid": discid_codes,
80    "help": help_codes,
81    "motd": motd_codes,
82    "proto": proto_codes,
83    "quit": quit_codes,
84    "sites": sites_codes,
85    "stat": stat_codes,
86    "ver": ver_codes,
87    "unlink": unlink_codes,
88    "general_errors": misc_codes
89    }
90
91class CDDBPClient(basic.LineReceiver):
92    """ CDDB Protocol client implementation.
93   
94    @cvar buffer: Holds multi line reponse data.
95    @cvar command: The current executing command.
96    @cvar multi_line: Set it to true in L{self.handleStatus} when you expect
97    further incoming data.
98    """
99    buffer = []
100    command = ""
101    multi_line = False
102   
103    def connectionMade(self):
104        self.buffer = []
105        self.command = "login"
106        self.multi_line = False
107   
108    def lineReceived(self, line):
109        """ Called when a line is received from the server.
110       
111        This method checks if the current running command expects a multi
112        line response or not and calls the right method to handle the incoming
113        data.
114        """
115        if not self.multi_line:
116            code = int(line.split(" ", 1)[0])
117            self.handleStatus(code, line)
118        else:
119            if line == TERMINATING_MARKER:
120                self.parseResponse(self.buffer[:])
121                self.buffer = []
122                self.multi_line = False
123            else:
124                self.buffer.append(line)
125
126    def sendCommand(self, command, args=[]):
127        """ Create string commands to send to the server. """
128        str = "%s %s" % (command, " ".join(args))
129        self.sendLine(str)
130
131    def hello(self, username, hostname, app_name, app_version):
132        """ Send 'hello' command to initiate the session (handshake).
133       
134        sample command:
135            - C{cddb hello foo localhost app 1.0}
136        """
137        self.command = "hello"
138        self.sendCommand("cddb", [self.command, username, hostname,
139                                  app_name, app_version])
140   
141    def lscat(self):
142        """ Send 'lscat' command to get the list of available categories.
143       
144        sample command:
145            - C{cddb lscat}
146        """
147        self.command = "lscat"
148        self.sendCommand("cddb", [self.command])
149   
150    def query(self, full_discid):
151        """ Send 'query' command to query freedb for matching entries.
152           
153        full_discid has to be in the form explained in the
154        U{CDDB-protocol documentation<http://
155        freedb.org/modules.php?name=Sections&sop=viewarticle&artid=28>}       
156       
157        sample command:
158            - C{cddb query f2123610 16 150 29977 46577 68970 85297 104922 131622
159            150317 157300 181212 208612 231910 253045 273352 295987 326627 4664}
160           
161        @type full_discid: string
162        @param full_discid: The discid (in hexadecimal lower case
163        representation), followed by the number of tracks, the frame offset for
164        each track and finally the length of the cd in seconds, for example:
165           
166        C{f2123610 16 150 29977 46577 68970 85297 104922 131622 150317 157300
167        181212 208612 231910 253045 273352 295987 326627 4664}
168        """
169        self.command = "query"
170        self.sendCommand("cddb", [self.command, full_discid])
171
172    def read(self, category, discid):
173        """ Send 'read' command to read entries from a freedb server.
174        L{query} must B{always} be called before this method.
175       
176        sample command:
177            - C{cddb read misc f2123610}
178           
179        @type category: string
180        @param category: Category has to be one of the available categories,
181        call L{self.lscat} to know them.
182        """
183        self.command = "read"
184        self.sendCommand("cddb", [self.command, category, discid])
185
186    def discid(self, cd_data):
187        """ Send 'discid' command to let the server compute discid from cd data.
188       
189        sample command:
190            - C{discid 16 150 29977 46577 68970 85297 104922 131622 150317
191            157300 181212 208612 231910 253045 273352 295987 326627 4664}
192       
193        @type cd_data: string
194        @param cd_data: The number of tracks followed by the frame offset
195        of each cd track and the length of the cd in seconds, for example:
196           
197        C{16 150 29977 46577 68970 85297 104922 131622 150317 157300
198        181212 208612 231910 253045 273352 295987 326627 4664}
199        """
200        self.command = "discid"
201        self.sendCommand(self.command, [cd_data])
202
203    def proto(self, level=""):
204        """ Send 'proto' command to get/set the server's current cddbp
205        protocol level.
206       
207        The protocol level is a number between 1 and L{MAX_PROTO}. The protocol
208        level number is optional (if you let it blank you get the current
209        level).
210       
211        sample commands:
212            - C{proto}
213           
214            or
215            - C{proto 6}
216        @type level: int
217        @param level: The protocol level to set.
218        """
219        self.command = "proto"
220        self.sendCommand(self.command, [level])
221       
222    def help(self, help_cmd="", help_subcmd=""):
223        """ Send 'help' command to get help about supported commands.
224       
225        Sending only I{help} returns a brief explanation of available commands.
226        You can gain more help adding a specific command to help_cmd argument
227        and help_subcmd argument.
228       
229        sample commands:
230            - C{help}
231           
232            or
233            - C{help quit}
234           
235            or
236            - C{help cddb hello}
237           
238        @type help_cmd: string
239        @param help_cmd: Represents the command to get help for
240        @type help_subcmd: string
241        @param help_subcmd: Represents the sub command to get help for
242        """
243        self.command = "help"
244        self.sendCommand(self.command, [help_cmd, help_subcmd])
245   
246    def motd(self):
247        """ Send 'motd' command to get the message of the day.
248       
249        sample command:
250            - C{motd}
251        """
252        self.command = "motd"
253        self.sendCommand(self.command)
254
255    def quit(self):
256        """ Send 'quit' command to close the connection.
257       
258        sample command:
259            - C{quit}
260        """
261        self.command = "quit"
262        self.sendCommand(self.command)
263
264    def sites(self):
265        """ Send 'sites' command to get the whole list of known freedb mirrors.
266       
267        sample command:
268            - C{sites}
269        """
270        self.command = "sites"
271        self.sendCommand(self.command)
272
273    def stat(self):
274        """ Send 'stat' command to get the server's status.
275       
276        sample command:
277            - C{stat}
278        """
279        self.command = "stat"
280        self.sendCommand(self.command)
281   
282    def ver(self):
283        """ Send 'ver' command to get the server's version.
284       
285        sample command:
286            - C{ver}
287        """
288        self.command = "ver"
289        self.sendCommand(self.command)
290   
291    def unlink(self, category, discid):
292        """ Send 'unlink' command to delete an entry from database.
293       
294        Only administrative users could succeed issuing this command.
295       
296        sample command:
297            - C{cddb unlink misc f2123610}
298           
299        @type category: string
300        @param category: Category has to be one of the available categories,
301        call L{self.lscat} to know them.
302        """
303        self.command = "unlink"
304        self.sendCommand("cddb", [self.command, category, discid])
305   
306    def handleStatus(self, code, line):
307        """ Handle status response strings.
308       
309        Called from L{self.lineReceived} when it encounters the first line of a
310        a response received from the server.
311       
312        Use I{code} parameter and L{self.command} variable to check if the current
313        command response will have subsequents response lines setting
314        L{self.multi_line} to True. Due to oddities in the protocol you have
315        to remember to set it manually.
316       
317        @type code: int
318        @param code: The response code
319        @type line: string
320        @param line: The first line of incoming data.
321        """
322        raise NotImplementedError
323   
324    def parseResponse(self, response):
325        """ Parse multi line response data.
326       
327        Called from L{self.lineReceived} when multi line response data has
328        ended, so you could extract needed information from the data.
329       
330        @type response: list
331        @param response: Holds the full response data, each item of the list
332        corresponds to a new line in the response.
333        """
334        raise NotImplementedError
335 
336class CDDBPClientFactory(protocol.ClientFactory):
337    protocol = CDDBPClient
338
339    def clientLostConnection(self, connector, reason):
340        print reason
341
342    clientConnectionFailed = clientLostConnection