This is my first attempt at creating a way to play Tivo shows through XBMC. In a previous post, I lamented about this not already being an Add-On and did some basic research into this. The holidays have provided a lot more free time to investigate this. I started by looking at the notes on from the XBMC Development website. It's not super friendly, yet there is a lot of details on how to get started there. I will not go into it, as that's not the point of this post.
XBMC uses Python for it's scripts. I have very little, next to none, experience using Python. I have programming and scripting experience and I was not afraid, I jumped in. I started building the basics for the XBMC Plugin and felt I need to just address the code outside of XBMC first. So I started building a test Python script. A sort of CLI version or mock up of the code that I will later use in XBMC. This code has a few different elements.
- Must be able to make HTTP Requests
- Must be able to parse XML
- Should have the ability to navigate Tivo folders and shows from Now Playing
- Should be able to play a selection directly through a video player (passing through TivoDecode)
So in my last post, I used wget to retrieve the files and also was looking into TivoRoll, which I don't think really worked for me. So I looked into seeing if I could use wget to stream the Tivo file, via pipe, into TivoDecode, that would then stream the decoded video to mPlayer, again via pipe. Something like this:
wget --no-check-certificate --http-user=tivo --http-password=4155552222 -O - "
http://192.168.1.100/download/Adventure%20Time.TiVo?Container=%2FNowPlaying&id=653115&Format=video/x-tivo-mpeg-ts" | tivodecode -m 4155552222 - | mplayer -vf pp=lb -cache 32768 -
tivodecode did not seem to like the lack of file path. Will research that more, yet there was an easier way around this, buffer some of the file before sending it to TivoDecode. So this is still a work in progress and is not tested yet. I just wanted to get the code out there. If there are any Python savy folks out there, please let me know if I am doing something horribly wrong or if I can optimize the code any way.
This script uses
Requests: HTTP for Humans, It's not a regular Python library and you need to install it. You can do so using:
pip install requests
Tivo uses HTTP Digest Authentication, so that is also needed for proper authentication. I use DateTime to format the Hex time stamps from Tivo. Also found that Duration is in Milliseconds. I modeled the prompts and the interaction on the same actions you would take on your Tivo. I choose ElementTree over some of the other XML parsers out there. I found more details on it and most threads on StackOverflow suggest it. There are lots of details in the XML for Folders and Videos. I grab just the basics I needed so I could determine which show is which.
Python Query Tivo Now Playing, display list, and pass buffered tivo file to TivoDecode/mPlayer for viewing.
import os
import sys
import logging
import requests as REQ
from requests.auth import HTTPDigestAuth;
import datetime as DT
try:
import cElementTree as ET
except ImportError:
try:
# Python 2.5 need to import a different module
import xml.etree.cElementTree as ET
except ImportError:
exit_err("Failed to import cElementTree from any known place");
# Functions
def lsTivo(url, mak):
if url == None:
return "No URL presented";
if mak == None:
return "No MAK presented";
user = "tivo";
r = REQ.get(url, auth=HTTPDigestAuth(user, mak), verify=False);
root = ET.fromstring(r.content);
if r.status_code == REQ.codes.ok:
npList=[];
currentFolder = root[0][2].text;
for e in root.findall("{
http://www.tivo.com/developer/calypso-protocol-1.6/}Item"):
title = e[0][2].text;
url = e[1][0][0].text;
type = e[0][0].text;
cDate = int(e[0][5].text, 16);
if type == 'video/x-tivo-raw-tts':
desc = e[0][11].text;
dur = int(e[0][4].text);
progId = e[0][15].text;
else:
desc = "";
dur = 0;
progId = 0;
npList.append([title, url, type, cDate, desc, dur, progId]);
else:
return r.raise_for_status();
return npList;
def timeStamp(dt, format):
return DT.datetime.fromtimestamp(dt).strftime(format);
def clear():
# print chr(27) + "[2J";
os.system('clear');
def firstMenu():
clear();
# Create Listing Array
nowPlayingArray = lsTivo(startUrl, mak)
print('\t -- Current Folder:\tNow Playing --');
i = 1;
for item in nowPlayingArray:
cDate = timeStamp(item[3], '%a %m/%d');
if item[2] == 'x-tivo-container/folder':
print "{}.\t{}\t{}\t".format(i, item[0], cDate);
if item[2] == 'video/x-tivo-raw-tts':
print "{}.\t{}\t{}".format(i, item[0], cDate);
i += 1;
print('q.\t Quit');
print("\n");
retResult = raw_input("Enter your choice: ");
if retResult == 'q':
return ('quit', 0);
sel = int(retResult) - 1;
if nowPlayingArray[sel][2] == 'video/x-tivo-raw-tts':
return ('video', nowPlayingArray[sel]);
else:
return ('folder', nowPlayingArray[sel][1]);
def subMenu(url):
clear();
childArray = lsTivo(url, mak);
print('\t -- Current Folder:\t' + currentFolder + " --");
print('0. Go Back');
i = 1;
for item in childArray:
cDate = timeStamp(item[3], '%a %m/%d');
print("{}.\t{}\t{}".format(i, item[0], cDate));
i += 1;
print("\n");
retResult = raw_input('Enter your choice: ');
if int(retResult) == 0:
return 0;
sel = int(retResult) - 1
return childArray[sel];
def showDesc(title, desc, cDate, dur, dUrl, pUrl):
clear();
print("Title:\t" + title);
print("Description:\t" + desc);
print("Recorded:\t" + timeStamp(cDate, '%a %m/%d %I:%M %p'));
# Duration stored in ms
s, ms = divmod(dur, 1000);
m, s = divmod(s, 60);
h, m = divmod(m, 60);
print("Duration:\t{}:{}".format(h, m));
print("\n");
print("0. Go Back");
print("1. Play");
print("\n");
retResult = raw_input("Enter your choice: ");
if int(retResult) == 0:
return 0;
else:
return dUrl;
def fetchSelection(url, fp):
print('Requesting ' + url + '...');
r = REQ.get(url, auth=HTTPDigestAuth('tivo', mak), verify=False, stream=True);
print('Fetching your selection...');
if r.status_code == REQ.codes.ok:
print('HTTP Request sent, awaiting response... 200 OK');
print('Buffering...');
else:
print('HTTP Request sent, awaiting response... ' + str(r.status_code));
print(r.raise_for_status());
return;
# f = open(fp, 'wb');
video_file_size_start = 0;
video_file_size_end = 1048576 * cacheSize; # end in CacheSize in MB
block_size = 1024;
# while True:
# try:
# buffer = r.iter_content(block_size);
# if buffer == None:
# break;
# video_file_size_start += len(buffer);
# if video_file_size_start > video_file_size_end:
# break;
# f.write(buffer);
# print(video_file_size_start + 'MB /' + video_file_size_end + 'MB');
# except Exception, e:
# logging.exception(e);
#
# f.close();
with open(fp, 'wb') as fd:
for chunk in r.iter_content(block_size):
video_file_size_start += len(chunk);
if video_file_size_start > video_file_size_end:
break;
fd.write(chunk);
print(str(video_file_size_start/1024/1024) + ' MB / ' + str(video_file_size_end/1024/1024) + ' MB');
fd.close();
return;
# Define Constants
mak = '9105856706';
startUrl = '
https://192.168.1.102/TiVoConnect?Command=QueryContainer&Container=%2FNowPlaying&Recurse=No';
currentFolder = "";
videoFormat = "video/x-tivo-mpeg"; # Alt: video/x-tivo-mpeg-ts
downloadPath = 'resources/nowPlayingCache/';
cacheSize = 25; # MB
# Loop
nType, nUrl = firstMenu();
while not nType == 0:
if nType == 'folder':
details = subMenu(nUrl);
if(details == 0):
nType, nUrl = firstMenu();
else:
choice = showDesc(details[0], details[4], int(details[3]), details[5], details[1], startUrl);
if choice == 0:
nType, nUrl = firstMenu();
else:
filePath = downloadPath + details[6] + '.tivo';
fetchSelection(nUrl + '&Format=' + videoFormat, filePath);
print('Launching player...');
playerCommand = 'tivodecode -m ' + mak + ' ' + filePath + ' | ';
playerCommand += 'mplayer -vf pp=lb - cache 32768 -';
os.system(playerCommand);
nType = 0;
else:
details = nUrl; # Contains detail array from video
choice = showDesc(details[0], details[4], int(details[3]), details[5], details[1], startUrl);
if choice == 0:
nType, nUrl = firstMenu();
else:
filePath = downloadPath + details[6] + '.tivo';
fetchSelection(details[1] + '&Format=' + videoFormat, filePath)
streamCommand += 'tivodecode -m ' + mak + ' ' + filePath + ' | ';
streamCommand += 'gnome-mplayer -vf pp=lb -cache 32768 -';
print(streamCommand);
os.system(streamCommand);
nType = 0;