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.
2. Direct Download Links for .XSPF Files
- https://radioemisoras.cl/alternativafm.xspf (A playlist for a radio stream titled "Alternativa Radio FM 104.7 Ancud," containing one track with metadata such as title, annotation, and info.)
- https://realfm.net/fresh.xspf (A playlist for a radio stream from "Real FM Radio - FRESH," containing one track with metadata such as title, annotation, and info.)
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.
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;
// }