Sunday, September 19, 2010

Some recording stats from my MythTV box...

Here are the current stats from my PVR. Interesting numbers to show just how much (or how little) TV we actually watch.

Recording Statistics

Number of shows:
246
Number of episodes:
12448
First recording:
Sunday January 23rd, 2005
Last recording:
Sunday September 19th, 2010
Total Running Time:
5 years 7 months 26 days 10 hrs 27 mins
Total Recorded:
1 year 2 months 8 days 13 hrs 47 mins
Percent of time spent recording:
10%
Shows Channels
Title Recorded Last Recorded # Title Recorded Last Recorded
How It's Made 1123 September 18 2010 1 CTV Toronto 679 September 18 2010
The Daily Show With Jon Stewart 948 September 16 2010 2 Space 217 September 14 2010
The Colbert Report 896 September 16 2010 3 CTV 200 September 18 2010
Law & Order 529 September 17 2010 4 Discovery 166 September 18 2010
Flip That House 485 October 18 2008 5 NBC Boston 130 September 2 2010
Star Trek: Voyager 472 September 7 2010 6 Mystery 114 September 19 2010
Star Trek: The Next Generation 395 November 26 2008 7 Comedy 95 July 28 2010
Frasier 368 December 3 2007 8 Global 72 July 12 2010
CSI: Crime Scene Investigation 366 May 20 2010 9 CTV NTV 72 September 19 2010
CSI: Miami 356 August 3 2010 10 Fox Boston 71 September 13 2010
Law & Order: Special Victims Unit 329 September 19 2010 11 CityTV Edmonton 71 September 16 2010
Law & Order: Criminal Intent 328 September 15 2010 12 CBS Seattle 70 August 21 2010
Due South 312 August 29 2008 13 ABC Boston 40 September 8 2010
Without a Trace 303 May 19 2009 14 A&E 38 August 3 2010
JAG 299 March 6 2008 15 Viva 36 June 8 2010
Cold Case 290 August 21 2010 16 CTV Vancouver 28 September 18 2010
MythBusters 262 September 12 2010 17 CityTV Calgary 21 September 18 2010
NCIS 240 September 1 2010 18 Showcase 17 July 2 2010
CSI: NY 230 June 30 2010 19 CityTV Toronto 16 September 9 2010
Battlestar Galactica 206 June 21 2009 20 Global Calgary 14 September 1 2010
Criminal Minds 197 June 29 2010 21 NBC Seattle 10 October 8 2009
House 192 September 19 2010 22 Fox Seattle 10 February 15 2010
Star Trek: Deep Space Nine 180 October 21 2008 23 Showcase Diva 9 June 12 2010
Tripping the Rift 178 September 12 2008 24 Global Vancouver 7 April 3 2010
Pimp My Ride 173 March 5 2009 25 Spike TV 5 February 5 2009

Monday, August 9, 2010

On HTML5, Video tags and Subtitles

Recently I read a rant about how HTML5 doesn't have closed caption support. This is true, but it's not so much of a hurdle as you might think. In a few hours I have worked out (mostly) a system that would work exactly the same way as the proposed extension (Barring the CSS wizardry requested – I'm not sure how to handle the requested pseudo-element selectors, so this one uses nice, simple CSS classes), all in nice, cross-platform JavaScript.

The script is copied below. I've not actually run it, or tested it, as I don't have any HTML5 video or SRT subtitle tracks to work with, but it should work in theory. And even if it doesn't, this is a reasonable starting point for building a complete system.

It's reasonably well commented, and should work just fine. I've not added any code to display the tracks dropdown, or to handle updating that part, and there's nothing in there to handle switching subtitle tracks, but that is a UI consideration IMHO. I may come back to this post and add that functionality at a later date. It also relies on being able to insert HTML elements inside a VIDEO tag. If that doesn't work, this could easily be changed to place an absolutely-positioned DIV over the top of the video, rather than drawing a DIV inside the video tag.

// Get array of videos on page
var v=document.getElementsByTagName("video");
 
// Loop through that array
for (var iVideos=0;iVideos<v.length;iVideos++) {
 
    // If the video has a "Track" element (or two)
    if v[iVideos].hasElement("track") {
        v[iVideos].tracks = [];
        v[iVideos].trackSelected = -1; // Change this to an index to change the displaying subtitle.
        // Get an array of tracks for each video
        var t=v[iVideos].getElementsByTagName("track");
        
        // Look through those tracks
        for (var iTracks=0;iTracks<t.length;iTracks++) {
            
            // If the kind is "Captions" or "Subtitles"
            if (t[iTracks].getAttribute("kind").toLowerCase()=="captions" || t[iTracks].getAttribute("kind").toLowerCase()=="subtitles") {
                
                // Add an object to the tracks array for this video object
                var iNewPos = v[iVideos].tracks.push({
                    "label": t[iTracks].getAttribute("label"), 
                    "kind": t[iTracks].getAttribute("kind"), 
                    "src": t[iTracks].getAttribute("src"),
                    "srclang": t[iTracks].getAttribute("srclang"),
                    "element": v[iVideos]
                });
                
                // Set up an async web request to fetch the text of the SRT file
                var xmlhttp=new XMLHttpRequest();
                if (!xmlhttp) 
                    xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
                
                // Store a reference to where we are, so we can work with it without having to wander through the page again
                xmlhttp.track = v[iVideos].tracks[iNewPos];
                xmlhttp.onreadystatechanged = new function() {
                    
                    // If we have a valid response
                    if (this.readyState == 4 && this.status == 200) {
                        
                        // Store the text, and fire off an SRT parser
                        this.track.webSRTText=responseText;
                        parseWebSRT(this.track);
                    }
                }
                
                // Fire off the request asynchronously
                xmlhttp.open("GET",t[iTracks].getAttribute("src"),true);
            }
        }
        
        // Set up a container for the subtitles
        var oSubtitleDiv = new document.createElement("div");
        oSubtitleDiv.style.position = "relative";
        oSubtitleDiv.style.bottom = "0px";
        oSubtitleDiv.style.left = "0px";
        oSubtitleDiv.style.right = "0px";
        oSubtitleDiv.cssClass = "cue";
        v[iVideos].appendChild(oSubtitleDiv);
        v[iVideos].subtitleDiv = oSubtitleDiv;
        v[iVideos].displaying = -1;
        
        // Set a function to update the subtitle container
        // Hopefully this doesn't fire TOO often, might want to only let this run every
        // x cycles or so, to ensure we don't bog the system down
        v[iVideos].timeupdate = new function() {
            if (this.trackSelected) {
                var track = this.tracks[this.trackSelected];
                if (track.cues) {
                    for (var iSRT=0;iSRT<track.cues.length;iSRT++) {
                        if (v[iVideos].currentTime > track.cues[iSRT].start && v[iVideos].currentTime < track.cues[iSRT].end) {
                            if (v[iVideos].displaying != iSRT) {
                                
                                // Remove the currently displaying contents
                                this.subtitleDiv.innerHTML = "";
                                
                                // Add the lines in this SRT file
                                for (var sLine in track.cues[iSRT].lines) {
                                    var oLine = new document.createElement("p");
                                    oLine.appendChild(document.createTextNode(sLine));
                                    this.subtitleDiv.appendChild(oLine);
                                }
                                this.subtitleDiv.cssClass = "cue cue" + track.cues[iSRT].cuenumber;
                                v[iVideos].displaying = iSRT;
                            }
                        }
                    }
                }
            }
        }
    }        
}
 
function parseWebSRT(track) {
    
    // Set up a variable to contain the text, with normalised line endings
    var text = track.webSRTText.replace(/(\r\n|\r|\n)/g, '\n');
    
    // Split the source into SRT blocks
    var aSRTParts = text.split("\n\n");
    
    var webSRT = [];
    
    // For each SRT part
    for (sPart in aSRTParts) {
        
        // Split it into lines
        var aSRTLines = sPart.split("\n");
        
        // Separate out the lines
        var aTimes = aSRTLines[1].split(" --> ");
        
        // Start time is the first defined
        var sStart = aTimes[0];
        
        // End time is the second defined. There may be some junk after it, separated by a space, so drop the rest
        // WARNING: This may barf, not tested
        var sEnd = aTimes[1].split(" ",2)[0];
        
        // Parse the times
        var iStart = parseSRTTime(sStart);
        var iEnd = parseSRTTime(sEnd);
        
        // Add the cue to the array
        var iNewPos = webSRT.push({
            "cuenumber": parseInt(aSRTLines[0]),
            "start": iStart,
            "end": iEnd,
            "lines": []
        })
        
        // Add the text lines to the newly-created cue object's lines property
        for (var i=2;i<aSRTLines.length;i++) {
            webSRT[iNewPos].lines.push(aSRTLines[i]);
        }
    }
    
    // Set the cues variable
    track.cues = webSRT;
}
 
function parseSRTTime(sTime) {
    
    // Split the time into parts, separated by colons
    aTimeParts = sTime.split(":");
    
    // The last part is the time in seconds, followed by a comma, followed by the time in ms
    // Easy way to sort that is to replace the , with a . and parse it as a float
    var iSecs = parseFloat(aTimeParts[aTimeParts.length - 1].replace(",","."));
    var iPosition = 60;
    
    // Loop through the parts BACKWARDS
    for (i=aTimeParts.length-2;i>0;i--) {
        
        // Increment the number of seconds, dependant on the position
        iSecs += parseInt(aTimeParts[i]) * iPosition;
        
        // Moving to the next highest denomenator, multiply by 60.
        // As the SRT only lists up to hours, this is a safe assumption to make
        iPosition = iPosition * 60;
    }
    
    // return the number of seconds total
    return iSecs;
}

Saturday, February 27, 2010

Umbraco… Not ready for the limelight yet?

I’ve been trying to use Umbraco CMS with my web host (See yesterday’s rant for a little more on that), but it seems that every version prior to 4.1 won’t run under “Medium Trust” (Something my web host seems to need). Thus I have downloaded the 4.1b2 version of Umbraco, and what do I find?

Configuration Error

Description: An error occurred during the processing of a configuration file required to service this request. Please review the specific error details below and modify your configuration file appropriately.

Parser Error Message: Request failed.

Source Error:

Line 145:            <providers>
Line 146:                <clear />
Line 147:                <add name="UmbracoRoleProvider" type="umbraco.providers.members.UmbracoRoleProvider" />
Line 148:            </providers>
Line 149:        </roleManager>

Looking through the bugtracker, I find the answer that “This'll be because I haven't brought my replacements for umbraco.providers into the 4.1 trunk yet. Will do later on today.” That was posted on the 18th, and the issue was marked as “Fixed”, yet going through the changelogs there doesn’t appear to be any such fix applied as of yet.

This kind of thing is infuriating, and makes the past 3 months spent learning Umbraco’s templating and design system a complete waste. At the moment, I can’t use this system, as powerful and nice as it is, which means I’ve wasted valuable time. So I’m on the lookout for another open-source (or at least free-to-use) CMS system that supports discussion fora, is reasonably simple to use on the back end, and works under Medium Trust .net v2 (Perhaps v3.5, but given that 3.5 runs on-top of v2, it’s difficult to see what version is actually installed). I’d also prefer C#, but that’s not really a necessity.

Anyone have any suggestions?

1&1 Internet – Great pricing, not so hot .Net support, Atrocious billing…

Well, here we go with another rant. It’s been a little too long since the last one of these, so here we go.

I have an account with 1&1 internet, to do Microsoft Web Hosting. The price was great for what you get (I mean, really, check it out for yourself!) but for billing… I guess it would be easier if we had a credit card, but the banks seem to disagree with us on that, so we used (or at least tried to use) a Visa Gift Card instead.

They have difficulty accepting them, it seems. And while you can pay with PayPal, it uses PayPal’s “Pre-Authorised Debit” system. Great, except for two things. One, it needs a credit card associated with the account, and two, it doesn’t work at all outside the US. Not that you can see that. You can go (from their merchant link to set it up) and set the authorisation, choose the maximum amount they can take, and as far as you can see everything is AOK. But if you sign in to your PayPal account (Using your country’s portal), there’s no sign of any pre-authorised options, you can’t adjust them or anything. ‘Cause it’s US only.

So now I have paid them using a Swift EFT, a direct electronic money transfer from my bank to them. I called them to confirm they’ve received it, and they claim so, but I’m still getting automated messages on my account details page saying it’s overdue. *sigh*. I’ll call them again on Monday and see what they say.

The other issue is that they run their .net apps at Medium trust, and with little sign of what version and extensions are installed.

The “Medium Trust” lead me to not be able to use Umbraco 4.0b (Which I was developing against on my local staging site). I’m hoping they’re running .net 3.5, ‘cause then I can use the newly-released Umbraco 4.1b (Which does work against Medium trust, but Requires .net 3.5). If they’re not running .net 3.5, I’m going to be screwed here, quite honestly. At that point it starts looking more cost and effort effective to buy a basic box on Amazon EC2 and use that to host, though the bandwidth costs there would be the main expense.

Still, live and learn, huh?

Sunday, November 22, 2009

Sasktel’s service is great, it’s a shame their hardware isn’t so…

Guess what? It’s another rant! (I seem to be very good at these, for some reason).

Okay, so I’m a Sasktel subscriber here in Regina – That means I use their ADSL2e service for internet and TV, as they have a nifty IPTV network. The IPTV part I love, as it works a dream with my MythTV DVR, but the Router they provide (And that you HAVE to use – because of the way it handles DHCP assignments for the set-top boxes, and talking to the DSLAM, you have no other option), a 2-Wire unit customised (slightly) for them, is very poor. It doesn’t support UPNP for port allocation, not that it’s THAT much of an issue – If you have more than around 100 ports forwarded, it slows access to a crawl. The internal DHCP server and DNS forwarder also slow down within hours of rebooting, and stop working after a few days of uptime. If you change your network settings (or, indeed, the handed-out settings) to use the upstream DNS servers, things work fine, unless (of course) you need to connect a “new” machine to the network. Then, as the DHCP server is timing out every time, you can’t.

I have a second router (What I was using before I signed up for Sasktel) that I am more than pleased with – It’s a Belkin router that’s been hacked to run DD-WRT. That makes for a great router (With working UPNP and QoS), and it’s fast enough, and has enough RAM that there are rarely issues with internal servers dying. It can even make “Hotspot” systems with a little setting up, though I have no need of that functionality. But, because of the way things work, I can’t use it, so it’s next to useless, and after a few days running, so is this Sasktel unit.

Friday, November 13, 2009

Another rant… This time about WLW! (Updated!)

WLW being Windows Live Writer.

I’m not saying it’s bad (It’s actually rather good, IMHO), but there seems to be some kind of funky interaction between it and Blogger.

The source for a post, when it’s inside WLW looks like this:

<p>WLW being Windows Live Writer.</p>
<p>I’m not saying it’s bad (It’s actually rather good, IMHO), 
but there seems to be some kind of funky interaction between it and Blogger.</p>
<p>The source for a post, when it’s inside WLW looks like this:</p>

But, when it’s posted to blogger, it comes out like this:

<p>WLW being Windows Live Writer.</p>
<br />
<br />
<p>I’m not saying it’s bad (It’s actually rather good, IMHO), 
but there seems to be some kind of funky interaction between it and Blogger.</p>
<br />
<br />
<p>The source for a post, when it’s inside WLW looks like this:</p>

Obviously, this isn’t right. It looks like blogger is replacing every newline character with a <br /> and a newline. This would be fine if I was composing in plain text, but the editor I’m using is already doing the work at making sure everything is nicely XHTML formatted – I don’t need the extra newlines.

This was, for a while, making everything look FAR too spaced out, until I tweaked my CSS like so:

.post-body br {
   display: none;
}
.post-body p > br {
   display: auto;
}

What does this do?

It tells browsers to not display any “BR” elements inside the “.post-body” div, and then overrides that to say DO display “BR”s that are directly inside “P” (Paragraph) elements.

This fixes the problem, but I have a feeling I’m going about this the wrong way. Anyone out there use WLW with a similar problem? How did you fix it?

[Update] Well, I found the problem. Hidden deep within the settings of Blogger, there is the option “Convert Line Breaks” that defaults to “Yes”, with the description:

If Yes is selected, single hard-returns entered in the Post Editor will be replaced with single <br /> tags in your blog, and two hard-returns will be replaced with two tags (<br /><br />).

The wording of this implies it’s only going to do it for items posted through the Blogger “Post Editor”, but it seems this enables this behaviour everywhere, including through the Blogger API.

Now that this is switched off, everything looks just right again.

So, my apologies WLW, it wasn’t your fault at all. Blogger: What were you thinking?

Python didn’t have it, anyway…

Now, however, it does. Kinda.

def Python_to_XML_String(inst, name):
   builder = "<" + name + ">"
   builder += Python_to_XML_String_Recursion(inst)
   builder += "</" + name + ">"
   return minidom.parseString(builder)
   
def Python_to_XML_String_Recursion(inst):
   # Put entire object inside an elem w/ same name as the class.
   builder = ""
   for attr in inst:
      value = inst[attr]
      if type(value) == types.DictionaryType:
         # Recursively process subobjects
         builder += '<' + attr + '>'
         builder += Python_to_XML_String_Recursion(value)
         builder += '</' + attr + '>'
      elif type(value) == types.ListType:
         builder += '<' + attr + '>'
         for item in value:
            if type(item) == types.DictionaryType:
               builder += '<Item>'
               builder += Python_to_XML_String_Recursion(item)
               builder += '</Item>'
            else:
               builder += '<Item>'
               builder += str(item).replace("&","&amp;").replace("<","&lt;")
               builder += '</Item>'
         builder += '</' + attr + '>'
      else:
         # Convert anything else to string, put it in an element
         builder += '<' + attr + '>'
         builder += str(value).replace("&","&amp;").replace("<","&lt;")
         builder += '</' + attr + '>'
   return builder

(Note: I need a better source colouring plugin for WLW. Anyone know of one?)

This code is pretty limiting:

  • It’ll pretty much only work with generic type objects ({}, [] and primitives), but that’s all I need.
  • It also relies on the str() function to convert everything into a string, with no typing. This means that booleans (True, False) stay capitalised, which could cause some problems if your code is expecting lower-case values, and dates are unformatted.
  • It returns a minidom object (Which, obviously, has to be imported). You can output this with a .toxml() call.
  • It relies on the first object being passed in being a dictionary object, and that there will never be a list inside a list. This could be coded around, but it’ll take more work (and more helper functions, to fill in the “missing” information.
  • It takes an initial “name” argument to define the outer object.

To use it, call it like this:

Python_to_XML_String({'Thing': 'Value', 'Object', ['Thing 1', 'Thing 2']},'MyObject')

And it’ll return (for that example) an XML object looking like this:

<MyObject>
   <Thing>Value</Thing>
   <Object>
      <Item>Thing 1</Item>
      <Item>Thing 2</Item>
   </Object>
</MyObject>

I hope this is helpful to someone else out there…