mashTape Provider API

Introduction

The new mashTape 0.2.0 add-on allows developers to extend it by developing new providers to provide additional content sources for Songbird users.  While mashTape comes bundled with a set of default providers, it can be extended via simple JavaScript extensions.

Examples

If you're the kind of person who wants to look at some code while reading this guide, we've implemented two additional providers to show how to extend mashTape.

Architecture

The mashTape.idl interface file describes the types of providers possible.  In a nutshell there exist 4 classes of providers, one for each of the 4 tabs mashTape supports:

  • Artist Info (sbIMashTapeInfoProvider)
  • News (sbIMashTapeRSSProvider)
  • Photos (sbIMashTapePhotoProvider)
  • Videos (sbIMashTapeFlashProvider)

To implement a mashTape extension, you simply need to provide an XPCOM component that implements one of these 4 interfaces.  All 4 of these interfaces are derived from sbIMashTapeProvider.

sbIMashTapeProvider

To be a mashTape provider, regardless of which specific sub-type you implement, your component will need to provide a minimum of two attributes (the provider name & type), and a query() method:

readonly attribute string providerName;
readonly attribute string providerType;
void query(in AUTF8String searchTerms, in sbIMashTapeCallback updateFn);

providerName

providerName is simply a freeform string naming your provider; this is visible to the end-user in the mashTape preference pane so they can enable and disable individual services. 

providerType

providerType is a short text string noting which class of provider your provider falls into:

  • info (for Artist Info)
  • rss (for News)
  • photo (for Photos)
  • flash (for Videos)

Your provider *must* have providerType set to one of those 4 strings, or else it will fail to be detected.

query()

The query method simply takes in two parameters, the first being the search terms.  mashTape will call the query() method passing in the artist name for the searchTerms.  The second parameter is a callback function within mashTape that takes care of the rendering of results.

sbIMashTape{Photo|RSS|Flash}Provider

These three types of providers, at the moment, simply extend sbIMashTapeProvider by having a providerIcon attribute which is simply a string pointing to a 16x16 favicon-style image for the provider.  Typically these should be bundled within the chrome of your add-on so that they loaded locally, but they can also point to remote sites (e.g. http://getsongbird.com/favicon.ico)

sbIMashTapeInfoProvider

The artist info provider extends sbIMashTapeProvider by having 5 different providerIcon fields:

  • providerIconBio
  • providerIconTags
  • providerIconDiscography
  • providerIconLinks
  • providerIconMembers

These are the various providerIcon strings for the different sections an artist info provider can provide.  sbIMashTapeInfoProviders must also provide a numSections attribute which is an integer count of the # of sections the infoProvider provides.  e.g. if your provider implements all 5 sections, numSections should be 5.  If it only implements the Biography & Discography, numSections should be 2.

Developing a mashTape Provider Add-on

We'll now take a look at putting together an add-on, specifically we'll look at how the Del.icio.us add-on is put together.  If you grab the Del.icio.us XPI and unzip it, you'll see the following files:

./chrome/content/favicon.ico
./chrome/content/icon.png
./chrome/content/main.xul
./chrome.manifest
./components/Delicious.js
./install.rdf

The favicon.ico is our providerIcon mentioned above, the 16x16 favicon from http://delicious.com/favicon.ico.  The icon.png is referenced from install.rdf as the add-on icon to display in the add-ons manager and on the add-ons site.  The convention is to simply overlay the 16x16 favicon on top of the mashTape "tape" icon.

Since there isn't "chrome" in the traditional sense of a Mozilla add-on, the rest of the chrome sub-directory is empty, and the chrome.manifest reflects this by merely setting pointers to the paths:

chrome.manifest

content mt_delicious chrome/content/
skin mt_delicious classic/1.0 chrome/skin/

There is nothing special in the install.rdf file, it's a standard vanilla install.rdf.

install.rdf

<?xml version="1.0"?>
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
     xmlns:em="http://www.mozilla.org/2004/em-rdf#"
     xmlns:songbird="http://www.songbirdnest.com/2007/addon-metadata-rdf#">
    <Description rdf:about="urn:mozilla:install-manifest">
        <em:id>delicious@grommit.com</em:id>
        <em:name>Delicious mashTape Provider </em:name>
        <em:version>0.1</em:version>
        <em:creator>Stephen Lau (stevel@songbirdnest.com)</em:creator>
        <em:description>Adds a Delicious mashTape RSS/News provider</em:description>
        <em:iconURL>chrome://mt_delicious/content/icon.png</em:iconURL>
        <em:homepageURL>http://mashtape.mozdev.org</em:homepageURL>
        <em:targetApplication>
            <Description>
                <em:id>songbird@songbirdnest.com</em:id>
                <em:minVersion>0.8.0pre</em:minVersion>
                <em:maxVersion>0.8.0pre</em:maxVersion>
            </Description>
        </em:targetApplication>
    </Description>
</RDF>

You can see the <em:iconURL> that points to the add-on icon in the chrome/content/icon.png, but otherwise this is a totally standard vanilla install.rdf file.

This leaves us with only the provider XPCOM Javascript file.  You can name it whatever you like, it just needs to be in the components sub-directory so Songbird will detect it during its scan for XPCOM components during initialisation.

Delicious.js

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;

const DESCRIPTION = "mashTape Provider: Delicious";
const CID         = "{6c780af0-8048-11dd-ad8b-0800200c9a66}";
const CONTRACTID  = "@songbirdnest.com/mashTape/provider/rss/Delicious;1";

// XPCOM constructor for our Delicious mashTape provider
function Delicious() {
	this.wrappedJSObject = this;
}

Delicious.prototype.constructor = Delicious;
Delicious.prototype = {
	classDescription: DESCRIPTION,
	classID:          Components.ID(CID),
	contractID:       CONTRACTID,
	QueryInterface: XPCOMUtils.generateQI([Ci.sbIMashTapeRSSProvider,
			Ci.sbIMashTapeProvider]),

	providerName: "Delicious",
	providerType: "rss",
	providerUrl: "http://del.icio.us",
	providerIcon: "chrome://mt_delicious/content/favicon.ico",

	query: function(artist, callback) {
		var req = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
			.createInstance(Ci.nsIXMLHttpRequest);
		var url = "http://feeds.delicious.com/v2/rss/tag/" + artist;
		var name = this.providerName;
		var providerurl = this.providerUrl;
		req.open("GET", encodeURI(url), true);
		req.onreadystatechange = function(ev) {
			return function(updateFn, providerName, providerUrl) {
				if (req.readyState != 4)
					return;
				if (req.status == 200) {
					var results = new Array();
					var x = new XML(req.responseText.replace(
						/<\?xml version="1.0" encoding="[uU][tT][fF]-8"\s?\?>/,
						""));
					for each (var entry in x..item) {
						var pubDate = entry.pubDate.toString();
						pubDate = pubDate.replace(/\+0000$/, "GMT");
						var timestamp = new Date(pubDate);

						var item = {
							title: entry.title,
							url: entry.link,
							time: timestamp.getTime(),
							provider: providerName,
							providerUrl: providerUrl,
							content: entry.description,
						}
						results.push(item);
					}
					
					results.wrappedJSObject = results;
					updateFn.wrappedJSObject.update(CONTRACTID, results);
				}
			}(callback, name, providerurl);
		}
		req.send(null);
	},
}

var components = [Delicious];
function NSGetModule(compMgr, fileSpec) {
	return XPCOMUtils.generateModule([Delicious]);
}

You can see this is a pretty simple component.  It simply creates our Delicious object, giving it a description, classID, and contractID.  You MUST generate a unique contract ID for each provider.  The UUID Web Generator page is a great page to go to to get a guaranteed unique ID for your provider.

QueryInterface: XPCOMUtils.generateQI([Ci.sbIMashTapeRSSProvider,
			Ci.sbIMashTapeProvider]),

This line uses the convenient XPCOMUtils methods (imported from XPCOMUtils.jsm on the first line of Delicious.js) to generate the appropriate QueryInterface lines setting our Delicious provider to implement both the sbIMashTapeRSSProvider & sbIMashTapeProvider interfaces.

Being an sbIMashTapeProvider, it now needs to provide providerName & providerType, as well as query().  To implement sbIMashTapeRSSProvider it needs to provide providerIcon as well, you can see all of these implemented in the lines following QueryInterface:

 

providerName: "Delicious",
	providerType: "rss",
	providerUrl: "http://del.icio.us",
	providerIcon: "chrome://mt_delicious/content/favicon.ico",

	query: function(artist, callback) {

The providerUrl is not part of any interface, it's just a convenient shortcut used later on in query()

Making the actual query()

All a mashTape provider needs to do, generally, is make an outgoing asynchronous web call (using XMLHttpRequest) and then format the results back in such a way that mashTape can interpret them, and pass the results back to mashTape via the callback.  In the body of the query() method above you can see it sets up the XMLHttpRequest and issues it asynchronously.  Within the onreadystatechange handler, where it gets back the data, it uses E4X to parse the XML.  It then formats it into the format needed for a mashTape RSS Provider, and then issues a call to the callback function.  Generally, the results need to be an array.  In order to pass the array back via XPCOM, we need to do the following trick:

results.wrappedJSObject = results;

We then pass this to the update function callback via:

updateFn.wrappedJSObject.update(CONTRACTID, results);

The update() routine takes two parameters, the Contract ID of our provider (in order to match it up against which providers it made outgoing query() calls to), and the actual results array.

Formatting the results

The results need to be formatted differently depending on what class your provider is.

Artist Info

RSS/News

results needs to be an array of elements, where each element is an object consisting of:

 Attribute
Description
 title  The title of the entry
 url  The "Read More" detail link to the entry for further reading
 time  The timestamp (in EPOCH format, # of seconds since Jan 1, 1970)
 provider  The name of the provider (typically the same as .providerName)
 providerUrl  The URL to the provider's homepage
 content  The actual contents of the article to be read from within mashTape

 

Photo

results needs to be an array of elements, where each element is an object consisting of:

 Attribute
Description
 title  The title of the photo
 url  The link for more detail on the photo
 small  The URL to the small version of the photo
 medium  The URL to the medium version of the photo
 large  The URL to the large version of the photo
 owner
 The photo author's name
 ownerUrl
 The link to the author's page for more info on the author
 time
 The timestamp (in EPOCH format, # of seconds since Jan 1, 1970)
 width
 The width of the small image
 height  The height of the small image

 

Flash/Video

results needs to be an array of elements, where each element is an object consisting of:

 Attribute
Description
 title  The title of the entry
 url  The link to the page for more information on this video
 swfUrl  The direct URL to the swf (used as the src for the object embed)
 duration  The length in seconds of the video if available (set to 0 if not)
 time  The timestamp (in EPOCH format, # of seconds since Jan 1, 1970)
 thumbnail  The thumbnail image of the video
 author  The video author's name
 authorUrl  The link to the author's page for more info on the author
 description  The description of the video
 provider  The name of the provider
 providerUrl  The link to the provider's homepage
 ratio
 The ratio to attempt to keep the photo during resizing (width / height)
 width  The width of the video
 height  The height of the video

 

Extra Credit: Checking for mashTape Presence

For bonus points, you can add a few simple lines to check to make sure mashTape is installed in case a user installs your provider without having installed mashTape previously.  Simply add the following line to your chrome.manifest:

overlay chrome://songbird/content/xul/layoutBaseOverlay.xul chrome://mt_delicious/content/main.xul

Replacing "mt_delicious" with the path to your chrome content URL.  And then add the following chrome/content/main.xul file:

<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet href="chrome://shoutcast-radio/skin/sps.css" type="text/css"?>
<overlay id="shoutcast-radio-overlay" xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
	<script type="application/x-javascript">
		window.addEventListener("load", function() {
			if (!("sbIMashTapeProvider" in Components.interfaces)) {
				var msg = "You installed the Delicious mashTape provider which requires the mashTape add-on to also be installed.  Do you want to install this add-on now?";
				if (confirm(msg)) {
					installXPI("http://addons.songbirdnest.com/addon/73/compatible-xpi");
				}
			}
		}, false);
	</script>
</overlay>
Tag page
You must login to post a comment.