| | 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 | |