thingsinjars

  • 30 Sep 2010

    Writing a Plex Plugin Part III

    This is the final part of my walkthrough of the Plex Media Server Transmission plugin.

    Right.

    We've done the required built-in functionality (preference management, for instance) and the bits that talk to Transmission itself. Basically, we're done. Anything else added here is just extra. That is, of course, the best reason to add stuff here. As I have previously ranted at length, there's no point doing anything if you aren't trying to do it as well as it possibly can be done. In this particular instance, that manifests itself in the ability to browse, search for and download torrents all within the Plex client interface.

    EZTV

    I love EZTV. It makes things easy. Previous versions of this plugin included an EZTV search but after rooting around in the source of the µTorrent plugin, I found some nifty code which turned me onto the clever XML parsing Plex can do.

    Note: although some of this stuff looks clever, all the cleverness was done by the Plex dev team and the author of the µTorrent plugin. I'm a good copy-and-paster.

    This function grabs the H2s from the page http://ezrss.it/shows/. If you go there, you'll see that the page lists every TV show in EZTV. The original µTorrent function listed everything but there are a lot of shows there now so it was actually taking a long time just to get that list. As they've split the page up by section, we can just grab the bits we want. This is going to be a full page in Plex (not a popup) so we're using a MediaContainer.

    def TVShowListFolders(sender):
      dir = MediaContainer()

    Using the built-in XML module, we can simply pass in a URL and get back an object containing the hierarchical structure of the entire page. Seriously, how simple is this? As it's HTML, add in the option isHTML=True.

      showsPage = XML.ElementFromURL(
                    'http://ezrss.it/shows/', 
                    isHTML=True, 
                    errors='ignore'
                  )

    Now that we have the whole page structure, take the chunks of the page we want. All the sections we want (and one we don't) are divs with the class 'block' so use that in xpath to pull them out.

      blocks = showsPage.xpath('//div[@class="block"]')

    The first block is the one we don't want (if you look at the page, it's the one that lists all the letters) so we remove it.

      blocks.pop(0)

    For each of the remaining blocks, find the text in the first H2. That is the letter title of the section ('A', 'B', 'C', etc). Add that to Plex as a menu item then return the entire list.

      for block in blocks:
        letter = block.xpath("h2")[0].text
        dir.Append(
          Function(
            DirectoryItem(
              TVShowListSubfolders,
              letter,
              subtitle=None,
              summary=None,
              thumb=R(ICON),
              art=R(ART)
            ),
            letter=letter
          )
        )
      return dir

    I hope I'm not the only one impressed with that (although I have a feeling I might be). Using just a couple of lines from the XML module and a sprinkle of xpath and we've got another menu, dynamically generated from a third-party website. If EZTV ever change their layout, it should be a simple matter of changing the xpath to match and we're done. Again.

    We can now do the same again but this time, we only pull out a single section based on the letter passed in.

    def TVShowListSubfolders(sender, letter):
      dir = MediaContainer()
      showsPage = XML.ElementFromURL(
                    'http://ezrss.it/shows/',
                    isHTML=True,
                    errors='ignore'
                  )
      blocks = showsPage.xpath(
                '//div[@class="block" and h2 = "%s"]' % letter
               )

    Remembering to ignore any 'back to top' links, write out a list of the shows in this section. These will call the TVEpisodeList method next.

     for block in blocks:
      for href in block.xpath('.//a'):
       if href.text != "# Top":
        requestUrl = "http://ezrss.it" + href.get("href") + "&mode=rss"
        dir.Append(
         Function(
          DirectoryItem(
           TVEpisodeList,
           href.text,
           subtitle=None,
           summary=None,
           thumb=R(ICON),
           art=R(ART)
          ),
          name=href.text,
          url=requestUrl
         )
        )
     return dir

    This lists all available torrents for the chosen show. By this point, you should be familiar with how this works. We're using the XML module to grab the page at the URL (this time it's an RSS feed so we don't need to parse it as HTML); we use XPath to iterate through the items in the feed; we generate a menu item from the data which will call a function when selected; we append that to a MediaContainer then return the whole thing to Plex. Done. The AddTorrent function was defined higher up.

    def TVEpisodeList(sender, name, url):
     dir = MediaContainer()
     feed = XML.ElementFromURL(url, isHTML=False, errors='ignore').xpath("//item")
     for element in feed:
      title = element.xpath("title")[0].text
      link = element.xpath("link")[0].text
      dir.Append(
       Function(
        DirectoryItem(
         AddTorrent,
         title,
         subtitle=None,
         summary=None,
         thumb=R(ICON),
         art=R(ART)
        ),
        torrentUrl=link
       )
      )
     return dir

    Adult considerations...

    There is currently a section in the plugin which will allow you to search IsoHunt. This might get dropped in future versions of the plugin as results from IsoHunt are almost exclusively...ahem...adult, regardless of search terms. Sure, that might be exactly what you were looking for but if you were actually looking for Desperate Housewives, you might be surprised when your file comes down and it's actual 'desperate housewives'...

    Search EZTV

    The final part is a straightforward search of EZTV. The interesting thing to note is that this uses a different type of menu item. Where normally, you'd use a DirectoryItem in a Function, this uses an InputDirectoryItem in a Function. This type of menu item will pop open an on-screen keyboard before calling the target function giving you the opportunity to grab some user input.

    It's appended to the menu in the usual way:

     dir.Append(
      Function(
       InputDirectoryItem(
        SearchEZTV,
        L('MenuSearchTV'),
        "Search the TV shows directory",
        summary="This will use EZTV to search.",
        thumb=R(SEARCH),
        art=R(ART)
       )
      )
     )

    By the way, I think there's a minor bug in the InputDirectoryItem in that it doesn't like it when subtitle is passed as a named argument. I should probably file that as a bug with Elan.

    When the user has entered their input and submitted, the named Function SearchEZTV is called with the standard argument sender and the extra argument query containing the user's input.

    This function was a lot longer in the previous version of the Framework. It was so much simpler this time round.

    def SearchEZTV(sender, query=None):
      dir = MediaContainer()
      url = "http://ezrss.it/search/index.php?simple&mode=rss&show_name="
      if query != None:
       url += "%s" % query
      feed = XML.ElementFromURL(
              url, 
              isHTML=False, 
              errors='ignore'
             ).xpath("//item")
      if feed == None:
        return MessageContainer("Error", "Search failed")
      if len(feed) == 0:
        return MessageContainer("Error", "No results")
      for element in feed:
        title = element.xpath("title")[0].text
        category = element.xpath("category")[0].text
        link = element.find("enclosure").get("url")
        size = prettysize(int(element.find("enclosure").get("length")))
        dir.Append(
          Function(
           DirectoryItem(
            AddTorrent,
            title,
            subtitle=None,
            summary="Category: %s\nSize: %s" % (category,size),
            thumb=R(ICON),
            art=R(ART)
           ),
          torrentUrl=link
         )
        )
      return dir

    Done

    That's it. The only other little thing to mention is how handy it is to use the built-in Log function. The first argument is a standard Python string, the second is 'Should this only turn up in the console when in debug mode?' to which the answer will almost always be 'True'. There is a third argument but unless you're messing with character encodings, you don't need to worry about it.

    Log("Message to log is: %s %d" % (errorString, errorCode), True)

    Go, make...

    If you made it to the end here, you're probably either keen to start making your own Plex plugins or my wife who I am going to get to proofread this. Assuming you're the former, here are some handy links:

    • Online plugin development manual

      There are plenty of bits missing but it's still the best reference available for the framework.

    • Plex forums

      Particularly the Media Server Plugins forum

    • Plex Plugins Lighthouse

      This is where bugs are filed, suggestions made and final plugin submission happens. It's handy for picking little tips if someone else has had the same problem as you.

    If you want to use the plugin, it's available in 'Plex Online' in Plex/Nine or 'Plex App Store' in Plex/Eight. If you'd like to read through the complete source, you can download the zipped .Bundle.

    Transmission Plugin for Plex Media Server v1.0 [Zip - 1.1MB]

    Geek, Development

  • 23 Sep 2010

    Writing a Plex Plugin Part II

    This is part two of my walkthrough of the Plex Media Server Transmission plugin.

    The previous part dealt with the basic required functions and preparing the main menu. This bit goes through the torrent control and the next will cover the built-in site-scraping functionality. To be honest, I'm not sure how much of this middle section will be of use to anyone but other torrent client plugin makers. The cool stuff really happens in the next one. Think of this as the difficult second album that you have to listen to before the return-to-form third.

    Listing the torrents

    This is the main interface to Transmission. Using the RTC method from before, this prepares a request to send via HTTP and reacts depending on the result (or error) we get back. First of all, we say we want information ('torrent-get') and we specify what info we want (the 'fields')

    def TorrentList(sender):
      error, result  = RTC("torrent-get",
        { "fields": [
          "hashString","name","status",
          "eta","errorString",
          "totalSize","leftUntilDone","sizeWhenDone",
          "peersGettingFromUs",  "peersSendingToUs",  "peersConnected",
          "rateDownload",      "rateUpload",
          "downloadedEver",    "uploadedEver"
        ] }
      )

    If we get an error back, we check what it was.

      if error != None:
        if error != "Connection refused":
          return MessageContainer(
              "Transmission unavailable",
              "There was an unknown error."
          )
        else:
          return MessageContainer(
              "Transmission unavailable",
              "Please make sure Transmission is installed and running."
          )

    Now we have our information, we create a MediaContainer to display it. We'll be building these entries up as if they were standard library MediaItems although the final action will not be to play them.

      elif result["torrents"] != None:
        dir = MediaContainer()

    For each set of torrent information we get back, we need to prepare the info and make it pretty.

        for torrent in result["torrents"]:
          progress    = 100;
          summary = ""
     
          if torrent["errorString"]:
            summary += "Error: %s\n" % (torrent["errorString"])

    If we have some time until we're done and we're not seeding, display the progress as "12.3 MB of 45.6 GB (0%)". We add this to the MediaItem's summary field. This is where we use the prettysize and prettyduration functions we imported at the top. They take a computer-friendly value (1048576 bytes) and return a human-friendly one (1MB).

      if torrent["leftUntilDone"] > 0 and
        torrent["status"] != TRANSMISSION_SEEDING:
        progress = ((torrent["sizeWhenDone"] - torrent["leftUntilDone"]) /
              (torrent["sizeWhenDone"] / 100))
     
        summary += "%s of %s (%d%%)\n" % (
            prettysize(torrent["sizeWhenDone"] - torrent["leftUntilDone"]),
            prettysize(torrent["sizeWhenDone"]), progress
          )

    Similarly, if there's an estimated time until the file is finished downloading, add that to the summary as "3 days remaining"

        if torrent["eta"] > 0 and torrent["status"] != TRANSMISSION_PAUSED:
          summary += prettyduration(torrent["eta"]) + " remaining\n"
        else:
          summary += "Remaining time unknown\n"

    Display download status ("Downloading from 3 of 6 peers") and download and upload rates ("Downloading at 3KB/s, Uploading at 1KB/s").

      if torrent["status"] == TRANSMISSION_DOWNLOADING:
        summary += "Downloading from %d of %d peers\n" % (
          torrent["peersSendingToUs"],
          torrent["peersConnected"])
        summary += "Downloading at %s/s\nUploading at %s/s\n" % (
          prettysize(torrent["rateDownload"]),
          prettysize(torrent["rateUpload"]))

    For all other downloading statuses, we don't need extended information so we just return a human-friendly version of the status we get back (we do this via another method below).

      else:
        summary += TorrentStatus(torrent)

    If we're seeding (the torrent has finished downloading and we're just uploading now), write out information about the uploading.

      else:
        if torrent["status"] == TRANSMISSION_SEEDING:
          summary += "Complete\n"
          progress=100
          if torrent["downloadedEver"] == 0:
            torrent["downloadedEver"] = 1

    "45.6GB, uploaded 22.8GB (Ratio 0.50)" and some detail about the people we're uploading to.

      summary += "%s, uploaded %s (Ratio %.2f)\n" % (
        prettysize(torrent["totalSize"]),
        prettysize(torrent["uploadedEver"]),
        float(torrent["uploadedEver"]) / float(torrent["downloadedEver"]))
      if torrent["status"] == TRANSMISSION_SEEDING:
        summary += "Seeding to %d of %d peers\n" % (
          torrent["peersGettingFromUs"],
          torrent["peersConnected"])
        summary += "Uploading at %s/s\n" % (
          prettysize(torrent["rateUpload"]))

    Icon generation

    The next addition was a bit of a tricky point for this version of the plugin. Previous versions generated the thumbnail icon dynamically using the Python Imaging Library (PIL). It would create a progress bar showing the exact percentage and write the name of the file on the icon. In order to be able to achieve this, PIL had to be imported which generated a whole bunch of deprecation warnings. There are rumours that a future version of the plugin framework will include some functionality to generate images on-the-fly (possibly a variation of PIL itself) but until then, I decided the best way forward would be to generate the images by hand and include them in the plugin. This meant that I could either generate 101 images (0% - 100%) or display the percentage rounded off. In order to save space, I went with rounding to the nearest 10%.

      nearest = int(round(progress/10)*10)

    The last thing to do in this loop (remember, we're still looping through the torrent information we received all the way back up at the top of the page) is to actually add this item. It is added as a PopupDirectoryItem so that selecting it will display a context menu of action choices specified in the TorrentInfo method below. With that, we also add the summary we've spent so long crafting, the percentage icon as the thumb and a couple of extra bits of information to help later functions know what to do.

      dir.Append(
        Function(
          PopupDirectoryItem(
            TorrentInfo,
            torrent["name"],
            summary=summary,
            thumb=R("%s.png" % nearest)
          ),
          name = torrent["name"],
          status = torrent["status"],
          hash = torrent["hashString"]
        )
      )

    To finish this menu, we add the same functions that are available to individual torrents but acting on all – 'Pause all' and 'Resume all' – then return the menu to Plex for display.

      dir.Append(
        Function(
          DirectoryItem(
            PauseTorrent,
            L('MenuPauseAll'),
            subtitle=None,
            summary=None,
            thumb=R(PAUSE),
            art=R(ART)
          ),
          hash='all'
        )
      )
      dir.Append(
        Function(
          DirectoryItem(
            ResumeTorrent,
            L('MenuResumeAll'),
            subtitle=None,
            summary=None,
            thumb=R(RESUME),
            art=R(ART)
          ),
          hash='all'
        )
      )
      return dir

    Here's the TorrentStatus lookup. Again, this uses the built-in localisation function 'L' to display the text and, again, I still haven't actually translated any of it so there's still only english. I must get round to that eventually.

    def TorrentStatus(torrent):
      if torrent == None or torrent["status"] == None:
        return L('TorrentStatusUnknown')
      elif torrent["status"] == TRANSMISSION_WAITING:
        return L('TorrentStatusWaiting')
      elif torrent["status"] == TRANSMISSION_CHECKING:
        return L('TorrentStatusVerifying')
      elif torrent["status"] == TRANSMISSION_PAUSED:
        return L('TorrentStatusPaused')
      elif torrent["status"] == TRANSMISSION_DOWNLOADING:
        return L('TorrentStatusDownloading')
      elif torrent["status"] == TRANSMISSION_SEEDING:
        return L('TorrentStatusSeeding')
      else:
        return L('TorrentStatusUnknown')

    Torrent action menu

    This is the popup menu displayed when you select one of the listed torrents. The only thing to notice from these is that the option to pause is only shown for active torrents and the option to resume is only shown for paused torrents. The hash mentioned here is the id of the torrent which will be needed later.

    def TorrentInfo(sender, name, status, hash):
      dir = MediaContainer()
      dir.Append(
        Function(
          DirectoryItem(
            ViewFiles,
            L('MenuViewFiles'),
            subtitle=None,
            summary=None,
            thumb=R(ICON),
            art=R(ART)
          ),
          hash=hash
        )
      )
      if status == TRANSMISSION_PAUSED:
        dir.Append(
          Function(
            DirectoryItem(
              ResumeTorrent,
              L('MenuResume'),
              subtitle=None,
              summary=None,
              thumb=R(ICON),
              art=R(ART)
            ),
            hash=hash
          )
        )
      else:
        dir.Append(
          Function(
            DirectoryItem(
              PauseTorrent,  
              L('MenuPause'),      
              subtitle=None,
              summary=None,
              thumb=R(ICON),
              art=R(ART)
            ),
            hash=hash
          )
        )
        dir.Append(
          Function(
            DirectoryItem(
              RemoveTorrent,
              L('MenuRemove'),
              subtitle=None,
              summary=None,
              thumb=R(ICON),
              art=R(ART)
            ),
            hash=hash
          )
        )
        dir.Append(
          Function(
            DirectoryItem(
              DeleteTorrent,
              L('MenuDelete'),
              subtitle=None,
              summary=None,
              thumb=R(ICON),
              art=R(ART)
            ),
            hash=hash
          )
        )
      return dir

    Torrent action functions

    Each of the torrent action functions called (ViewFiles, ResumeTorrent, etc.) could have been references to a more generic function with an action option passed in but I decided to keep them distinct and separate so that any extra customisation that might be done later would be easier to do rather than hacking it in. This isn't so much a problem with this plugin but if this were to be adapted for another torrent client, there might be specific hoops that needed jumped through.

    I won't go through each of them in detail as they are all very similar. Instead, I'll just describe one of them – RemoveTorrent.

    Each Function menu item (pretty much every menu item in this plugin) takes at least one argument: sender. This tells Plex where it's supposed to return control after it's finished here. The second argument is the id of the torrent we want to act on.

    def RemoveTorrent(sender, hash):

    We define the action to perform and the arguments to pass to Transmission.

      action = "torrent-remove"
      arguments = { "ids" : hash }

    Call Transmission's RPC via the RTC method defined earlier catching the results and any errors returned.

      error, result = RTC(action, arguments)

    If there's an error, any error, display it. Otherwise, display a success method. Both of these are displayed as a popup MessageContainer.

      if error != None:
        return MessageContainer("Error", error)
      else:
        return MessageContainer("Transmission", L('ActionTorrentRemoved'))

    Okay, so the middle section of the plugin might not be that interesting. Next time I'll cover the clever built-in XML parsing bits and everything'll be cool again. I promise.

    Geek, Development

  • 16 Sep 2010

    Writing a Plex Plugin Part I

    In my awesome home cinema setup (about which I'll need to blog sometime), I use many pieces of inter-connected software and hardware. The hub, though, is the OS X computer in the middle running the Plex Media Server and the Transmission BitTorrent client. This post describes the plugin which ties them together.

    I've just released version 1.0 of the Transmission plugin for the Plex Media Server. As with all good software projects, there were actually many releases before 1.0 but I thought this was the right time to write up a walk-through of what the code looks like and does. I didn't write the initial version of the plugin but I've maintained it since v0.7 and now pretty much rewritten everything a couple of times. I'll post my walkthrough in a few installments because it's quite long.

    The code as it currently stands (v1.0) is available for download below but within a few days you should also be able to download it within 'Plex Online'

    Transmission Plugin for Plex Media Server v1.0

    As Michael A. pointed out in the comments, I haven't actually mentioned anywhere below what it is that the plugin does. For a quick overview, check out the Plex wiki Transmission page.

    This plugin is built to be compatible with Plex Plugin Framework v1 which initially looked like quite a major change from the previous version of the framework but isn't really that different. For Plex plugins, every time an action is performed, the system generates a URL and passes it down through its own menu structure until it gets to a plugin that handles that URL. The plugin then deals with the info in the URL however it likes. In the previous versions of this plugin, the URL was parsed manually and split into strings separated by '/'

    For example the URL:

    /video/transmission/id/status/action/

    would first cause Plex to look in its 'video' menu item and find the plugin that said it could handle 'transmission' URLs. The plugin would take the rest of the string and separate it out. First contact the Transmission application, ask for all information about the torrent called 'id', check its 'status' and then perform the 'action' if possible.

    The new plugin does pretty much the same except I no longer manually parse the URL. The plugin still registers with Plex to say it can handle URLs starting '/video/transmission' but then it passes functions through instead of URL-catching menu items. If you're familiar with JavaScript, it's like passing an anonymous function to handle something instead of catching the event manually.

    Anyway, here's the python code with a running commentary:

    Imports

    First we import the Plex Media Server framework:

    from PMS import *
    from PMS.Objects import *
    from PMS.Shortcuts import *

    These are a couple of handy functions from the very first version of the plugin which make the outputs much more readable.

    from texttime  import prettyduration
    from textbytes  import prettysize

    This next line actually causes Plex to issue a warning. These libraries won't all be available in the next version of the framework. Instead of urllib and urllib2, developers are to use the built-in HTTP module. Unfortunately, HTTP doesn't allow access to the headers of responses and the Transmission authentication system relies on exchanging a session ID via headers.

    import urllib,urllib2,base64

    Declarations

    Set up some constants to save typing later on

    PLUGIN_PREFIX = "/video/transmission"
    PLUGIN_TITLE = "Transmission"

    This is the first call to the Localisation module. Plex plugins allow for complete string localisation via a JSON file (I've only included English in this version because my torrent-related German and Japanese are poor). This will look in the JSON file for the key 'Title' and return whatever value is associated with it (or 'Title' if there is none).

    NAME = L('Title')

    More shorthand

    ART        = 'art-default.jpg'
    ICON      = 'icon-default.png'
    SETTINGS  = 'settings-hi.png'
    PAUSE      = 'pause-hi.png'
    RESUME    = 'resume-hi.png'
    SEARCH    = 'search-hi.png'
    TV        = 'tv-hi.png'
    
    TRANSMISSION_WAITING      = 1
    TRANSMISSION_CHECKING      = 2
    TRANSMISSION_DOWNLOADING  = 4
    TRANSMISSION_SEEDING      = 8
    TRANSMISSION_PAUSED        = 16

    Definitions

    Required

    Here is where we start the plugin code. This is one of the standard functions which gets called when the plugin is initialised.

    def Start():

    Tell Plex we can handle '/video/transmission' URLs and that our main function is called 'MainMenu'

      Plugin.AddPrefixHandler(
        PLUGIN_PREFIX, 
        MainMenu, 
        PLUGIN_TITLE, 
        ICON, 
        ART)
    
      MediaContainer.art = R(ART)
      MediaContainer.title1 = NAME
      DirectoryItem.thumb = R(ICON)

    Another standard function, this handles the preferences. To connect to Transmission, you need the URL and port it is running on (127.0.0.1:9091 if it's on the same machine as the Plex Media Server) and the username and password if you have set them.

    def CreatePrefs():
        Prefs.Add(id='hostname', type='text', default='127.0.0.1:9091', label='Hostname')
        Prefs.Add(id='username', type='text', default='', label='Username')
        Prefs.Add(id='password', type='text', default='', label='Password', option='hidden')

    This is called immediately after the preferences dialog is submitted. This is the most basic checking you can do but it could include a call to Transmission to verify the info provided.

    def ValidatePrefs():
        u = Prefs.Get('username')
        p = Prefs.Get('password')
        h = Prefs.Get('hostname')
        if( h ):
            return MessageContainer(
                "Success",
                "Info provided is ok"
            )
        else:
            return MessageContainer(
                "Error",
                "You need to provide url, username, and password"
            )

    You'll notice the return here is a MessageContainer. That's Plex's version of an alert. It doesn't generate a new page, just pops up a little window.

    Custom

    That was the end of the predefined functions, the plugin proper starts here. As Transmission requires a username, password and a short-lived session ID (since Transmission v1.53) to perform actions, we define a function which will attempt to make a connection with just username & password. Transmission will then send back a 409 Conflict response to basically say "Urk, that's not quite right. If you want to talk to me, you'll need this:" and give us our session ID in a header.

    def GetSession():
      h = Prefs.Get('hostname')
      u = Prefs.Get('username')
      p = Prefs.Get('password')
      url = "http://%s/transmission/rpc/" % h
      request = { "method" : "session-get" }
      headers = {}
      if( u and p and h):
        headers["Authorization"] = "Basic %s" % 
          (base64.encodestring("%s:%s" % (u, p))[:-1])
        try:
          body = urllib2.urlopen(
            urllib2.Request(
              url, 
              JSON.StringFromObject(request), 
              headers
            )
          ).read()
        except urllib2.HTTPError, e:
          if e.code == 401 or e.code == 403:
            return L('ErrorInvalidUsername'), {}
          return e.hdrs['X-Transmission-Session-Id']
        except:
          return L('ErrorNotRunning'), {}

    Once the HTTP module allows access to returned headers, we will be able to use something like this to set global authorisation once and forget about it:

    response = HTTP.Request(
        url, 
        { "method" : "session-get" }, 
        headers={}, 
        cacheTime=None
        )
    HTTP.SetPassword(h,u,p)
    HTTP.SetHeader(
      'X-Transmission-Session-Id', 
      response.headers['X-Transmission-Session-Id']
      )

    Remote Transmission Calls

    This uses the RPC API of Transmission to do everything we need. We pass into the function 'What we want to do' and 'Who we want it done to' basically.

    def RTC(method, arguments = {}, headers = {}):
      h = Prefs.Get('hostname')
      u = Prefs.Get('username')
      p = Prefs.Get('password')
      url = "http://%s/transmission/rpc/" % h
    
      session_id = GetSession()
    
      request = {
        "method":    method,
        "arguments":  arguments
      }

    Setup authentication here because, even though we've already gotten the session ID, it's useless if we don't actually use it.

      if( u and p ):
        headers["Authorization"] = "Basic %s" %
          (base64.encodestring("%s:%s" % (u, p))[:-1])
    
      headers["X-Transmission-Session-Id"] = session_id

    Now that we've built our instruction, throw it at Transmission and see what comes back.

      try:
        body = urllib2.urlopen(
          urllib2.Request(
            url, 
            JSON.StringFromObject(request), 
            headers)
          ).read()
      except urllib2.HTTPError, e:
        if e.code == 401 or e.code == 403:
          return L('ErrorInvalidUsername'), {}
        return "Error reading response from Transmission", {}
      except urllib2.URLError, e:
        return e.reason[1], {}
    
      result = JSON.ObjectFromString(body)

    We don't do error handling here as we want this function to be as generic as possible so we send anything we receive straight back to the calling function.

      if result["result"] == "success":
        result["result"] = None
    
      if result["arguments"] == None:
        result["arguments"] = {}
    
      return result["result"], result["arguments"]

    Menus

    Right, we've got our helper methods set up, we're ready to make our first menu. This is the main one we mentioned earlier.

    def MainMenu():

    You can set your menu screen to be laid out as “List”, “InfoList”, “MediaPreview”, “Showcase”, “CoverFlow”, “PanelStream” or “WallStream”. I'm keeping it simple here. Also, there's an extra call to GetSession just to check everything's fine and wake the app up.

        dir = MediaContainer(viewGroup="List")
        GetSession()

    Pretty much all the menu items throughout the rest of this plugin are added using the same code which boils down to:

      dir.Append(
        Function(
          DirectoryItem(
            FunctionName,
            "Pretty Menu Item Name",
            subtitle="Short subtitle",
            summary="Longer menu item summary and description",
            thumb=R(ICON),
            art=R(ART)
          )
        )
      )

    Starting in the middle, this reads as:

    • Create a DirectoryItem.
    • When this item is selected, use the function FunctionName to handle it.
    • Display the text "Pretty Menu Item Name" for this item
    • Display the text "Short subtitle" underneath this item (or None)
    • Display the text "Longer menu item summary and description" for this item if required (or None)
    • Use the resource called ICON (mentioned above) as the icon for this item
    • Use the resource ART as the background
    • This is a Function menu item
    • Finally, Append this to the current menu

    The first two main menu items are built exactly like that:

      dir.Append(
        Function(
          DirectoryItem(
            TorrentList,
            "Torrents",
            subtitle=None,
            summary="View torrent progress and control your downloads.",
            thumb=R(ICON),
            art=R(ART)
          )
        )
      )
      dir.Append(
        Function(
          DirectoryItem(
            SearchTorrents,
              "Search for a torrent",
              subtitle=None,
              summary="Browse the TV shows directory or search for files to download.",
              thumb=R(SEARCH),
              art=R(ART)
            )
          )
        )

    This is a special 'Preferences' item that will call the Prefs functions defined at the top.

      dir.Append(
        PrefsItem(
          title="Preferences",
          subtitle="Set Transmission access details",
          summary="Make sure Transmission is running and 'Remote access' is enabled then enter the access details here.",
          thumb=R(SETTINGS)
        )
      )

    A quick note: the function 'R' here, much like the localisation one 'L' above is a built-in helper function. It handles resources such as images. If you're developing a plugin and can't understand why your icon images are caching so much, it might be because you're going through this function.

    Send the directory (or Menu) back to Plex

        return dir

    The rest of the code deals with torrent control and some clever built-in site scraping functionality which I'll cover later.

    Geek, Development

  • 19 Aug 2010

    Maze 1k

    • Random perfect maze generation, solution and rendering in 1011 bytes

    Okay, this is the last one for a while. Really.

    Unlike my previous 1k JavaScript demos, I really had to struggle to get this one into the 1024 byte limit. I'd already done all the magnification and optimization techniques I knew of so I had to bring in some things which were new to me from qfox.nl and Ben Alman and a few other places. This, combined with some major code refactoring, brought it down from 1.5k to just under 1. In the process, all possible readability was lost so here's a quick run through what it does and why.

    First up, a bunch of declarations.

    These are the colours used to draw the maze. Note, I used the shortest possible way to write each colour (red instead of #F00, 99 instead of 100). The value stored in the maze array (mentioned later) refers not only to the type of block it is but also to the index of the colour in this array, saving some space.

    u = ["#eff", "red", "#122", "rgba(99,255,99,.1)", "#ff0"],

    This is used for the number of blocks wide and high the maze is, the number of pixels per block, the size of the canvas and the redraw interval. Thanks to Ben Alman for pointing out in his article how to best use a constant.

     c = 21,

    Here is the reference to the canvas. Mid-minification, I did have a bunch of function shortcuts here - r=Math.random, for instance - but I ended up refactoring them out of the code.

    m = document.body.children[0];

    For most of the time when working on this, the maze was wider than it was high because I thought that made a more interesting maze. When it came down to it, though, it was really a matter of bytesaving to drop the distinct values for width and height. After that, we grab the canvas context so we can actually draw stuff.

    m.width = m.height = c * c;
    h = m.getContext("2d");

    The drawing function

    The generation and solution algorithm is quite nice and all but without this to draw it on the screen, it's really just a mathematical curio. This takes a colour, x and y and draws a square.

    l = function (i, e, f) {
      h.fillStyle = i;
      h.fillRect(e * c, f * c, c, c)
    };

    Designing a perfect maze

    "You've got 1 minute to design a maze it takes 2 minutes to solve."
    - Cobb, Inception.

    Apologies for the unnecessary Inception quote. It's really not relevant.

    Algorithmically, this is a fairly standard perfect maze generator. It starts at one point and randomly picks a direction to walk in then it stops picks another random direction and repeats. If it can't move, it goes back to the last choice it made and picks a different direction, if there are no more directions, all blocks have been covered and we're done. In a perfect maze, there is a path (and only one path) between any two randomly chosen points so we can make the maze then denote the top left as the start and the bottom right as the end. This particular algorithm takes 2 steps in every direction instead of 1 so that we effectively carve out rooms and leave walls. You can take single steps but it's actually more of a hassle.

    For more on how this kind of maze-generation works, check out this article on Tile-based maze generation.

    Blank canvas

    This is a standard loop to create a blank maze full of walls with no corridors. The 2 represents the 'wall' block type and the colour #122. The only really odd thing about this is the code f-->0 which is not to be read 'as f tends to zero' but is instead 'decrement f by 1, is it greater than zero?'

    g = function () {
      v = [];  //our stack of moves taken so we can retrace our steps.
      for (i = [], e = c; e-- > 0;) {
        i[e] = [];
        for (f = c; f-- > 0;) i[e][f] = 2
      }

    By this point, we have a two-dimensional JavaScript array filled with 2s

    Putting things in

      f = e = 1;    // our starting point, top left.
      i[e][f] = 0; // us, the zippy yellow thing

    Carving out the walls

    This is our first proper abuse of the standard for-loop convention. You don't need to use the three-part structure for 'initialize variable; until variable reaches a different value; change value of variable'. It's 'do something before we start; keep going until this is false; do this after every repetition' so here we push our first move onto the stack then repeat the loop while there's still a move left on the stack.

      for (v.push([e, f]); v.length;) {

    P here is the list of potential moves from our current position. For every block, we have a look to see what neighbours are available then concatenate that cardinal direction onto the strong of potential moves. This was originally done with bitwise flags (the proper way) but it ended up longer. It's also a bit of a nasty hack to set p to be 0 instead of "" but, again, it's all about the bytes.

      p = 0;

    Can we walk this way?

    These are all basically the same and mean 'if we aren't at the edge of the board and we're looking at a wall, we can tunnel into it.'.

     if (e < 18 && i[e + 2][f] == 2) p += "S"
     if (e >= 2 && i[e - 2][f] == 2) p += "N";
     if (f >= 2 && i[e][f - 2] == 2) p += "W";
     if (i[e][f + 2] == 2) p += "E";
    
     if (p) { //    If we've found at least one move
      switch (p[~~ (Math.random() * p.length)]) { // randomly pick one

    If there was anything to note from that last chunk, it would be that the operator ~~ can be used to floor the current value. It will return the integer below thye current value.

    Take two steps

    This is a nice little hack. Because we're moving two spaces, we need to set the block we're on and the next one to be 0 (empty). This takes advantage of the right-associative unary operator 'decrement before' and the right associativity of assignment operators. It subtracts 1 from e (to place us on the square immediately above) then sets that to equal 0 then subtracts 1 from the new e (to put us on the next square up again) and sets that to equal the same as the previous operation, i.e. 0.

      case "N":
          i[--e][f] = i[--e][f] = 0;
          break;

    Do the same for s, w and e

        case "S":
          i[++e][f] = i[++e][f] = 0;
          break;
        case "W":
          i[e][--f] = i[e][--f] = 0;
          break;
        case "E":
          i[e][++f] = i[e][++f] = 0
        }

    Whichever move we chose, stick that onto the stack.

        v.push([e, f])

    If there were no possible moves, backtrack

      } else {
        b = v.pop(); //take the top move off the stack
        e = b[0]; // move there
        f = b[1]
      }
     }

    End at the end

    At the very end, set the bottom right block to be the goal then return the completed maze.

      i[19][19] = 1;
      return i
    };

    Solver

    This is the solving function. Initially, it used the same algorithm as the generation function, namely 'collect the possible moves, randomly choose one' but this took too much space. So instead it looks for spaces north, then south, then west, then east. It follows the first one it finds.

    s = function () {

    Set the block type of the previous block as 'visited' (rgba(99,255,99,.1) the alpha value makes the yellow fade to green).

      n[o][y] = 3;

    A bit of ternary background

    This next bit looks worse than it is. It's the ternary operator nested several times. The ternary operator is a shorthand way of writing:

    if ( statement A is true ) {
      Do Action 1
    } else {
      Do Action 2
    }

    In shorthand, this is written as:

    Statement A ? Action 1 : Action 2;

    In this, however, I've replace Action 2 with another ternary operator:

    Statement A ? Action 1 : ( Statement B ? Action 2 : Action 3 );

    And again, and again. Each time, it checks a direction, if it's empty, mark it as visited and push the move onto our stack.

    (n[o + 1][y] < 2) ?
      (n[++o][y] = 0, v.push([o, y])) :
        (n[o - 1][y] < 2) ?
          (n[--o][y] = 0, v.push([o, y])) :
            (n[o][y - 1] < 2) ?
              (n[o][--y] = 0, v.push([o, y])) :
                (n[o][y + 1] < 2) ?
                  (n[o][++y] = 0, v.push([o, y])) :

    If none of the neighbours are available, backtrack

                    (b = v.pop(), o = b[0], y = b[1]);

    Show where we are

    Finally, set our new current block to be yellow

      n[o][y] = 4;

    Are we there yet?

    If we are at the bottom right square, we've completed the maze

      if (o == 19 && y == 19) {
      n = g();    //Generate a new maze
      o = y = 1; //Move us back to the top right
      s()     //Solve again

    If we haven't completed the maze, call the solve function again to take the next step but delay it for 21 milliseconds so that it looks pretty and doesn't zip around the maze too fast.

      } else setTimeout(s, c);

    Paint it black. And green. And yellow.

    This is the code to render the maze. It starts at the top and works through the whole maze array calling the painting function with each block type (a.k.a. colour) and position.

        for (d in n) for (a in n[d]) l(u[n[d][a]], a, d)
      };

    Start

    This is the initial call to solve the maze. The function s doesn't take any arguments but by passing these in, they get called before s and save a byte that would have been used if they had been on a line themselves.

    s(n = g(), o = y = 1)

    Done

    This little demo isn't as visually appealing as the Art Maker 1k or as interactive as the Spinny Circles 1k but it is quite nice mathematically. There are now some astounding pieces of work in the JS1K demo competition, though. I do recommend spending a good hour playing about with them all. Don't, however, open a bunch of them in the background at the same time. Small code isn't necessarily memory-efficient and you could quite easily grind your computer to a standstill.

    Geek, Javascript, Development

  • newer posts
  • older posts

Categories

Toys, Guides, Opinion, Geek, Non-geek, Development, Design, CSS, JS, Open-source Ideas, Cartoons, Photos

Shop

Colourful clothes for colourful kids

I'm currently reading

Projects

  • Awsm Street – Kid's clothing
  • Stickture
  • Explanating
  • Open Source Snacks
  • My life in sans-serif
  • My life in monospace
Simon Madine (thingsinjars)

@thingsinjars.com

Hi, I’m Simon Madine and I make music, write books and code.

I’m the Engineering Lead for komment.

© 2026 Simon Madine