thingsinjars

  • 15 Oct 2010

    The Elementals

    In one of my day jobs I do something involving education, large public institutions and web stuff and a while back I thought it might be an excellent idea to have a go at designing some cool educational toys. Learnin' 'n' Fun! At the same time! omg! And so forth!

    The idea was to build the kind of thing you could use to squeeze knowledge into people's heads without them complaining. Y'see, it's never a good thing to trick people into learning. If your educational toy/game/experience relies too much on hiding the information behind the fun then the big reveal at the end – "Ha, I tricked you into learning something!" – will leave the player feeling cheated and not change their attitude towards learning. If, on the other hand, you try and push the ideas you want to get across at the expense of the core game mechanic, you'll end up with a bored user. My opinion is that you've got to be up front about the learning. You've got to say to the user "Look, this is learning and it's fun. No tricks here, it's exactly what it looks like". As for getting it to appeal in the first place, I find that very few things can beat extremely cute cartoons.

    To that end, I present my first dabble in interactive educational whaddyamacallits: The Elementals, a fun periodic table where every element has its own unique personality.

    The Elementals

    It's available as an iPhone app initially but I'll be venturing into the Android Marketplace soon and putting it online as a web app.

    Available on the App Store

    Geek, iOS, Design

  • 11 Oct 2010

    Appington concept

    Note: this is a concept sketch only. This doesn't actually exist. It'd be cool if it did, though.

    Appington. Your applications brought to you.

    Appington LogoAppington is, fundamentally, a single-application VNC client with a simple interface for task switching. Where most VNC applications present the user with the entire screen, Appington only shows a single window at any one time. This simplified interface makes interaction easier and saves on client application memory and reduces data transfer allowing the viewer to be more responsive. In some applications, this data transfer saving may be used to facilitate audio capture and transmission.

    Applications list

    This screen shows a list of all available applications grouped by first letter. In the lower-left, the user can toggle between listing all applications or only listing currently running applications. The right-hand panel shows more information about the selected application. In this example, Google Chrome is selected and running. The current memory and CPU usage are shown along with a note of how many active windows the application has. Because Chrome is currently running, the option to quit is shown. If we had selected an unlaunched application, this button would show the option to launch. In case of emergencies, there is always the option to Force Quit a running application.

    Application window (portrait)

    This shows a standard single application window. The button in the top left would return the user to the previous screen. From the right, the remaining buttons allow the user to capture a screen shot, maximize the application window to match the current iPad viewport (if possible), refresh the current screen (in case of render errors) and access the application's menu. In OS X, menu access would be accomplished by way of the accessibility API. At the moment, I'm not sure how it would work on other OSs.

    Application window (landscape)

    This shows a single window of an application with multiple windows. You'll notice the extra button at the top between Menu and Refresh. This menu will allow you to select which window you want to access between however many the application currently has open.

    Other images

    The partner application to this is a modified VNC server running on the host machine. It is responsible for window management, task-switching, menu parsing and audio capture (à la SoundFlower). If there is already a VNC server running, the partner app will leave the standard VNC functionality alone and act purely as a helper, providing the extra functionality but not using extra memory by duplicating functionality. This is a variation of noVNC using the python proxy to manage the socket connection allowing the client to be built in PhoneGap using HTML 5.

    Like I said at the top, this hasn't been built yet. It'd be cool if someone did build it, though.

    Ideas, Geek

  • 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

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

© 2025 Simon Madine