Feedparser

Permalink

Nachdem ich im vorangegangenen Artikel über die Steuerung eines Audio-Players mittels einer Playlist geschrieben habe soll es diesmal darum gehen wie diese Playlist überhaupt zustande kommt.

Für die Einträge der Playlist zu erhalten bemühe ich die News-Feeds der gewünschten Podcasts. Um die wesentlichen Aspekte nicht aus den Augen zu verlieren sind das im folgenden lediglich zwei fest vorgegebene URL’s. Der Zugriff auf diese kleine Liste an Feeds erfolgt über eine prototypische Funktion readSourceList().

var readSourceList = function() {
  "use strict";
  var sources = ['http://feeds.feedburner.com/cre-podcast', 'http://chaosradio.ccc.de/chaosradio-latest.rss'];
  return sources;
};

Interessant wird es jetzt: Für jeden Eintrag dieser Liste erzeuge ich einen Ajax-Request und verarbeite den Response:

var i, tracks = [], sources;
sources = readSourceList();
for (i = 0; i < sources.length; i++) {
  tracks = tracks.concat(downloadSource(sources[i]));
}
var downloadSource = function(source) {
  "use strict";
  var tracks, successfunction;
  tracks = [];
  successfunction = new function(data, jqXHR) {
    parseFeed(data, tracks);
  };
  $.ajax({
    'url': source,
    'async': false,
    'dataType': 'xml',
    'success': successfunction
  });
  return tracks;
};
var parseFeed = function(feed, tracks) {
  "use strict";
  var xml = $(feed);
  xml.find('item').has('enclosure').slice(0, 5).each(function() {
    tracks.push({
      'uri': $(this).find('link:first').text(),
      'title': $(this).find('title:first').text(),
      'mediaUrl' : $(this).find('enclosure:first').attr('url'),
      'updated' : new Date($(this).find('pubDate:first').text()),
      'source' : xml.find('channel > title').text()
    });
  });
};

Leider tritt hier auch schon das erste Problem zutage. Aufgrund von Sicherheitssystemen in den Browsern kann man leider keine URL’s fremder Domains mittels Ajax aufrufen. Grund dafür ist die Sicherheitstechnik Same-Origin-Policy. Für dieses Problem gibt es leider keine einfache Lösung dafür aber zwei unterschiedliche Ansätze.

Cross-Origin Resource Sharing

Um die SOP zu umgehen wurde das Cross-Origin Resource Sharing entwickelt. Hierbei erlaubt der Anbieter einer Ressource explizit den Zugriff auf diese durch Dritte. Dies geschieht mittels eines speziellen HTTP-Headers.

Access-Control-Allow-Origin: http://lab.human-injection.de

Wenn alle Podcasts die ich hören möchte diesen Header für ihre Feeds setzten würden könnte ich die Feeds ohne Beeinträchtigung verarbeiten. Aber alle Anbieter von Podcasts anzuschreiben und zu überzeugen das doch mal einzuführen ist doch eher utopisch.

HTTP-Proxy

Als alternative unter eigener Kontrolle kommt ein Proxy in frage. Dieser Proxy muss unter der selben Domain wie der Podcatcher erreichbar sein um die Same-Origin-Policy (SOP) zu umgehen. Aufgabe des Proxys ist es eine URL als Parameter entgegenzunehmen. Vom Server aus diese URL aufzurufen und das Ergebnis an den Client weiterzuleiten.

Ich habe für Testzwecke einen sehr rudimentären Proxy in Python geschrieben um das ganze testen zu können. Näheres hierzu gibt es in einem eigenen Artikel namens Umgehungsstraße

Um den Proxy zu verwenden muss man lediglich den Ajax-Request so anpassen das er nicht den Feed direkt lädt sondern das ganze über den Proxy umleitet. Da so ein Proxy natürlich immer etwas langsamer ist als eine direkte Verbindung habe ich beide Aufrufe kombiniert. Zuerst wird versucht den Feed direkt zu laden. Kommt es dabei zu einem Fehler wird ein zweiter Versuch mit dem Proxy unternommen. Dies geschieht mittels der Error-Callback-Funktion des Ajax-Requests. Einige Browser wie Chrome lösen zusätzlich eine Exception aus wenn sie einen Verstoß gegen die SOP feststellen. Diese muss zusätzlich abgefangen werden damit das Script nicht abbricht.

var downloadSource = function(source) {
  "use strict";
  var tracks, successfunction, errorfunction;
  tracks = [];
  successfunction = function(data) {
    parseFeed(data, tracks);
  };
  errorfunction = function() {
    $.ajax({
      'url': "sourceparsing.py?url=" + source,
      'async': false,
      'dataType': 'xml',
      'success': successfunction
    });
  };
  try {
    $.ajax({
      'url': source,
      'async': false,
      'dataType': 'xml',
      'success': successfunction,
      'error': errorfunction
    });
  } catch (ignore) {}
  return tracks;
};

Das Ergebnis dieser Funktion ist ein Array von Objekten mit den Daten der einzelnen Episoden. Dieses Array kann man nun sortieren und daraus das benötigte HTML-Markup erzeugen.

Ausblick

Wenn auch wie erwartet die Same-Origin-Policy zugeschlagen hat, so ist es mir doch gelungen einen praktikablen Weg zu finden das weite, weite Internet anzuzapfen um an neuen Stoff für meine Ohren zu kommen. Damit jeder sich selbst überzeugen kannst habe ich auch wieder einen Prototyp bereitgestellt der die hier besprochene Funktionalität bereitstellt. Und zu meinem Glück funktioniert der auch in Opera 12 und 15, Firefox 22, Chrome 28, sowie Internet Explorer 10 und 11. Unter Android klappt es mit Opera, Chrome und Firefox.

Mit der Technik aus diesem und dem vorangegangenen Artikel ist es bereits möglich eine Liste von Episoden zusammenzustellen und auch abzuspielen. Eine weitere Anforderung ist aber die Offlinefähigkeit. Als nächstes muss ich also einen Weg finden sowohl die HTML- und Javascript-Codes, die Konfiguration und die Playlist sowie die MP3-Dateien im Browser zu speichern.