| 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 getFrontends(self): |
| 266 | """ |
| 267 | Returns a list of Frontend objects for accessible frontends |
| 268 | """ |
| 269 | cursor = self.db.db.cursor() |
| 270 | cursor.execute("SELECT DISTINCT hostname FROM settings WHERE hostname IS NOT NULL and value='NetworkControlEnabled' and data=1") |
| 271 | frontends = [] |
| 272 | for fehost in cursor.fetchall(): |
| 273 | try: |
| 274 | frontend = self.getFrontend(fehost[0]) |
| 275 | frontends.append(frontend) |
| 276 | except: |
| 277 | print "%s is not a valid frontend" % fehost[0] |
| 278 | cursor.close() |
| 279 | return frontends |
| 280 | |
| 281 | def getFrontend(self,host): |
| 282 | """ |
| 283 | Returns a Frontend object for the specified host |
| 284 | """ |
| 285 | port = self.db.getSetting("NetworkControlPort",host) |
| 286 | return Frontend(host,port) |
| 287 | |
| 288 | def splitInt(self,integer): |
| 289 | """ |
| 290 | Returns a pair of signed integers from a single long |
| 291 | """ |
| 292 | return integer/(2**32),integer%2**32 - (integer%2**32 > 2**31)*2**32 |
| 293 | |
| 294 | class FileTransfer: |
| 295 | """ |
| 296 | A connection to mythbackend intended for file transfers |
| 297 | """ |
| 298 | sockno = None |
| 299 | socket = None |
| 300 | tsize = 2**16 |
| 301 | |
| 302 | def __init__(self, file, parent=None): |
| 303 | self.db = MythDB(sys.argv[1:]) |
| 304 | if not isinstance(parent, MythTV): |
| 305 | self.parent = MythTV() |
| 306 | else: |
| 307 | self.parent = parent |
| 308 | if isinstance(file, Program): |
| 309 | self.master_host, self.master_port = file.filename.split('/')[-2].split(':') |
| 310 | self.master_port = int(self.master_port) |
| 311 | self.filename = file.filename.split('/')[-1] |
| 312 | self.sgroup = file.storagegroup |
| 313 | elif isinstance(file, tuple): |
| 314 | if len(file) != 3: |
| 315 | log.Msg(CRITICAL, 'Incorrect FileTransfer() input size') |
| 316 | sys.exit(1) |
| 317 | else: |
| 318 | self.master_host = file[0] |
| 319 | self.master_port = int(self.db.getSetting('BackendServerPort',self.master_host)) |
| 320 | self.filename = file[1] |
| 321 | self.sgroup = file[2] |
| 322 | else: |
| 323 | log.Msg(CRITICAL, 'Improper input to FileTransfer()') |
| 324 | sys.exit(1) |
| 325 | |
| 326 | try: |
| 327 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 328 | self.socket.settimeout(10) |
| 329 | self.socket.connect((self.master_host, self.master_port)) |
| 330 | res = self.send('MYTH_PROTO_VERSION %s' % PROTO_VERSION).split(BACKEND_SEP) |
| 331 | if res[0] == 'REJECT': |
| 332 | log.Msg(CRITICAL, 'Backend has version %s and we speak version %s', res[1], PROTO_VERSION) |
| 333 | sys.exit(1) |
| 334 | |
| 335 | res = self.send('ANN FileTransfer %s%s%s%s%s' % (socket.gethostname(), BACKEND_SEP, self.filename, BACKEND_SEP, self.sgroup)) |
| 336 | if res.split(BACKEND_SEP)[0] != 'OK': |
| 337 | log.Msg(CRITICAL, 'Unexpected answer to ANN command: %s', res) |
| 338 | else: |
| 339 | log.Msg(INFO, 'Successfully connected mythbackend at %s:%d', self.master_host, self.master_port) |
| 340 | sp = res.split(BACKEND_SEP) |
| 341 | self.sockno = int(sp[1]) |
| 342 | self.pos = 0 |
| 343 | self.size = (int(sp[2]) + (int(sp[3])<0))*2**32 + int(sp[3]) |
| 344 | |
| 345 | except socket.error, e: |
| 346 | log.Msg(CRITICAL, 'Couldn\'t connect to %s:%d (is the backend running)', self.master_host, self.master_port) |
| 347 | sys.exit(1) |
| 348 | |
| 349 | def __del__(self): |
| 350 | if self.sockno: |
| 351 | self.parent.backendCommand('QUERY_FILETRANSFER %d%sDONE' % (self.sockno, BACKEND_SEP)) |
| 352 | if self.socket: |
| 353 | self.socket.shutdown(1) |
| 354 | self.socket.close() |
| 355 | |
| 356 | def send(self,data): |
| 357 | command = '%-8d%s' % (len(data), data) |
| 358 | log.Msg(DEBUG, 'Sending command: %s', command) |
| 359 | self.socket.send(command) |
| 360 | return self.recv() |
| 361 | |
| 362 | def recv(self): |
| 363 | data = self.socket.recv(8) |
| 364 | try: |
| 365 | length = int(data) |
| 366 | except: |
| 367 | return '' |
| 368 | data = [] |
| 369 | while length > 0: |
| 370 | chunk = self.socket.recv(length) |
| 371 | length = length - len(chunk) |
| 372 | data.append(chunk) |
| 373 | return ''.join(data) |
| 374 | |
| 375 | def tell(self): |
| 376 | """ |
| 377 | Return the current offset from the beginning of the file |
| 378 | """ |
| 379 | return self.pos |
| 380 | |
| 381 | def close(self): |
| 382 | """ |
| 383 | Close the data transfer socket |
| 384 | """ |
| 385 | self.__del__() |
| 386 | |
| 387 | def rewind(self): |
| 388 | """ |
| 389 | Seek back to the start of the file |
| 390 | """ |
| 391 | self.seek(0) |
| 392 | |
| 393 | def read(self, size): |
| 394 | """ |
| 395 | Read a block of data, requests over 64KB will be buffered internally |
| 396 | """ |
| 397 | if size == 0: |
| 398 | return '' |
| 399 | if size > self.size - self.pos: |
| 400 | size = self.size - self.pos |
| 401 | csize = size |
| 402 | rsize = 0 |
| 403 | if csize > self.tsize: |
| 404 | csize = self.tsize |
| 405 | rsize = size - csize |
| 406 | |
| 407 | res = self.parent.backendCommand('QUERY_FILETRANSFER %d%sREQUEST_BLOCK%s%d' % (self.sockno,BACKEND_SEP,BACKEND_SEP,csize)) |
| 408 | self.pos = self.pos + int(res) |
| 409 | # if int(res) == csize: |
| 410 | # if csize < size: |
| 411 | # self.tsize += 8192 |
| 412 | # else: |
| 413 | # self.tsize -= 8192 |
| 414 | # rsize = size - int(res) |
| 415 | # print 'resizing buffer to %d' % self.tsize |
| 416 | |
| 417 | return self.socket.recv(int(res)) + self.read(rsize) |
| 418 | |
| 419 | def seek(self, offset, whence=0): |
| 420 | """ |
| 421 | Seek 'offset' number of bytes |
| 422 | whence==0 - from start of file |
| 423 | whence==1 - from current position |
| 424 | """ |
| 425 | if whence == 0: |
| 426 | if offset < 0: |
| 427 | offset = 0 |
| 428 | if offset > self.size: |
| 429 | offset = self.size |
| 430 | elif whence == 1: |
| 431 | if offset + self.pos < 0: |
| 432 | offset = -self.pos |
| 433 | if offset + self.pos > self.size: |
| 434 | offset = self.size - self.pos |
| 435 | elif whence == 2: |
| 436 | if offset > 0: |
| 437 | offset = 0 |
| 438 | if offset < -self.size: |
| 439 | offset = -self.size |
| 440 | else: |
| 441 | log.Msg(CRITICAL, 'Whence can only be 0, 1, or 2') |
| 442 | |
| 443 | curhigh,curlow = self.parent.splitInt(self.pos) |
| 444 | offhigh,offlow = self.parent.splitInt(offset) |
| 445 | |
| 446 | 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) |
| 447 | self.pos = (int(res[0]) + (int(res[1])<0))*2**32 + int(res[1]) |
| 448 | |
| 449 | |
| 450 | class Frontend: |
| 451 | isConnected = False |
| 452 | socket = None |
| 453 | host = None |
| 454 | port = None |
| 455 | |
| 456 | def __init__(self, host, port): |
| 457 | self.host = host |
| 458 | self.port = int(port) |
| 459 | self.connect() |
| 460 | self.disconnect() |
| 461 | |
| 462 | def __del__(self): |
| 463 | if self.isConnected: |
| 464 | self.disconnect() |
| 465 | |
| 466 | def __repr__(self): |
| 467 | return "%s@%d" % (self.host, self.port) |
| 468 | |
| 469 | def __str__(self): |
| 470 | return "%s@%d" % (self.host, self.port) |
| 471 | |
| 472 | def connect(self): |
| 473 | self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) |
| 474 | self.socket.settimeout(10) |
| 475 | self.socket.connect((self.host, self.port)) |
| 476 | if self.recv()[:28] != "MythFrontend Network Control": |
| 477 | self.socket.close() |
| 478 | self.socket = None |
| 479 | raise Exception('FrontendConnect','Connected socket does not belong to a mythfrontend') |
| 480 | self.isConnected = True |
| 481 | |
| 482 | def disconnect(self): |
| 483 | self.send("exit") |
| 484 | self.socket.close() |
| 485 | self.socket = None |
| 486 | self.isConnected = False |
| 487 | |
| 488 | def send(self,command): |
| 489 | if not self.isConnected: |
| 490 | self.connect() |
| 491 | self.socket.send("%s\n" % command) |
| 492 | |
| 493 | def recv(self,curstr=""): |
| 494 | def subrecv(self,curstr=""): |
| 495 | try: |
| 496 | curstr += self.socket.recv(100) |
| 497 | except: |
| 498 | return None |
| 499 | if curstr[-4:] != '\r\n# ': |
| 500 | curstr = subrecv(self,curstr) |
| 501 | return curstr |
| 502 | return subrecv(self)[:-4] |
| 503 | |
| 504 | def sendJump(self,jumppoint): |
| 505 | """ |
| 506 | Sends jumppoint to frontend |
| 507 | """ |
| 508 | self.send("jump %s" % jumppoint) |
| 509 | if self.recv() == 'OK': |
| 510 | return 0 |
| 511 | else: |
| 512 | return 1 |
| 513 | |
| 514 | def getJump(self): |
| 515 | """ |
| 516 | Returns a tuple containing available jumppoints |
| 517 | """ |
| 518 | self.send("help jump") |
| 519 | res = self.recv().split('\r\n')[3:-1] |
| 520 | points = [] |
| 521 | for point in res: |
| 522 | spoint = point.split(' - ') |
| 523 | points.append((spoint[0].rstrip(),spoint[1])) |
| 524 | return tuple(points) |
| 525 | |
| 526 | def sendKey(self,key): |
| 527 | """ |
| 528 | Sends keycode to connected frontend |
| 529 | """ |
| 530 | self.send("key %s" % key) |
| 531 | if self.recv() == 'OK': |
| 532 | return 0 |
| 533 | else: |
| 534 | return 1 |
| 535 | |
| 536 | def getKey(self): |
| 537 | """ |
| 538 | Returns a tuple containing available special keys |
| 539 | """ |
| 540 | self.send("help key") |
| 541 | res = self.recv().split('\r\n')[4] |
| 542 | keys = [] |
| 543 | for key in res.split(','): |
| 544 | keys.append(key.strip()) |
| 545 | return tuple(keys) |
| 546 | |
| 547 | def sendQuery(self,query): |
| 548 | """ |
| 549 | Returns query from connected frontend |
| 550 | """ |
| 551 | self.send("query %s" % query) |
| 552 | return self.recv() |
| 553 | |
| 554 | def getQuery(self): |
| 555 | """ |
| 556 | Returns a tuple containing available queries |
| 557 | """ |
| 558 | self.send("help query") |
| 559 | res = self.recv().split('\r\n')[:-1] |
| 560 | queries = [] |
| 561 | tmpstr = "" |
| 562 | for query in res: |
| 563 | tmpstr += query |
| 564 | squery = tmpstr.split(' - ') |
| 565 | if len(squery) == 2: |
| 566 | tmpstr = "" |
| 567 | queries.append((squery[0].rstrip().lstrip('query '),squery[1])) |
| 568 | return tuple(queries) |
| 569 | |
| 570 | def sendPlay(self,play): |
| 571 | """ |
| 572 | Send playback command to connected frontend |
| 573 | """ |
| 574 | self.send("play %s" % play) |
| 575 | if self.recv() == 'OK': |
| 576 | return 0 |
| 577 | else: |
| 578 | return 1 |
| 579 | |
| 580 | def getPlay(self): |
| 581 | """ |
| 582 | Returns a tuple containing available playback commands |
| 583 | """ |
| 584 | self.send("help play") |
| 585 | res = self.recv().split('\r\n')[:-1] |
| 586 | plays = [] |
| 587 | tmpstr = "" |
| 588 | for play in res: |
| 589 | tmpstr += play |
| 590 | splay = tmpstr.split(' - ') |
| 591 | if len(splay) == 2: |
| 592 | tmpstr = "" |
| 593 | plays.append((splay[0].rstrip().lstrip('play '),splay[1])) |
| 594 | return tuple(plays) |
| 595 | |
| 596 | |
| 597 | |
| 684 | def toString(self): |
| 685 | string = self.title |
| 686 | string += BACKEND_SEP + self.subtitle |
| 687 | string += BACKEND_SEP + self.description |
| 688 | string += BACKEND_SEP + self.category |
| 689 | if self.chanid: |
| 690 | string += BACKEND_SEP + str(self.chanid) |
| 691 | else: |
| 692 | string += BACKEND_SEP |
| 693 | string += BACKEND_SEP + self.channum |
| 694 | string += BACKEND_SEP + self.callsign |
| 695 | string += BACKEND_SEP + self.channame |
| 696 | string += BACKEND_SEP + self.filename |
| 697 | string += BACKEND_SEP + str(self.fs_high) |
| 698 | string += BACKEND_SEP + str(self.fs_low) |
| 699 | string += BACKEND_SEP + str(int(mktime(self.starttime.timetuple()))) |
| 700 | string += BACKEND_SEP + str(int(mktime(self.endtime.timetuple()))) |
| 701 | string += BACKEND_SEP + str(self.duplicate) |
| 702 | string += BACKEND_SEP + str(self.shareable) |
| 703 | string += BACKEND_SEP + str(self.findid) |
| 704 | string += BACKEND_SEP + self.hostname |
| 705 | string += BACKEND_SEP + str(self.sourceid) |
| 706 | string += BACKEND_SEP + str(self.cardid) |
| 707 | string += BACKEND_SEP + str(self.inputid) |
| 708 | string += BACKEND_SEP + str(self.recpriority) |
| 709 | string += BACKEND_SEP + str(self.recstatus) |
| 710 | string += BACKEND_SEP + str(self.recordid) |
| 711 | string += BACKEND_SEP + self.rectype |
| 712 | string += BACKEND_SEP + self.dupin |
| 713 | string += BACKEND_SEP + self.dupmethod |
| 714 | string += BACKEND_SEP + str(int(mktime(self.recstartts.timetuple()))) |
| 715 | string += BACKEND_SEP + str(int(mktime(self.recendts.timetuple()))) |
| 716 | string += BACKEND_SEP + str(self.repeat) |
| 717 | string += BACKEND_SEP + self.programflags |
| 718 | string += BACKEND_SEP + self.recgroup |
| 719 | string += BACKEND_SEP + str(self.commfree) |
| 720 | string += BACKEND_SEP + self.outputfilters |
| 721 | string += BACKEND_SEP + self.seriesid |
| 722 | string += BACKEND_SEP + self.programid |
| 723 | string += BACKEND_SEP + self.lastmodified |
| 724 | string += BACKEND_SEP + str(self.stars) |
| 725 | string += BACKEND_SEP + self.airdate |
| 726 | string += BACKEND_SEP + str(self.hasairdate) |
| 727 | string += BACKEND_SEP + self.playgroup |
| 728 | string += BACKEND_SEP + str(self.recpriority2) |
| 729 | string += BACKEND_SEP + self.parentid |
| 730 | string += BACKEND_SEP + self.storagegroup |
| 731 | string += BACKEND_SEP + self.audio_props |
| 732 | string += BACKEND_SEP + self.video_props |
| 733 | string += BACKEND_SEP + self.subtitle_type |
| 734 | string += BACKEND_SEP + self.year |
| 735 | |
| 736 | return string |
| 737 | |