Task 848: .XSPF File Format

Task 848: .XSPF File Format

1. Properties of the .XSPF File Format Intrinsic to Its File System

The .XSPF (XML Shareable Playlist Format) is an XML-based format for playlists, defined by a specific structure rooted in the <playlist> element. It does not possess binary headers or magic numbers typical of non-text formats, as it is a text-based XML file. Properties intrinsic to its file system include the file extension (.xspf), MIME type (application/xspf+xml), and encoding (typically UTF-8). The format's core properties are derived from its XML schema, encompassing elements and attributes that define the playlist's metadata and content. Below is a comprehensive list of these properties, based on the official XSPF Version 1 specification:

Root Element: <playlist>

  • Attributes:
  • xmlns: Specifies the namespace (required; value: "http://xspf.org/ns/0/").
  • version: Indicates the format version (required; value: "1").
  • Child Elements:
  • <title>: Human-readable title of the playlist (optional; plain text; exactly one).
  • <creator>: Name of the entity that authored the playlist (optional; plain text; exactly one).
  • <annotation>: Human-readable comment or note (optional; plain text, no markup; exactly one).
  • <info>: URI for additional information or purchase (optional; URI; exactly one).
  • <location>: URI of the playlist resource itself (optional; URI; exactly one).
  • <identifier>: Canonical, location-independent identifier (optional; URI; zero or more).
  • <image>: URI of an image to display for the playlist (optional; URI; exactly one).
  • <date>: Creation date in XML Schema dateTime format (e.g., "2005-01-08T17:10:47-05:00") (optional; exactly one).
  • <license>: URI describing the license for the playlist (optional; URI; zero or one).
  • <attribution>: Container for attribution URIs (optional; exactly one).
  • Child Elements:
  • <location>: URI for attribution (optional; URI; zero or more, ordered).
  • <identifier>: Identifier for attribution (optional; URI; zero or more, ordered).
  • <link>: Links to related resources (optional; zero or more).
  • Attributes:
  • rel: URI identifying the relation type (required).
  • Content: URI of the linked resource.
  • <meta>: Custom metadata (optional; zero or more).
  • Attributes:
  • rel: URI defining the metadata type (required).
  • Content: Plain text value (no markup).
  • <extension>: Application-specific extensions (optional; zero or more).
  • Attributes:
  • application: URI defining the nested XML structure (required).
  • Content: Arbitrary nested XML.
  • <trackList>: Container for tracks (required; exactly one).
  • Child Elements: Zero or more <track> elements.

Track Element: <track> (within <trackList>)

  • Child Elements:
  • <location>: URI of the media resource (optional; URI; zero or more, but only one should be rendered).
  • <identifier>: Canonical identifier for the track (optional; URI; zero or more).
  • <title>: Human-readable title of the track (optional; plain text; exactly one).
  • <creator>: Name of the track's author (optional; plain text; exactly one).
  • <annotation>: Human-readable comment (optional; plain text, no markup; exactly one).
  • <info>: URI for track information or purchase (optional; URI; exactly one).
  • <image>: URI of an image for the track (optional; URI; exactly one).
  • <album>: Name of the album or collection (optional; plain text; exactly one).
  • <trackNum>: Positive integer indicating track position (optional; non-negative integer; exactly one).
  • <duration>: Duration in milliseconds (optional; non-negative integer; exactly one; hint only).
  • <link>: Links to related resources (optional; zero or more; same attributes and content as playlist-level <link>).
  • <meta>: Custom metadata (optional; zero or more; same attributes and content as playlist-level <meta>).
  • <extension>: Application-specific extensions (optional; zero or more; same attributes and content as playlist-level <extension>).

These properties ensure the format's portability and extensibility, with URIs resolved according to XML Base and RFC 2396 standards.

3. HTML JavaScript for Drag-and-Drop .XSPF File Parsing (Embeddable in Ghost Blog)

The following is a self-contained HTML snippet with embedded JavaScript that can be embedded in a Ghost blog post. It provides a drag-and-drop area where users can drop a .XSPF file. Upon dropping, the script parses the XML, extracts all properties from the list above, and displays them on the screen in a structured format.

XSPF Parser
Drag and drop a .XSPF file here

4. Python Class for .XSPF File Handling

The following Python class uses the xml.etree.ElementTree module to open, parse (decode), read, write, and print all properties from a .XSPF file.

import xml.etree.ElementTree as ET

class XSPFHandler:
    def __init__(self):
        self.root = None
        self.namespace = '{http://xspf.org/ns/0/}'

    def load(self, file_path):
        """Load and parse the XSPF file."""
        tree = ET.parse(file_path)
        self.root = tree.getroot()
        if self.root.tag != self.namespace + 'playlist':
            raise ValueError("Not a valid XSPF file")

    def get_properties(self):
        """Retrieve all properties as a dictionary."""
        if not self.root:
            raise ValueError("No file loaded")
        
        properties = {
            'playlist': {
                'attributes': {
                    'xmlns': self.root.get('xmlns'),
                    'version': self.root.get('version')
                },
                'elements': {}
            },
            'tracks': []
        }

        # Extract playlist elements
        for child in self.root:
            tag = child.tag.replace(self.namespace, '')
            if tag == 'trackList':
                continue
            elif tag in ['title', 'creator', 'annotation', 'info', 'location', 'identifier', 'image', 'date', 'license', 'album', 'trackNum', 'duration']:
                properties['playlist']['elements'][tag] = child.text.strip() if child.text else None
            elif tag == 'attribution':
                properties['playlist']['elements']['attribution'] = {
                    'locations': [loc.text.strip() for loc in child.findall(self.namespace + 'location')],
                    'identifiers': [id.text.strip() for id in child.findall(self.namespace + 'identifier')]
                }
            elif tag == 'link':
                rel = child.get('rel')
                properties['playlist']['elements'].setdefault('links', []).append({rel: child.text.strip()})
            elif tag == 'meta':
                rel = child.get('rel')
                properties['playlist']['elements'].setdefault('metas', []).append({rel: child.text.strip()})
            elif tag == 'extension':
                app = child.get('application')
                properties['playlist']['elements'].setdefault('extensions', []).append({app: ET.tostring(child, encoding='unicode')})

        # Extract tracks
        track_list = self.root.find(self.namespace + 'trackList')
        if track_list:
            for track in track_list.findall(self.namespace + 'track'):
                track_props = {'elements': {}}
                for child in track:
                    tag = child.tag.replace(self.namespace, '')
                    if tag in ['title', 'creator', 'annotation', 'info', 'location', 'identifier', 'image', 'album', 'trackNum', 'duration']:
                        track_props['elements'][tag] = [child.text.strip()] if child.text else []
                    elif tag == 'link':
                        rel = child.get('rel')
                        track_props['elements'].setdefault('links', []).append({rel: child.text.strip()})
                    elif tag == 'meta':
                        rel = child.get('rel')
                        track_props['elements'].setdefault('metas', []).append({rel: child.text.strip()})
                    elif tag == 'extension':
                        app = child.get('application')
                        track_props['elements'].setdefault('extensions', []).append({app: ET.tostring(child, encoding='unicode')})
                properties['tracks'].append(track_props)

        return properties

    def print_properties(self):
        """Print all properties to console."""
        props = self.get_properties()
        print("XSPF Properties:")
        print("\nPlaylist Attributes:")
        for key, value in props['playlist']['attributes'].items():
            print(f"  {key}: {value}")
        print("\nPlaylist Elements:")
        for key, value in props['playlist']['elements'].items():
            print(f"  {key}: {value}")
        
        for i, track in enumerate(props['tracks'], 1):
            print(f"\nTrack {i} Elements:")
            for key, value in track['elements'].items():
                print(f"  {key}: {value}")

    def save(self, file_path):
        """Write the XSPF back to a file."""
        if not self.root:
            raise ValueError("No file loaded")
        tree = ET.ElementTree(self.root)
        tree.write(file_path, encoding='utf-8', xml_declaration=True)

# Example usage:
# handler = XSPFHandler()
# handler.load('example.xspf')
# handler.print_properties()
# handler.save('output.xspf')

5. Java Class for .XSPF File Handling

The following Java class uses javax.xml.parsers.DocumentBuilder to open, parse, read, write, and print all properties from a .XSPF file.

import org.w3c.dom.*;
import javax.xml.parsers.*;
import javax.xml.transform.*;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;

public class XSPFHandler {
    private Document document;
    private final String namespace = "http://xspf.org/ns/0/";

    public void load(String filePath) throws Exception {
        DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
        factory.setNamespaceAware(true);
        DocumentBuilder builder = factory.newDocumentBuilder();
        document = builder.parse(new File(filePath));
        if (!document.getDocumentElement().getLocalName().equals("playlist")) {
            throw new IllegalArgumentException("Not a valid XSPF file");
        }
    }

    public void printProperties() throws Exception {
        if (document == null) {
            throw new IllegalStateException("No file loaded");
        }

        System.out.println("XSPF Properties:");

        Element playlist = document.getDocumentElement();
        System.out.println("\nPlaylist Attributes:");
        System.out.println("  xmlns: " + playlist.getAttribute("xmlns"));
        System.out.println("  version: " + playlist.getAttribute("version"));

        System.out.println("\nPlaylist Elements:");
        printElementProperties(playlist, false);

        Node trackList = playlist.getElementsByTagNameNS(namespace, "trackList").item(0);
        if (trackList != null) {
            NodeList tracks = ((Element) trackList).getElementsByTagNameNS(namespace, "track");
            for (int i = 0; i < tracks.getLength(); i++) {
                System.out.println("\nTrack " + (i + 1) + " Elements:");
                printElementProperties((Element) tracks.item(i), true);
            }
        }
    }

    private void printElementProperties(Element element, boolean isTrack) {
        String[] simpleElements = {"title", "creator", "annotation", "info", "location", "identifier", "image", 
                                    "date", "license", "album", "trackNum", "duration"};
        for (String tag : simpleElements) {
            NodeList nodes = element.getElementsByTagNameNS(namespace, tag);
            for (int j = 0; j < nodes.getLength(); j++) {
                Node node = nodes.item(j);
                if (node.getParentNode() == element) {
                    System.out.println("  " + tag + ": " + (node.getTextContent() != null ? node.getTextContent().trim() : "null"));
                }
            }
        }

        // Attribution (playlist only)
        if (!isTrack) {
            Element attribution = (Element) element.getElementsByTagNameNS(namespace, "attribution").item(0);
            if (attribution != null) {
                System.out.println("  attribution:");
                NodeList locations = attribution.getElementsByTagNameNS(namespace, "location");
                for (int j = 0; j < locations.getLength(); j++) {
                    System.out.println("    location: " + locations.item(j).getTextContent().trim());
                }
                NodeList identifiers = attribution.getElementsByTagNameNS(namespace, "identifier");
                for (int j = 0; j < identifiers.getLength(); j++) {
                    System.out.println("    identifier: " + identifiers.item(j).getTextContent().trim());
                }
            }
        }

        // Links, metas, extensions
        NodeList links = element.getElementsByTagNameNS(namespace, "link");
        for (int j = 0; j < links.getLength(); j++) {
            Element link = (Element) links.item(j);
            if (link.getParentNode() == element) {
                System.out.println("  link (rel=" + link.getAttribute("rel") + "): " + link.getTextContent().trim());
            }
        }
        NodeList metas = element.getElementsByTagNameNS(namespace, "meta");
        for (int j = 0; j < metas.getLength(); j++) {
            Element meta = (Element) metas.item(j);
            if (meta.getParentNode() == element) {
                System.out.println("  meta (rel=" + meta.getAttribute("rel") + "): " + meta.getTextContent().trim());
            }
        }
        NodeList extensions = element.getElementsByTagNameNS(namespace, "extension");
        for (int j = 0; j < extensions.getLength(); j++) {
            Element ext = (Element) extensions.item(j);
            if (ext.getParentNode() == element) {
                System.out.println("  extension (application=" + ext.getAttribute("application") + "): " + nodeToString(ext));
            }
        }
    }

    private String nodeToString(Node node) throws Exception {
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.setOutputProperty(OutputKeys.OMIT_XML_DECLARATION, "yes");
        StringWriter writer = new StringWriter();
        transformer.transform(new DOMSource(node), new StreamResult(writer));
        return writer.getBuffer().toString().trim();
    }

    public void save(String filePath) throws Exception {
        if (document == null) {
            throw new IllegalStateException("No file loaded");
        }
        TransformerFactory tf = TransformerFactory.newInstance();
        Transformer transformer = tf.newTransformer();
        transformer.setOutputProperty(OutputKeys.INDENT, "yes");
        DOMSource source = new DOMSource(document);
        StreamResult result = new StreamResult(new File(filePath));
        transformer.transform(source, result);
    }

    // Example usage:
    // public static void main(String[] args) throws Exception {
    //     XSPFHandler handler = new XSPFHandler();
    //     handler.load("example.xspf");
    //     handler.printProperties();
    //     handler.save("output.xspf");
    // }
}

6. JavaScript Class for .XSPF File Handling

The following JavaScript class uses DOMParser to open (via FileReader), parse, read, write (to Blob for download), and print all properties from a .XSPF file to the console.

class XSPFHandler {
    constructor() {
        this.xmlDoc = null;
        this.namespace = 'http://xspf.org/ns/0/';
    }

    load(file) {
        return new Promise((resolve, reject) => {
            const reader = new FileReader();
            reader.onload = (event) => {
                try {
                    const parser = new DOMParser();
                    this.xmlDoc = parser.parseFromString(event.target.result, 'application/xml');
                    if (this.xmlDoc.getElementsByTagName('parsererror').length > 0 || 
                        this.xmlDoc.documentElement.localName !== 'playlist') {
                        throw new Error('Not a valid XSPF file');
                    }
                    resolve();
                } catch (error) {
                    reject(error);
                }
            };
            reader.readAsText(file);
        });
    }

    getProperties() {
        if (!this.xmlDoc) {
            throw new Error('No file loaded');
        }

        const properties = {
            playlist: {
                attributes: {
                    xmlns: this.xmlDoc.documentElement.getAttribute('xmlns'),
                    version: this.xmlDoc.documentElement.getAttribute('version')
                },
                elements: {}
            },
            tracks: []
        };

        const playlist = this.xmlDoc.documentElement;
        const simpleElements = ['title', 'creator', 'annotation', 'info', 'location', 'identifier', 'image', 
                                'date', 'license', 'album', 'trackNum', 'duration'];
        simpleElements.forEach(tag => {
            const nodes = playlist.getElementsByTagNameNS(this.namespace, tag);
            for (let node of nodes) {
                if (node.parentNode === playlist) {
                    properties.playlist.elements[tag] = node.textContent.trim();
                }
            }
        });

        // Attribution
        const attribution = playlist.getElementsByTagNameNS(this.namespace, 'attribution')[0];
        if (attribution) {
            properties.playlist.elements.attribution = {
                locations: Array.from(attribution.getElementsByTagNameNS(this.namespace, 'location')).map(loc => loc.textContent.trim()),
                identifiers: Array.from(attribution.getElementsByTagNameNS(this.namespace, 'identifier')).map(id => id.textContent.trim())
            };
        }

        // Links, metas, extensions
        const links = playlist.getElementsByTagNameNS(this.namespace, 'link');
        properties.playlist.elements.links = Array.from(links).filter(link => link.parentNode === playlist).map(link => ({
            rel: link.getAttribute('rel'),
            value: link.textContent.trim()
        }));
        const metas = playlist.getElementsByTagNameNS(this.namespace, 'meta');
        properties.playlist.elements.metas = Array.from(metas).filter(meta => meta.parentNode === playlist).map(meta => ({
            rel: meta.getAttribute('rel'),
            value: meta.textContent.trim()
        }));
        const extensions = playlist.getElementsByTagNameNS(this.namespace, 'extension');
        properties.playlist.elements.extensions = Array.from(extensions).filter(ext => ext.parentNode === playlist).map(ext => ({
            application: ext.getAttribute('application'),
            content: ext.innerHTML.trim()
        }));

        // Tracks
        const trackList = playlist.getElementsByTagNameNS(this.namespace, 'trackList')[0];
        if (trackList) {
            const tracks = trackList.getElementsByTagNameNS(this.namespace, 'track');
            for (let track of tracks) {
                const trackProps = { elements: {} };
                simpleElements.forEach(tag => {
                    const nodes = track.getElementsByTagNameNS(this.namespace, tag);
                    for (let node of nodes) {
                        if (node.parentNode === track) {
                            trackProps.elements[tag] = [node.textContent.trim()];
                        }
                    }
                });
                trackProps.elements.links = Array.from(track.getElementsByTagNameNS(this.namespace, 'link')).map(link => ({
                    rel: link.getAttribute('rel'),
                    value: link.textContent.trim()
                }));
                trackProps.elements.metas = Array.from(track.getElementsByTagNameNS(this.namespace, 'meta')).map(meta => ({
                    rel: meta.getAttribute('rel'),
                    value: meta.textContent.trim()
                }));
                trackProps.elements.extensions = Array.from(track.getElementsByTagNameNS(this.namespace, 'extension')).map(ext => ({
                    application: ext.getAttribute('application'),
                    content: ext.innerHTML.trim()
                }));
                properties.tracks.push(trackProps);
            }
        }

        return properties;
    }

    printProperties() {
        const props = this.getProperties();
        console.log('XSPF Properties:');
        console.log('\nPlaylist Attributes:');
        console.log(props.playlist.attributes);
        console.log('\nPlaylist Elements:');
        console.log(props.playlist.elements);
        props.tracks.forEach((track, index) => {
            console.log(`\nTrack ${index + 1} Elements:`);
            console.log(track.elements);
        });
    }

    save() {
        if (!this.xmlDoc) {
            throw new Error('No file loaded');
        }
        const serializer = new XMLSerializer();
        const xmlStr = serializer.serializeToString(this.xmlDoc);
        const blob = new Blob([xmlStr], { type: 'application/xml' });
        return blob;  // Can be used to download, e.g., via URL.createObjectURL(blob)
    }
}

// Example usage:
// const handler = new XSPFHandler();
// handler.load(file).then(() => {
//     handler.printProperties();
//     const blob = handler.save();
//     // Download blob if needed
// });

7. C Implementation for .XSPF File Handling

Since C does not support classes natively, the following implementation uses structures and functions. It relies on the libxml2 library (a common XML parsing library for C; assume it is installed) to open, parse, read, write, and print all properties from a .XSPF file.

#include <stdio.h>
#include <string.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
#include <libxml/xmlstring.h>

#define NAMESPACE "http://xspf.org/ns/0/"

typedef struct {
    xmlDocPtr doc;
} XSPFHandler;

void xspf_init(XSPFHandler *handler) {
    handler->doc = NULL;
}

int xspf_load(XSPFHandler *handler, const char *file_path) {
    handler->doc = xmlReadFile(file_path, NULL, 0);
    if (handler->doc == NULL) {
        fprintf(stderr, "Failed to parse %s\n", file_path);
        return -1;
    }
    xmlNodePtr root = xmlDocGetRootElement(handler->doc);
    if (root == NULL || xmlStrcmp(root->name, (const xmlChar *)"playlist") != 0) {
        fprintf(stderr, "Not a valid XSPF file\n");
        xmlFreeDoc(handler->doc);
        handler->doc = NULL;
        return -1;
    }
    return 0;
}

void print_node_properties(xmlNodePtr node, int is_track) {
    const char *simple_elements[] = {"title", "creator", "annotation", "info", "location", "identifier", "image",
                                      "date", "license", "album", "trackNum", "duration", NULL};
    for (int i = 0; simple_elements[i]; i++) {
        xmlNodePtr cur = node->children;
        while (cur) {
            if (cur->type == XML_ELEMENT_NODE && xmlStrcmp(cur->name, (const xmlChar *)simple_elements[i]) == 0 &&
                xmlStrcmp(cur->ns->href, (const xmlChar *)NAMESPACE) == 0) {
                xmlChar *content = xmlNodeGetContent(cur);
                printf("  %s: %s\n", simple_elements[i], content ? (char *)content : "null");
                xmlFree(content);
            }
            cur = cur->next;
        }
    }

    // Attribution (playlist only)
    if (!is_track) {
        xmlNodePtr attribution = xmlFirstElementChild(node);
        while (attribution) {
            if (xmlStrcmp(attribution->name, (const xmlChar *)"attribution") == 0) {
                printf("  attribution:\n");
                xmlNodePtr child = attribution->children;
                while (child) {
                    if (child->type == XML_ELEMENT_NODE) {
                        xmlChar *content = xmlNodeGetContent(child);
                        printf("    %s: %s\n", (char *)child->name, content ? (char *)content : "null");
                        xmlFree(content);
                    }
                    child = child->next;
                }
                break;
            }
            attribution = attribution->next;
        }
    }

    // Links, metas, extensions
    xmlNodePtr cur = node->children;
    while (cur) {
        if (cur->type == XML_ELEMENT_NODE && xmlStrcmp(cur->ns->href, (const xmlChar *)NAMESPACE) == 0) {
            if (xmlStrcmp(cur->name, (const xmlChar *)"link") == 0) {
                xmlChar *rel = xmlGetProp(cur, (const xmlChar *)"rel");
                xmlChar *content = xmlNodeGetContent(cur);
                printf("  link (rel=%s): %s\n", rel ? (char *)rel : "null", content ? (char *)content : "null");
                xmlFree(rel);
                xmlFree(content);
            } else if (xmlStrcmp(cur->name, (const xmlChar *)"meta") == 0) {
                xmlChar *rel = xmlGetProp(cur, (const xmlChar *)"rel");
                xmlChar *content = xmlNodeGetContent(cur);
                printf("  meta (rel=%s): %s\n", rel ? (char *)rel : "null", content ? (char *)content : "null");
                xmlFree(rel);
                xmlFree(content);
            } else if (xmlStrcmp(cur->name, (const xmlChar *)"extension") == 0) {
                xmlChar *app = xmlGetProp(cur, (const xmlChar *)"application");
                // For simplicity, print first-level content
                xmlChar *content = xmlNodeGetContent(cur);
                printf("  extension (application=%s): %s\n", app ? (char *)app : "null", content ? (char *)content : "null");
                xmlFree(app);
                xmlFree(content);
            }
        }
        cur = cur->next;
    }
}

void xspf_print_properties(XSPFHandler *handler) {
    if (handler->doc == NULL) {
        fprintf(stderr, "No file loaded\n");
        return;
    }

    printf("XSPF Properties:\n");

    xmlNodePtr playlist = xmlDocGetRootElement(handler->doc);
    printf("\nPlaylist Attributes:\n");
    xmlChar *xmlns = xmlGetProp(playlist, (const xmlChar *)"xmlns");
    xmlChar *version = xmlGetProp(playlist, (const xmlChar *)"version");
    printf("  xmlns: %s\n", xmlns ? (char *)xmlns : "null");
    printf("  version: %s\n", version ? (char *)version : "null");
    xmlFree(xmlns);
    xmlFree(version);

    printf("\nPlaylist Elements:\n");
    print_node_properties(playlist, 0);

    xmlNodePtr track_list = playlist->children;
    while (track_list) {
        if (track_list->type == XML_ELEMENT_NODE && xmlStrcmp(track_list->name, (const xmlChar *)"trackList") == 0) {
            xmlNodePtr track = track_list->children;
            int track_num = 1;
            while (track) {
                if (track->type == XML_ELEMENT_NODE && xmlStrcmp(track->name, (const xmlChar *)"track") == 0) {
                    printf("\nTrack %d Elements:\n", track_num++);
                    print_node_properties(track, 1);
                }
                track = track->next;
            }
            break;
        }
        track_list = track_list->next;
    }
}

int xspf_save(XSPFHandler *handler, const char *file_path) {
    if (handler->doc == NULL) {
        fprintf(stderr, "No file loaded\n");
        return -1;
    }
    return xmlSaveFormatFileEnc(file_path, handler->doc, "UTF-8", 1);
}

void xspf_free(XSPFHandler *handler) {
    if (handler->doc) {
        xmlFreeDoc(handler->doc);
        handler->doc = NULL;
    }
}

// Example usage:
// int main() {
//     XSPFHandler handler;
//     xspf_init(&handler);
//     if (xspf_load(&handler, "example.xspf") == 0) {
//         xspf_print_properties(&handler);
//         xspf_save(&handler, "output.xspf");
//     }
//     xspf_free(&handler);
//     xmlCleanupParser();
//     return 0;
// }