| 1 | """CDDB protocol implementation. |
|---|
| 2 | |
|---|
| 3 | Future Plans: |
|---|
| 4 | - add server protocol |
|---|
| 5 | - add not readonly client commands |
|---|
| 6 | """ |
|---|
| 7 | |
|---|
| 8 | from twisted.internet import reactor, protocol |
|---|
| 9 | from twisted.protocols import basic |
|---|
| 10 | |
|---|
| 11 | TERMINATING_MARKER = "." |
|---|
| 12 | MAX_PROTO = "6" |
|---|
| 13 | |
|---|
| 14 | # response codes |
|---|
| 15 | OK = 200 |
|---|
| 16 | EXACT_MATCH = OK |
|---|
| 17 | CURRENT_PROTO_LEVEL = OK |
|---|
| 18 | OK_READ_ONLY = 201 |
|---|
| 19 | OK_PROTO_LEVEL_CHANGED = 201 |
|---|
| 20 | NO_MATCH_FOUND = 202 |
|---|
| 21 | OK_FURTHER_DATA_FOLLOWS = 210 |
|---|
| 22 | MULTIPLE_INEXACT_MATCHES = 211 |
|---|
| 23 | OK_VERSION_INFO_FOLLOWS = 211 |
|---|
| 24 | QUIT_OK_CLOSING = 230 |
|---|
| 25 | NOT_AVAILABLE = 401 |
|---|
| 26 | UNLINK_PERMISSION_DENIED = 401 |
|---|
| 27 | FILE_ACCESS_FAILED = 402 |
|---|
| 28 | SERVER_ERROR = 402 |
|---|
| 29 | HANDSHAKE_ALREADY_DONE = 402 |
|---|
| 30 | DB_ENTRY_CORRUPTED = 403 |
|---|
| 31 | CGI_ENV_ERROR = 408 |
|---|
| 32 | HANDSHAKE_NOT_DONE = 409 |
|---|
| 33 | HANDSHAKE_FAILED = 431 |
|---|
| 34 | CONNECT_PERMISSION_DENIED = 432 |
|---|
| 35 | USER_LIMIT_EXCEED = 433 |
|---|
| 36 | SYSTEM_LOAD_TOO_HIGH = 434 |
|---|
| 37 | SYNTAX_ERROR = 500 |
|---|
| 38 | UNKOWN_COMMAND = SYNTAX_ERROR |
|---|
| 39 | UNIMPLEMENTED_COMMAND = SYNTAX_ERROR |
|---|
| 40 | INVALID_DATA = 501 |
|---|
| 41 | SAME_PROTO_LEVEL = 502 |
|---|
| 42 | QUIT_ERROR_CLOSING = 530 |
|---|
| 43 | SERVER_TIMEOUT = 530 |
|---|
| 44 | |
|---|
| 45 | # proper codes for each supported command |
|---|
| 46 | login_codes = [OK, OK_READ_ONLY, CONNECT_PERMISSION_DENIED, USER_LIMIT_EXCEED, |
|---|
| 47 | SYSTEM_LOAD_TOO_HIGH] |
|---|
| 48 | hello_codes = [OK, HANDSHAKE_FAILED, HANDSHAKE_ALREADY_DONE] |
|---|
| 49 | lscat_codes = [OK_FURTHER_DATA_FOLLOWS] |
|---|
| 50 | query_codes = [EXACT_MATCH, MULTIPLE_INEXACT_MATCHES, NO_MATCH_FOUND, |
|---|
| 51 | DB_ENTRY_CORRUPTED, HANDSHAKE_NOT_DONE] |
|---|
| 52 | read_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE, SERVER_ERROR, |
|---|
| 53 | DB_ENTRY_CORRUPTED, HANDSHAKE_NOT_DONE] |
|---|
| 54 | discid_codes = [OK, SYNTAX_ERROR] |
|---|
| 55 | help_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE] |
|---|
| 56 | motd_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE] |
|---|
| 57 | proto_codes = [CURRENT_PROTO_LEVEL, OK_PROTO_LEVEL_CHANGED, |
|---|
| 58 | INVALID_DATA, SAME_PROTO_LEVEL] |
|---|
| 59 | quit_codes = [QUIT_OK_CLOSING, QUIT_ERROR_CLOSING] |
|---|
| 60 | sites_codes = [OK_FURTHER_DATA_FOLLOWS, NOT_AVAILABLE] |
|---|
| 61 | stat_codes = [OK_FURTHER_DATA_FOLLOWS] |
|---|
| 62 | ver_codes = [OK, OK_FURTHER_DATA_FOLLOWS] |
|---|
| 63 | misc_codes = [SERVER_ERROR, CGI_ENV_ERROR, SYNTAX_ERROR, |
|---|
| 64 | UNKOWN_COMMAND, UNIMPLEMENTED_COMMAND, SERVER_TIMEOUT] |
|---|
| 65 | unlink_codes = [OK, UNLINK_PERMISSION_DENIED, FILE_ACCESS_FAILED, INVALID_DATA] |
|---|
| 66 | |
|---|
| 67 | # helper codes list |
|---|
| 68 | successful_codes = [200, 201, 230] |
|---|
| 69 | multi_line_codes = [210, 211] |
|---|
| 70 | failure_error_codes = [431, 432, 433, 434] |
|---|
| 71 | |
|---|
| 72 | # dictionary to map codes |
|---|
| 73 | RESPONSE_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 | |
|---|
| 91 | class 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 | |
|---|
| 336 | class CDDBPClientFactory(protocol.ClientFactory): |
|---|
| 337 | protocol = CDDBPClient |
|---|
| 338 | |
|---|
| 339 | def clientLostConnection(self, connector, reason): |
|---|
| 340 | print reason |
|---|
| 341 | |
|---|
| 342 | clientConnectionFailed = clientLostConnection |
|---|