I couldn't let it lie. The nifty JavaScript from the previous post was all well and good but I had to have a go at jQuery plugin-ifying it. It has been Enpluginated.
If you still have no idea what I'm talking about, you can read about the attribute. There are still a couple of bugs when the scoped blocks are deeply nested within other scoped areas so I'm hoping someone with a keen sense of how to straighten out Webkit oddness can help. When browsers don't implement functionality, it's a bit tricky to guess what they'd mean.
Aside from that, it's cross-browser (IE7+) compatible and ready to use. I'm interested to know if anyone finds it useful or finds any odd combinations of styles that don't scope nicely.
One of the things in HTML5 that caught my eye a while back was the new 'scoped' attribute for style tags. The idea of it is that you can include a style element mid-document, mark it as scoped and its declarations will only apply to the style elements parent element and its child elements. The styles won't affect anything outside this. The biggest problem with bringing in this attribute is that it's not backwards compatible. If you include a style block mid-page, its declarations will be applied to ever element whose selector matches, inside or outside scope. It is anti-progressively enhancable. This means that designers and developers can't start using it until there's enough support. What we need is another of those JS tricks to make it usable.
My first attempt at solving this problem with JS involved generating a unique class, adding it to the parent element and then parsing the style blocks using JSCSSP so that I could rewrite them with the new class to add specificity. This approach only worked for the most basic declarations, unfortunately. The parser worked perfectly but there's a lot of detail in CSS specificity that mean this would be a much larger problem than I thought.
My second attempt involved:
Allowing the style blocks to affect everything on the page (at which point, the elements in-scope look right, those out-of-scope look wrong)
Using JS to read the current computed styles of each element in-scope and copy them to a temporary array
Emptying out the scoped style element (in-scope look wrong, out-of-scope looks right)
Copying the styles back from the temporary array onto each element
This worked great unless you had more than one scoped block – step 1 allowed scoped styles to affect each other.
The current attempt involves temporarily emptying all other scoped styles before taking the computed styles from a block. I'm now just thinking that this method might not work if you have multiple scoped blocks within the same context. Oh well, there's something to fix in the future.
Yes, it's a mess, yes the JS is scrappy and yes, it doesn't currently work in IE but I'll get round to that next. It took long enough to get it working in Firefox as there's no simple way to convert a ComputedCSSStyleDeclaration to a string in Mozilla unlike Webkit's implementation of cssText or IE's currentStyle. I might even make it into one of those new-fangled jQuery plugins everyone's using these days.
You may have seen Google's 'Watch this space' advertising appearing all over the place. They have quite eye-catching diagonally striped backgrounds in various colours. A couple of days ago, I was wondering how easy it would be to recreate this in CSS without images. Unfortunately, the state of CSS 3 is such that some things work wonderfully, some just plain don't (scoped attribute, I'm looking at you). The following code relies on vendor extensions and so, unless you're willing to tend it and correct it after the spec is finalised, don't use this on a production server.
The most obvious thing to notice from the following code, though, is the competing syntax for the repeating linear gradient style. Mozilla have separated it into a distinct style (-moz-repeating-linear-gradient) while Webkit have built it as an option to their general gradient style (-webkit-gradient).
To get a better idea of what this does, view source on this demo page. This includes a button to change the class on the body (using JS) which simply changes the background colour – the stripes are semi-transparent on top of that. Remember, due to the vendor prefixes, this only works in -moz or -webkit browsers.
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.
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.
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)
)
)
)
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:
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.