| 233 | def getRecording(self, chanid, starttime): |
| 234 | """ |
| 235 | Returns a Program object matching the channel id and start time |
| 236 | """ |
| 237 | res = self.backendCommand('QUERY_RECORDING TIMESLOT %d %d' % (chanid, starttime)).split(BACKEND_SEP) |
| 238 | if res[0] == 'ERROR': |
| 239 | return None |
| 240 | else: |
| 241 | return Program(res[1:]) |
| 242 | |
| 243 | def getRecordings(self): |
| 244 | """ |
| 245 | Returns a list of all Program objects which have already recorded |
| 246 | """ |
| 247 | programs = [] |
| 248 | res = self.backendCommand('QUERY_RECORDINGS Play').split('[]:[]') |
| 249 | num_progs = int(res.pop(0)) |
| 250 | log.Msg(DEBUG, '%s total recordings', num_progs) |
| 251 | for i in range(num_progs): |
| 252 | programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) |
| 253 | + PROGRAM_FIELDS])) |
| 254 | return tuple(programs) |
| 255 | |
| 256 | def getCheckfile(self,program): |
| 257 | """ |
| 258 | Returns location of recording in file system |
| 259 | """ |
| 260 | res = self.backendCommand('QUERY_CHECKFILE[]:[]1[]:[]%s' % program.toString()).split(BACKEND_SEP) |
| 261 | if res[0] == 0: |
| 262 | return None |
| 263 | else: |
| 264 | return res[1] |
| 265 | |
| 266 | def deleteRecording(self,program,force=False): |
| 267 | """ |
| 268 | Deletes recording, set 'force' true if file is not available for deletion |
| 269 | Returns the number fewer recordings that exist afterwards |
| 270 | """ |
| 271 | command = 'DELETE_RECORDING' |
| 272 | if force: |
| 273 | command = 'FORCE_DELETE_RECORDING' |
| 274 | return self.backendCommand('%s%s%s' % (command,BACKEND_SEP,program.toString())) |
| 275 | |
| 276 | def forgetRecording(self,program): |
| 277 | """ |
| 278 | Forgets old recording and allows it to be re-recorded |
| 279 | """ |
| 280 | self.backendCommand('FORGET_RECORDING%s%s' % (BACKEND_SEP,program.toString())) |
| 281 | |
| 282 | def deleteFile(self,file,sgroup): |
| 283 | """ |
| 284 | Deletes a file from specified storage group on the connected backend |
| 285 | Takes a relative file path from the root of the storage group, and returns 1 on success |
| 286 | """ |
| 287 | return self.backendCommand('DELETE_FILE%s%s%s%s' % (BACKEND_SEP,file,BACKEND_SEP,sgroup)) |
| 288 | |
| 289 | def getFreeSpace(self,all=False): |
| 290 | """ |
| 291 | Returns a tuple of tuples, in the form: |
| 292 | str hostname |
| 293 | str path |
| 294 | bool is_local |
| 295 | int drive number |
| 296 | int storage group ID |
| 297 | int total space (in KB) |
| 298 | int used space (in KB) |
| 299 | """ |
| 300 | command = 'QUERY_FREE_SPACE' |
| 301 | if all: |
| 302 | command = 'QUERY_FREE_SPACE_LIST' |
| 303 | res = self.backendCommand(command).split(BACKEND_SEP) |
| 304 | dirs = [] |
| 305 | for i in range(0,len(res)/9): |
| 306 | line = [res[i*9]] |
| 307 | line.append(res[i*9+1]) |
| 308 | line.append(bool(int(res[i*9+2]))) |
| 309 | line.append(int(res[i*9+3])) |
| 310 | line.append(int(res[i*9+4])) |
| 311 | line.append(self.joinInt(int(res[i*9+5]),int(res[i*9+6]))) |
| 312 | line.append(self.joinInt(int(res[i*9+7]),int(res[i*9+8]))) |
| 313 | dirs.append(tuple(line)) |
| 314 | return tuple(dirs) |
| 315 | |
| 316 | def getFreeSpaceSummary(self): |
| 317 | """ |
| 318 | Returns a tuple of total space (in KB) and used space (in KB) |
| 319 | """ |
| 320 | res = self.backendCommand('QUERY_FREE_SPACE_SUMMARY').split(BACKEND_SEP) |
| 321 | return (self.joinInt(int(res[0]),int(res[1])),self.joinInt(int(res[2]),int(res[3]))) |
| 322 | |
| 323 | def getLoad(self): |
| 324 | """ |
| 325 | Returns a tuple of the 1, 5, and 15 minute load averages |
| 326 | """ |
| 327 | res = self.backendCommand('QUERY_LOAD').split(BACKEND_SEP) |
| 328 | return (float(res[0]),float(res[1]),float(res[2])) |
| 329 | |
| 330 | def getSGList(self,host,sg,path): |
| 331 | """ |
| 332 | Returns a tuple of directories and files |
| 333 | """ |
| 334 | res = self.backendCommand('QUERY_SG_GETFILELIST%s%s%s%s%s%s' % (BACKEND_SEP,host,BACKEND_SEP,sg,BACKEND_SEP,path)).split(BACKEND_SEP) |
| 335 | if res[0] == 'EMPTY LIST': |
| 336 | return -1 |
| 337 | if res[0] == 'SLAVE UNREACHABLE: ': |
| 338 | return -2 |
| 339 | dirs = [] |
| 340 | files = [] |
| 341 | for entry in res: |
| 342 | type,name = entry.split('::') |
| 343 | if type == 'file': |
| 344 | files.append(name) |
| 345 | if type == 'dir': |
| 346 | dirs.append(name) |
| 347 | return (tuple(dirs),tuple(files)) |
| 348 | |
| 349 | def getSGFile(self,host,sg,path): |
| 350 | """ |
| 351 | Returns a tuple of last modification time and file size |
| 352 | """ |
| 353 | res = self.backendCommand('QUERY_SG_FILEQUERY%s%s%s%s%s%s' % (BACKEND_SEP,host,BACKEND_SEP,sg,BACKEND_SEP,path)).split(BACKEND_SEP) |
| 354 | if res[0] == 'EMPTY LIST': |
| 355 | return -1 |
| 356 | if res[0] == 'SLAVE UNREACHABLE: ': |
| 357 | return -2 |
| 358 | return tuple(res[1:3]) |
| 359 | |
| 360 | def getFrontends(self): |
| 361 | """ |
| 362 | Returns a list of Frontend objects for accessible frontends |
| 363 | """ |
| 364 | cursor = self.db.db.cursor() |
| 365 | cursor.execute("SELECT DISTINCT hostname FROM settings WHERE hostname IS NOT NULL and value='NetworkControlEnabled' and data=1") |
| 366 | frontends = [] |
| 367 | for fehost in cursor.fetchall(): |
| 368 | try: |
| 369 | frontend = self.getFrontend(fehost[0]) |
| 370 | frontends.append(frontend) |
| 371 | except: |
| 372 | print "%s is not a valid frontend" % fehost[0] |
| 373 | cursor.close() |
| 374 | return frontends |
| 375 | |
| 376 | def getFrontend(self,host): |
| 377 | """ |
| 378 | Returns a Frontend object for the specified host |
| 379 | """ |
| 380 | port = self.db.getSetting("NetworkControlPort",host) |
| 381 | return Frontend(host,port) |
| 382 | |
| 383 | def joinInt(self,high,low): |
| 384 | """ |
| 385 | Returns a single long from a pair of signed integers |
| 386 | """ |
| 387 | return (high + (low<0))*2**32 + low |
| 388 | |
| 389 | def splitInt(self,integer): |
| 390 | """ |
| 391 | Returns a pair of signed integers from a single long |
| 392 | """ |
| 393 | return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32 |
| 394 | |
| 395 | class FileTransfer: |
| 396 | """ |
| 397 | A connection to mythbackend intended for file transfers |
| 398 | """ |
| 399 | sockno = None |
| 400 | sockwrite = False |
| 401 | datsock = None |
| 402 | tsize = 2**15 |
| 403 | write = False |
| 404 | |
| 405 | def __init__(self, file, mode): |
| 406 | regex = re.compile('myth://((?P<group>.*)@)?(?P<host>[0-9\.]*)(:(?P<port>[0-9]*))?/(?P<file>.*)') |
| 407 | self.db = MythDB(sys.argv[1:]) |
| 408 | self.comsock = MythTV() |
| 409 | self.sockwrite = write |
| 410 | if isinstance(file, Program): |
| 411 | match = regex.match(file.filename) |
| 412 | self.host = match.group('host') |
| 413 | self.port = int(match.group('port')) |
| 414 | self.filename = match.group('file') |
| 415 | self.sgroup = file.storagegroup |
| 416 | if self.port is None: |
| 417 | self.port = 6543 |
| 418 | elif isinstance(file, tuple): |
| 419 | if len(file) != 3: |
| 420 | log.Msg(CRITICAL, 'Incorrect FileTransfer() input size') |
| 421 | sys.exit(1) |
| 422 | else: |
| 423 | self.host = file[0] |
| 424 | self.port = int(self.db.getSetting('BackendServerPort',self.host)) |
| 425 | self.filename = file[1] |
| 426 | self.sgroup = file[2] |
| 427 | elif isinstance(file, str): |
| 428 | match = regex.match(file) |
| 429 | if match is None: |
| 430 | log.Msg(CRITICAL, 'Incorrect FileTransfer() input string: %s' % file) |
| 431 | sys.exit(1) |
| 432 | self.sgroup = match.group('group') |
| 433 | self.host = match.group('host') |
| 434 | self.port = int(match.group('port')) |
| 435 | self.filename = match.group('file') |
| 436 | if self.sgroup is None: |
| 437 | self.sgroup = '' |
| 438 | if self.port is None: |
| 439 | self.port = 6543 |
| 440 | else: |
| 441 | log.Msg(CRITICAL, 'Improper input to FileTransfer()') |
| 442 | sys.exit(1) |
| 443 | |
| 444 | try: |
| 445 | self.datsock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 446 | self.datsock.settimeout(10) |
| 447 | self.datsock.connect((self.host, self.port)) |
| 448 | res = self.send('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP) |
| 449 | if res[0] == 'REJECT': |
| 450 | log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION) |
| 451 | sys.exit(1) |
| 452 | res = self.send('ANN FileTransfer %s %d %d %d%s%s%s%s' % (socket.gethostname(), write, False, -1, BACKEND_SEP, self.filename, BACKEND_SEP, self.sgroup)) |
| 453 | if res.split(BACKEND_SEP)[0] != 'OK': |
| 454 | log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res) |
| 455 | else: |
| 456 | log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.host, self.port) |
| 457 | sp = res.split(BACKEND_SEP) |
| 458 | self.sockno = int(sp[1]) |
| 459 | self.pos = 0 |
| 460 | self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3]) |
| 461 | |
| 462 | except socket.error, e: |
| 463 | log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.host, self.port) |
| 464 | sys.exit(1) |
| 465 | |
| 466 | def __del__(self): |
| 467 | if self.sockno: |
| 468 | self.comsock.backendCommand('QUERY_FILETRANSFER %d%sDONE' % (self.sockno, BACKEND_SEP)) |
| 469 | if self.datsock: |
| 470 | self.datsock.shutdown(1) |
| 471 | self.datsock.close() |
| 472 | |
| 473 | def send(self,data): |
| 474 | command = '%-8d%s' % (len(data), data) |
| 475 | log.Msg(DEBUG, 'Sending command: %s', command) |
| 476 | self.datsock.send(command) |
| 477 | print 'sending command: %s' % command |
| 478 | return self.recv() |
| 479 | |
| 480 | def recv(self): |
| 481 | data = self.datsock.recv(8) |
| 482 | try: |
| 483 | length = int(data) |
| 484 | except: |
| 485 | return '' |
| 486 | data = [] |
| 487 | while length > 0: |
| 488 | chunk = self.datsock.recv(length) |
| 489 | length = length - len(chunk) |
| 490 | data.append(chunk) |
| 491 | return ''.join(data) |
| 492 | |
| 493 | def tell(self): |
| 494 | """ |
| 495 | Return the current offset from the beginning of the file |
| 496 | """ |
| 497 | return self.pos |
| 498 | |
| 499 | def close(self): |
| 500 | """ |
| 501 | Close the data transfer socket |
| 502 | """ |
| 503 | self.__del__() |
| 504 | |
| 505 | def rewind(self): |
| 506 | """ |
| 507 | Seek back to the start of the file |
| 508 | """ |
| 509 | self.seek(0) |
| 510 | |
| 511 | def read(self, size): |
| 512 | """ |
| 513 | Read a block of data, requests over 64KB will be buffered internally |
| 514 | """ |
| 515 | if self.sockwrite: |
| 516 | print 'Error: attempting to read from a write-only socket' |
| 517 | if size == 0: |
| 518 | return '' |
| 519 | if size > self.size - self.pos: |
| 520 | size = self.size - self.pos |
| 521 | csize = size |
| 522 | rsize = 0 |
| 523 | if csize > self.tsize: |
| 524 | csize = self.tsize |
| 525 | rsize = size - csize |
| 526 | |
| 527 | res = self.comsock.backendCommand('QUERY_FILETRANSFER %d%sREQUEST_BLOCK%s%d' % (self.sockno,BACKEND_SEP,BACKEND_SEP,csize)) |
| 528 | self.pos = self.pos + int(res) |
| 529 | # if int(res) == csize: |
| 530 | # if csize < size: |
| 531 | # self.tsize += 8192 |
| 532 | # else: |
| 533 | # self.tsize -= 8192 |
| 534 | # rsize = size - int(res) |
| 535 | # print 'resizing buffer to %d' % self.tsize |
| 536 | |
| 537 | return self.datsock.recv(int(res)) + self.read(rsize) |
| 538 | |
| 539 | def write(self, data): |
| 540 | """ |
| 541 | Write a block of data, requests over 64KB will be buffered internally |
| 542 | """ |
| 543 | if not self.sockwrite: |
| 544 | print 'Error: attempting to write to a read-only socket' |
| 545 | size = len(data) |
| 546 | buff = '' |
| 547 | print 'writing %d bytes to socket' % size |
| 548 | if size == 0: |
| 549 | return |
| 550 | if size > self.tsize: |
| 551 | size = self.tsize |
| 552 | buff = data[size:] |
| 553 | data = data[:size] |
| 554 | print self.datsock.send(data) |
| 555 | print self.comsock.backendCommand('QUERY_FILETRANSFER %d%sWRITE_BLOCK%s%d' % (self.sockno,BACKEND_SEP,BACKEND_SEP,size)) |
| 556 | self.write(buff) |
| 557 | return |
| 558 | |
| 559 | |
| 560 | def seek(self, offset, whence=0): |
| 561 | """ |
| 562 | Seek 'offset' number of bytes |
| 563 | whence==0 - from start of file |
| 564 | whence==1 - from current position |
| 565 | """ |
| 566 | if whence == 0: |
| 567 | if offset < 0: |
| 568 | offset = 0 |
| 569 | if offset > self.size: |
| 570 | offset = self.size |
| 571 | elif whence == 1: |
| 572 | if offset + self.pos < 0: |
| 573 | offset = -self.pos |
| 574 | if offset + self.pos > self.size: |
| 575 | offset = self.size - self.pos |
| 576 | elif whence == 2: |
| 577 | if offset > 0: |
| 578 | offset = 0 |
| 579 | if offset < -self.size: |
| 580 | offset = -self.size |
| 581 | else: |
| 582 | log.Msg(CRITICAL, 'Whence can only be 0, 1, or 2') |
| 583 | |
| 584 | curhigh,curlow = self.comsock.splitInt(self.pos) |
| 585 | offhigh,offlow = self.comsock.splitInt(offset) |
| 586 | |
| 587 | res = self.comsock.backendCommand('QUERY_FILETRANSFER %d%sSEEK%s%d%s%d%s%d%s%d%s%d' % (self.sockno, BACKEND_SEP,BACKEND_SEP,offhigh,BACKEND_SEP,offlow,BACKEND_SEP,whence,BACKEND_SEP,curhigh,BACKEND_SEP,curlow)).split(BACKEND_SEP) |
| 588 | self.pos = (int(res[0]) + (int(res[1])<0))*2**32 + int(res[1]) |
| 589 | |
| 590 | |
| 591 | class Frontend: |
| 592 | isConnected = False |
| 593 | socket = None |
| 594 | host = None |
| 595 | port = None |
| 596 | |
| 597 | def __init__(self, host, port): |
| 598 | self.host = host |
| 599 | self.port = int(port) |
| 600 | self.connect() |
| 601 | self.disconnect() |
| 602 | |
| 603 | def __del__(self): |
| 604 | if self.isConnected: |
| 605 | self.disconnect() |
| 606 | |
| 607 | def __repr__(self): |
| 608 | return "%s@%d" % (self.host, self.port) |
| 609 | |
| 610 | def __str__(self): |
| 611 | return "%s@%d" % (self.host, self.port) |
| 612 | |
| 613 | def connect(self): |
| 614 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 615 | self.socket.settimeout(10) |
| 616 | self.socket.connect((self.host, self.port)) |
| 617 | if self.recv()[:28] != "MythFrontend Network Control": |
| 618 | self.socket.close() |
| 619 | self.socket = None |
| 620 | raise Exception('FrontendConnect','Connected socket does not belong to a mythfrontend') |
| 621 | self.isConnected = True |
| 622 | |
| 623 | def disconnect(self): |
| 624 | self.send("exit") |
| 625 | self.socket.close() |
| 626 | self.socket = None |
| 627 | self.isConnected = False |
| 628 | |
| 629 | def send(self,command): |
| 630 | if not self.isConnected: |
| 631 | self.connect() |
| 632 | self.socket.send("%s\n" % command) |
| 633 | |
| 634 | def recv(self,curstr=""): |
| 635 | def subrecv(self,curstr=""): |
| 636 | try: |
| 637 | curstr += self.socket.recv(100) |
| 638 | except: |
| 639 | return None |
| 640 | if curstr[-4:] != '\r\n# ': |
| 641 | curstr = subrecv(self,curstr) |
| 642 | return curstr |
| 643 | return subrecv(self)[:-4] |
| 644 | |
| 645 | def sendJump(self,jumppoint): |
| 646 | """ |
| 647 | Sends jumppoint to frontend |
| 648 | """ |
| 649 | self.send("jump %s" % jumppoint) |
| 650 | if self.recv() == 'OK': |
| 651 | return 0 |
| 652 | else: |
| 653 | return 1 |
| 654 | |
| 655 | def getJump(self): |
| 656 | """ |
| 657 | Returns a tuple containing available jumppoints |
| 658 | """ |
| 659 | self.send("help jump") |
| 660 | res = self.recv().split('\r\n')[3:-1] |
| 661 | points = [] |
| 662 | for point in res: |
| 663 | spoint = point.split(' - ') |
| 664 | points.append((spoint[0].rstrip(),spoint[1])) |
| 665 | return tuple(points) |
| 666 | |
| 667 | def sendKey(self,key): |
| 668 | """ |
| 669 | Sends keycode to connected frontend |
| 670 | """ |
| 671 | self.send("key %s" % key) |
| 672 | if self.recv() == 'OK': |
| 673 | return 0 |
| 674 | else: |
| 675 | return 1 |
| 676 | |
| 677 | def getKey(self): |
| 678 | """ |
| 679 | Returns a tuple containing available special keys |
| 680 | """ |
| 681 | self.send("help key") |
| 682 | res = self.recv().split('\r\n')[4] |
| 683 | keys = [] |
| 684 | for key in res.split(','): |
| 685 | keys.append(key.strip()) |
| 686 | return tuple(keys) |
| 687 | |
| 688 | def sendQuery(self,query): |
| 689 | """ |
| 690 | Returns query from connected frontend |
| 691 | """ |
| 692 | self.send("query %s" % query) |
| 693 | return self.recv() |
| 694 | |
| 695 | def getQuery(self): |
| 696 | """ |
| 697 | Returns a tuple containing available queries |
| 698 | """ |
| 699 | self.send("help query") |
| 700 | res = self.recv().split('\r\n')[:-1] |
| 701 | queries = [] |
| 702 | tmpstr = "" |
| 703 | for query in res: |
| 704 | tmpstr += query |
| 705 | squery = tmpstr.split(' - ') |
| 706 | if len(squery) == 2: |
| 707 | tmpstr = "" |
| 708 | queries.append((squery[0].rstrip().lstrip('query '),squery[1])) |
| 709 | return tuple(queries) |
| 710 | |
| 711 | def sendPlay(self,play): |
| 712 | """ |
| 713 | Send playback command to connected frontend |
| 714 | """ |
| 715 | self.send("play %s" % play) |
| 716 | if self.recv() == 'OK': |
| 717 | return 0 |
| 718 | else: |
| 719 | return 1 |
| 720 | |
| 721 | def getPlay(self): |
| 722 | """ |
| 723 | Returns a tuple containing available playback commands |
| 724 | """ |
| 725 | self.send("help play") |
| 726 | res = self.recv().split('\r\n')[:-1] |
| 727 | plays = [] |
| 728 | tmpstr = "" |
| 729 | for play in res: |
| 730 | tmpstr += play |
| 731 | splay = tmpstr.split(' - ') |
| 732 | if len(splay) == 2: |
| 733 | tmpstr = "" |
| 734 | plays.append((splay[0].rstrip().lstrip('play '),splay[1])) |
| 735 | return tuple(plays) |
| 736 | |
| 737 | |
| 738 | |