| | 232 | def getRecording(self, chanid, starttime): |
| | 233 | """ |
| | 234 | Returns a Program object matching the channel id and start time |
| | 235 | """ |
| | 236 | res = self.backendCommand('QUERY_RECORDING TIMESLOT %d %d' % (chanid, starttime)).split(BACKEND_SEP) |
| | 237 | if res[0] == 'ERROR': |
| | 238 | return None |
| | 239 | else: |
| | 240 | return Program(res[1:]) |
| | 241 | |
| | 242 | def getRecordings(self): |
| | 243 | """ |
| | 244 | Returns a list of all Program objects which have already recorded |
| | 245 | """ |
| | 246 | programs = [] |
| | 247 | res = self.backendCommand('QUERY_RECORDINGS Play').split('[]:[]') |
| | 248 | num_progs = int(res.pop(0)) |
| | 249 | log.Msg(DEBUG, '%s total recordings', num_progs) |
| | 250 | for i in range(num_progs): |
| | 251 | programs.append(Program(res[i * PROGRAM_FIELDS:(i * PROGRAM_FIELDS) |
| | 252 | + PROGRAM_FIELDS])) |
| | 253 | return tuple(programs) |
| | 254 | |
| | 255 | def getCheckfile(self,program): |
| | 256 | """ |
| | 257 | Returns location of recording in file system |
| | 258 | """ |
| | 259 | res = self.backendCommand('QUERY_CHECKFILE[]:[]1[]:[]%s' % program.toString()).split(BACKEND_SEP) |
| | 260 | if res[0] == 0: |
| | 261 | return None |
| | 262 | else: |
| | 263 | return res[1] |
| | 264 | |
| | 265 | def deleteRecording(self,program,force=False): |
| | 266 | """ |
| | 267 | Deletes recording, set 'force' true if file is not available for deletion |
| | 268 | Returns the number fewer recordings that exist afterwards |
| | 269 | """ |
| | 270 | command = 'DELETE_RECORDING' |
| | 271 | if force: |
| | 272 | command = 'FORCE_DELETE_RECORDING' |
| | 273 | return self.backendCommand('%s%s%s' % (command,BACKEND_SEP,program.toString())) |
| | 274 | |
| | 275 | def forgetRecording(self,program): |
| | 276 | """ |
| | 277 | Forgets old recording and allows it to be re-recorded |
| | 278 | """ |
| | 279 | self.backendCommand('FORGET_RECORDING%s%s' % (BACKEND_SEP,program.toString())) |
| | 280 | |
| | 281 | def getFreeSpace(self,all=False): |
| | 282 | """ |
| | 283 | Returns a tuple of tuples, in the form: |
| | 284 | str hostname |
| | 285 | str path |
| | 286 | bool is_local |
| | 287 | int drive number |
| | 288 | int storage group ID |
| | 289 | int total space (in KB) |
| | 290 | int used space (in KB) |
| | 291 | """ |
| | 292 | command = 'QUERY_FREE_SPACE' |
| | 293 | if all: |
| | 294 | command = 'QUERY_FREE_SPACE_LIST' |
| | 295 | res = self.backendCommand(command).split(BACKEND_SEP) |
| | 296 | dirs = [] |
| | 297 | for i in range(0,len(res)/9): |
| | 298 | line = [res[i*9]] |
| | 299 | line.append(res[i*9+1]) |
| | 300 | line.append(bool(int(res[i*9+2]))) |
| | 301 | line.append(int(res[i*9+3])) |
| | 302 | line.append(int(res[i*9+4])) |
| | 303 | line.append(self.joinInt(int(res[i*9+5]),int(res[i*9+6]))) |
| | 304 | line.append(self.joinInt(int(res[i*9+7]),int(res[i*9+8]))) |
| | 305 | dirs.append(tuple(line)) |
| | 306 | return tuple(dirs) |
| | 307 | |
| | 308 | def getFreeSpaceSummary(self): |
| | 309 | """ |
| | 310 | Returns a tuple of total space (in KB) and used space (in KB) |
| | 311 | """ |
| | 312 | res = self.backendCommand('QUERY_FREE_SPACE_SUMMARY').split(BACKEND_SEP) |
| | 313 | return (self.joinInt(int(res[0]),int(res[1])),self.joinInt(int(res[2]),int(res[3]))) |
| | 314 | |
| | 315 | def getLoad(self): |
| | 316 | """ |
| | 317 | Returns a tuple of the 1, 5, and 15 minute load averages |
| | 318 | """ |
| | 319 | res = self.backendCommand('QUERY_LOAD').split(BACKEND_SEP) |
| | 320 | return (float(res[0]),float(res[1]),float(res[2])) |
| | 321 | |
| | 322 | def getFrontends(self): |
| | 323 | """ |
| | 324 | Returns a list of Frontend objects for accessible frontends |
| | 325 | """ |
| | 326 | cursor = self.db.db.cursor() |
| | 327 | cursor.execute("SELECT DISTINCT hostname FROM settings WHERE hostname IS NOT NULL and value='NetworkControlEnabled' and data=1") |
| | 328 | frontends = [] |
| | 329 | for fehost in cursor.fetchall(): |
| | 330 | try: |
| | 331 | frontend = self.getFrontend(fehost[0]) |
| | 332 | frontends.append(frontend) |
| | 333 | except: |
| | 334 | print "%s is not a valid frontend" % fehost[0] |
| | 335 | cursor.close() |
| | 336 | return frontends |
| | 337 | |
| | 338 | def getFrontend(self,host): |
| | 339 | """ |
| | 340 | Returns a Frontend object for the specified host |
| | 341 | """ |
| | 342 | port = self.db.getSetting("NetworkControlPort",host) |
| | 343 | return Frontend(host,port) |
| | 344 | |
| | 345 | def joinInt(self,high,low): |
| | 346 | """ |
| | 347 | Returns a single long from a pair of signed integers |
| | 348 | """ |
| | 349 | return (high + (low<0))*2**32 + low |
| | 350 | |
| | 351 | def splitInt(self,integer): |
| | 352 | """ |
| | 353 | Returns a pair of signed integers from a single long |
| | 354 | """ |
| | 355 | return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32 |
| | 356 | |
| | 357 | class FileTransfer: |
| | 358 | """ |
| | 359 | A connection to mythbackend intended for file transfers |
| | 360 | """ |
| | 361 | sockno = None |
| | 362 | socket = None |
| | 363 | tsize = 2**16 |
| | 364 | |
| | 365 | def __init__(self, file, parent=None): |
| | 366 | self.db = MythDB(sys.argv[1:]) |
| | 367 | if not isinstance(parent, MythTV): |
| | 368 | self.parent = MythTV() |
| | 369 | else: |
| | 370 | self.parent = parent |
| | 371 | if isinstance(file, Program): |
| | 372 | self.master_host, self.master_port = file.filename.split('/')[-2].split(':') |
| | 373 | self.master_port = int(self.master_port) |
| | 374 | self.filename = file.filename.split('/')[-1] |
| | 375 | self.sgroup = file.storagegroup |
| | 376 | elif isinstance(file, tuple): |
| | 377 | if len(file) != 3: |
| | 378 | log.Msg(CRITICAL, 'Incorrect FileTransfer() input size') |
| | 379 | sys.exit(1) |
| | 380 | else: |
| | 381 | self.master_host = file[0] |
| | 382 | self.master_port = int(self.db.getSetting('BackendServerPort',self.master_host)) |
| | 383 | self.filename = file[1] |
| | 384 | self.sgroup = file[2] |
| | 385 | else: |
| | 386 | log.Msg(CRITICAL, 'Improper input to FileTransfer()') |
| | 387 | sys.exit(1) |
| | 388 | |
| | 389 | try: |
| | 390 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| | 391 | self.socket.settimeout(10) |
| | 392 | self.socket.connect((self.master_host, self.master_port)) |
| | 393 | res = self.send('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP) |
| | 394 | if res[0] == 'REJECT': |
| | 395 | log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION) |
| | 396 | sys.exit(1) |
| | 397 | |
| | 398 | res = self.send('ANN FileTransfer %s%s%s%s%s' % (socket.gethostname(), BACKEND_SEP, self.filename, BACKEND_SEP, self.sgroup)) |
| | 399 | if res.split(BACKEND_SEP)[0] != 'OK': |
| | 400 | log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res) |
| | 401 | else: |
| | 402 | log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.master_host, self.master_port) |
| | 403 | sp = res.split(BACKEND_SEP) |
| | 404 | self.sockno = int(sp[1]) |
| | 405 | self.pos = 0 |
| | 406 | self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3]) |
| | 407 | |
| | 408 | except socket.error, e: |
| | 409 | log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.master_host, self.master_port) |
| | 410 | sys.exit(1) |
| | 411 | |
| | 412 | def __del__(self): |
| | 413 | if self.sockno: |
| | 414 | self.parent.backendCommand('QUERY_FILETRANSFER %d%sDONE' % (self.sockno, BACKEND_SEP)) |
| | 415 | if self.socket: |
| | 416 | self.socket.shutdown(1) |
| | 417 | self.socket.close() |
| | 418 | |
| | 419 | def send(self,data): |
| | 420 | command = '%-8d%s' % (len(data), data) |
| | 421 | log.Msg(DEBUG, 'Sending command: %s', command) |
| | 422 | self.socket.send(command) |
| | 423 | return self.recv() |
| | 424 | |
| | 425 | def recv(self): |
| | 426 | data = self.socket.recv(8) |
| | 427 | try: |
| | 428 | length = int(data) |
| | 429 | except: |
| | 430 | return '' |
| | 431 | data = [] |
| | 432 | while length > 0: |
| | 433 | chunk = self.socket.recv(length) |
| | 434 | length = length - len(chunk) |
| | 435 | data.append(chunk) |
| | 436 | return ''.join(data) |
| | 437 | |
| | 438 | def tell(self): |
| | 439 | """ |
| | 440 | Return the current offset from the beginning of the file |
| | 441 | """ |
| | 442 | return self.pos |
| | 443 | |
| | 444 | def close(self): |
| | 445 | """ |
| | 446 | Close the data transfer socket |
| | 447 | """ |
| | 448 | self.__del__() |
| | 449 | |
| | 450 | def rewind(self): |
| | 451 | """ |
| | 452 | Seek back to the start of the file |
| | 453 | """ |
| | 454 | self.seek(0) |
| | 455 | |
| | 456 | def read(self, size): |
| | 457 | """ |
| | 458 | Read a block of data, requests over 64KB will be buffered internally |
| | 459 | """ |
| | 460 | if size == 0: |
| | 461 | return '' |
| | 462 | if size > self.size - self.pos: |
| | 463 | size = self.size - self.pos |
| | 464 | csize = size |
| | 465 | rsize = 0 |
| | 466 | if csize > self.tsize: |
| | 467 | csize = self.tsize |
| | 468 | rsize = size - csize |
| | 469 | |
| | 470 | res = self.parent.backendCommand('QUERY_FILETRANSFER %d%sREQUEST_BLOCK%s%d' % (self.sockno,BACKEND_SEP,BACKEND_SEP,csize)) |
| | 471 | self.pos = self.pos + int(res) |
| | 472 | # if int(res) == csize: |
| | 473 | # if csize < size: |
| | 474 | # self.tsize += 8192 |
| | 475 | # else: |
| | 476 | # self.tsize -= 8192 |
| | 477 | # rsize = size - int(res) |
| | 478 | # print 'resizing buffer to %d' % self.tsize |
| | 479 | |
| | 480 | return self.socket.recv(int(res)) + self.read(rsize) |
| | 481 | |
| | 482 | def seek(self, offset, whence=0): |
| | 483 | """ |
| | 484 | Seek 'offset' number of bytes |
| | 485 | whence==0 - from start of file |
| | 486 | whence==1 - from current position |
| | 487 | """ |
| | 488 | if whence == 0: |
| | 489 | if offset < 0: |
| | 490 | offset = 0 |
| | 491 | if offset > self.size: |
| | 492 | offset = self.size |
| | 493 | elif whence == 1: |
| | 494 | if offset + self.pos < 0: |
| | 495 | offset = -self.pos |
| | 496 | if offset + self.pos > self.size: |
| | 497 | offset = self.size - self.pos |
| | 498 | elif whence == 2: |
| | 499 | if offset > 0: |
| | 500 | offset = 0 |
| | 501 | if offset < -self.size: |
| | 502 | offset = -self.size |
| | 503 | else: |
| | 504 | log.Msg(CRITICAL, 'Whence can only be 0, 1, or 2') |
| | 505 | |
| | 506 | curhigh,curlow = self.parent.splitInt(self.pos) |
| | 507 | offhigh,offlow = self.parent.splitInt(offset) |
| | 508 | |
| | 509 | res = self.parent.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) |
| | 510 | self.pos = (int(res[0]) + (int(res[1])<0))*2**32 + int(res[1]) |
| | 511 | |
| | 512 | |
| | 513 | class Frontend: |
| | 514 | isConnected = False |
| | 515 | socket = None |
| | 516 | host = None |
| | 517 | port = None |
| | 518 | |
| | 519 | def __init__(self, host, port): |
| | 520 | self.host = host |
| | 521 | self.port = int(port) |
| | 522 | self.connect() |
| | 523 | self.disconnect() |
| | 524 | |
| | 525 | def __del__(self): |
| | 526 | if self.isConnected: |
| | 527 | self.disconnect() |
| | 528 | |
| | 529 | def __repr__(self): |
| | 530 | return "%s@%d" % (self.host, self.port) |
| | 531 | |
| | 532 | def __str__(self): |
| | 533 | return "%s@%d" % (self.host, self.port) |
| | 534 | |
| | 535 | def connect(self): |
| | 536 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| | 537 | self.socket.settimeout(10) |
| | 538 | self.socket.connect((self.host, self.port)) |
| | 539 | if self.recv()[:28] != "MythFrontend Network Control": |
| | 540 | self.socket.close() |
| | 541 | self.socket = None |
| | 542 | raise Exception('FrontendConnect','Connected socket does not belong to a mythfrontend') |
| | 543 | self.isConnected = True |
| | 544 | |
| | 545 | def disconnect(self): |
| | 546 | self.send("exit") |
| | 547 | self.socket.close() |
| | 548 | self.socket = None |
| | 549 | self.isConnected = False |
| | 550 | |
| | 551 | def send(self,command): |
| | 552 | if not self.isConnected: |
| | 553 | self.connect() |
| | 554 | self.socket.send("%s\n" % command) |
| | 555 | |
| | 556 | def recv(self,curstr=""): |
| | 557 | def subrecv(self,curstr=""): |
| | 558 | try: |
| | 559 | curstr += self.socket.recv(100) |
| | 560 | except: |
| | 561 | return None |
| | 562 | if curstr[-4:] != '\r\n# ': |
| | 563 | curstr = subrecv(self,curstr) |
| | 564 | return curstr |
| | 565 | return subrecv(self)[:-4] |
| | 566 | |
| | 567 | def sendJump(self,jumppoint): |
| | 568 | """ |
| | 569 | Sends jumppoint to frontend |
| | 570 | """ |
| | 571 | self.send("jump %s" % jumppoint) |
| | 572 | if self.recv() == 'OK': |
| | 573 | return 0 |
| | 574 | else: |
| | 575 | return 1 |
| | 576 | |
| | 577 | def getJump(self): |
| | 578 | """ |
| | 579 | Returns a tuple containing available jumppoints |
| | 580 | """ |
| | 581 | self.send("help jump") |
| | 582 | res = self.recv().split('\r\n')[3:-1] |
| | 583 | points = [] |
| | 584 | for point in res: |
| | 585 | spoint = point.split(' - ') |
| | 586 | points.append((spoint[0].rstrip(),spoint[1])) |
| | 587 | return tuple(points) |
| | 588 | |
| | 589 | def sendKey(self,key): |
| | 590 | """ |
| | 591 | Sends keycode to connected frontend |
| | 592 | """ |
| | 593 | self.send("key %s" % key) |
| | 594 | if self.recv() == 'OK': |
| | 595 | return 0 |
| | 596 | else: |
| | 597 | return 1 |
| | 598 | |
| | 599 | def getKey(self): |
| | 600 | """ |
| | 601 | Returns a tuple containing available special keys |
| | 602 | """ |
| | 603 | self.send("help key") |
| | 604 | res = self.recv().split('\r\n')[4] |
| | 605 | keys = [] |
| | 606 | for key in res.split(','): |
| | 607 | keys.append(key.strip()) |
| | 608 | return tuple(keys) |
| | 609 | |
| | 610 | def sendQuery(self,query): |
| | 611 | """ |
| | 612 | Returns query from connected frontend |
| | 613 | """ |
| | 614 | self.send("query %s" % query) |
| | 615 | return self.recv() |
| | 616 | |
| | 617 | def getQuery(self): |
| | 618 | """ |
| | 619 | Returns a tuple containing available queries |
| | 620 | """ |
| | 621 | self.send("help query") |
| | 622 | res = self.recv().split('\r\n')[:-1] |
| | 623 | queries = [] |
| | 624 | tmpstr = "" |
| | 625 | for query in res: |
| | 626 | tmpstr += query |
| | 627 | squery = tmpstr.split(' - ') |
| | 628 | if len(squery) == 2: |
| | 629 | tmpstr = "" |
| | 630 | queries.append((squery[0].rstrip().lstrip('query '),squery[1])) |
| | 631 | return tuple(queries) |
| | 632 | |
| | 633 | def sendPlay(self,play): |
| | 634 | """ |
| | 635 | Send playback command to connected frontend |
| | 636 | """ |
| | 637 | self.send("play %s" % play) |
| | 638 | if self.recv() == 'OK': |
| | 639 | return 0 |
| | 640 | else: |
| | 641 | return 1 |
| | 642 | |
| | 643 | def getPlay(self): |
| | 644 | """ |
| | 645 | Returns a tuple containing available playback commands |
| | 646 | """ |
| | 647 | self.send("help play") |
| | 648 | res = self.recv().split('\r\n')[:-1] |
| | 649 | plays = [] |
| | 650 | tmpstr = "" |
| | 651 | for play in res: |
| | 652 | tmpstr += play |
| | 653 | splay = tmpstr.split(' - ') |
| | 654 | if len(splay) == 2: |
| | 655 | tmpstr = "" |
| | 656 | plays.append((splay[0].rstrip().lstrip('play '),splay[1])) |
| | 657 | return tuple(plays) |
| | 658 | |
| | 659 | |
| | 660 | |
| | 747 | def toString(self): |
| | 748 | string = self.title |
| | 749 | string += BACKEND_SEP + self.subtitle |
| | 750 | string += BACKEND_SEP + self.description |
| | 751 | string += BACKEND_SEP + self.category |
| | 752 | if self.chanid: |
| | 753 | string += BACKEND_SEP + str(self.chanid) |
| | 754 | else: |
| | 755 | string += BACKEND_SEP |
| | 756 | string += BACKEND_SEP + self.channum |
| | 757 | string += BACKEND_SEP + self.callsign |
| | 758 | string += BACKEND_SEP + self.channame |
| | 759 | string += BACKEND_SEP + self.filename |
| | 760 | string += BACKEND_SEP + str(self.fs_high) |
| | 761 | string += BACKEND_SEP + str(self.fs_low) |
| | 762 | string += BACKEND_SEP + str(int(mktime(self.starttime.timetuple()))) |
| | 763 | string += BACKEND_SEP + str(int(mktime(self.endtime.timetuple()))) |
| | 764 | string += BACKEND_SEP + str(self.duplicate) |
| | 765 | string += BACKEND_SEP + str(self.shareable) |
| | 766 | string += BACKEND_SEP + str(self.findid) |
| | 767 | string += BACKEND_SEP + self.hostname |
| | 768 | string += BACKEND_SEP + str(self.sourceid) |
| | 769 | string += BACKEND_SEP + str(self.cardid) |
| | 770 | string += BACKEND_SEP + str(self.inputid) |
| | 771 | string += BACKEND_SEP + str(self.recpriority) |
| | 772 | string += BACKEND_SEP + str(self.recstatus) |
| | 773 | string += BACKEND_SEP + str(self.recordid) |
| | 774 | string += BACKEND_SEP + self.rectype |
| | 775 | string += BACKEND_SEP + self.dupin |
| | 776 | string += BACKEND_SEP + self.dupmethod |
| | 777 | string += BACKEND_SEP + str(int(mktime(self.recstartts.timetuple()))) |
| | 778 | string += BACKEND_SEP + str(int(mktime(self.recendts.timetuple()))) |
| | 779 | string += BACKEND_SEP + str(self.repeat) |
| | 780 | string += BACKEND_SEP + self.programflags |
| | 781 | string += BACKEND_SEP + self.recgroup |
| | 782 | string += BACKEND_SEP + str(self.commfree) |
| | 783 | string += BACKEND_SEP + self.outputfilters |
| | 784 | string += BACKEND_SEP + self.seriesid |
| | 785 | string += BACKEND_SEP + self.programid |
| | 786 | string += BACKEND_SEP + self.lastmodified |
| | 787 | string += BACKEND_SEP + str(self.stars) |
| | 788 | string += BACKEND_SEP + self.airdate |
| | 789 | string += BACKEND_SEP + str(self.hasairdate) |
| | 790 | string += BACKEND_SEP + self.playgroup |
| | 791 | string += BACKEND_SEP + str(self.recpriority2) |
| | 792 | string += BACKEND_SEP + self.parentid |
| | 793 | string += BACKEND_SEP + self.storagegroup |
| | 794 | string += BACKEND_SEP + self.audio_props |
| | 795 | string += BACKEND_SEP + self.video_props |
| | 796 | string += BACKEND_SEP + self.subtitle_type |
| | 797 | string += BACKEND_SEP + self.year |
| | 798 | |
| | 799 | return string |
| | 800 | |