<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	>

<channel>
	<title>xscDevBlog - LastSharp &#38; Co. &#187; C#</title>
	<atom:link href="http://dev.xscheme.de/category/codeschnipsel/c/feed/" rel="self" type="application/rss+xml" />
	<link>http://dev.xscheme.de</link>
	<description>Der xscheme-DevelopmentBlog</description>
	<lastBuildDate>Sun, 23 May 2010 11:40:10 +0000</lastBuildDate>
	<generator>http://wordpress.org/?v=2.9.1</generator>
	<language>en</language>
	<sy:updatePeriod>hourly</sy:updatePeriod>
	<sy:updateFrequency>1</sy:updateFrequency>
			<item>
		<title>[Proof of Concept] SharpConnect</title>
		<link>http://dev.xscheme.de/2010/03/proof-of-concept-sharpconnect/</link>
		<comments>http://dev.xscheme.de/2010/03/proof-of-concept-sharpconnect/#comments</comments>
		<pubDate>Wed, 03 Mar 2010 00:22:40 +0000</pubDate>
		<dc:creator>WordPress</dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[Projekte]]></category>
		<category><![CDATA[Theorie]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=989</guid>
		<description><![CDATA[Nachdem ich in den letzten Tagen an einem Konzept getüftelt habe, wie man verschiedene webbasierte APIs einfach zugänglich machen könnte, möchte ich nun eine beispielhafte Implementierung vorführen: SharpConnect. (Download DLL) Und das Ziel meiner Wahl ist natürlich Last.FM, was auch sonst. Wohlgemerkt gehe ich hier nicht auf die Umsetzung ein, nur auf die Verwendung.
Es stellt [...]]]></description>
			<content:encoded><![CDATA[<p>Nachdem ich in den letzten Tagen an einem <a href="http://dev.xscheme.de/2010/03/concept-generischer-api-zugriff/">Konzept</a> getüftelt habe, wie man verschiedene webbasierte APIs einfach zugänglich machen könnte, möchte ich nun eine beispielhafte Implementierung vorführen: <strong>SharpConnect</strong>. (<a href="http://dev.xscheme.de/sources/LastConnect.dll">Download DLL</a>) Und das Ziel meiner Wahl ist natürlich Last.FM, was auch sonst. Wohlgemerkt gehe ich hier nicht auf die Umsetzung ein, nur auf die Verwendung.</p>
<p>Es stellt sich zuerst die Frage: <strong>Was wollen wir?</strong> Ich für meinen Teil würde z.B. gerne wissen, wo und wann Kasabian demnächst Konzerte geben (Nicht, dass die Chance bestünde, das München auf der Liste wäre, aber dennoch&#8230;), und somit böte sich der API-Aufruf <a href="http://www.lastfm.de/api/show/?service=117">artist.getEvents</a> zur näheren Betrachtung an: <strong>Welche Parameter hat er, wie muss er ausgeführt werden und wie sieht die Antwort aus?</strong></p>
<p>Wir sehen, dass es zwei Parameter gibt (&#8220;artist&#8221; und &#8220;api_key&#8221;, beide benötigt) und dass die Antwort ein XML-Dokument ist, dass die Wurzel &#8220;lfm&#8221; hat, anschließend den Knoten &#8220;events&#8221; und viele Kindknoten &#8220;event&#8221;, die die einzelnen Konzerte enthalten. Diese wiederum besitzen Werte für die auftretenden Künstler, den Ort, die Zeit, Ticketverkäufe, etc&#8230; Das Beispiel von der API-Seite:</p>
<pre class="brush:xml">&lt;events artist="Cher" xmlns:geo="http://www.w3.org/2003/01/geo/wgs84_pos#" total="4"&gt;
&lt;event&gt;
    &lt;id&gt;599858&lt;/id&gt;
  &lt;title&gt;Cher&lt;/title&gt;
  &lt;artists&gt;
    &lt;artist&gt;Cher&lt;/artist&gt;
    &lt;headliner&gt;Cher&lt;/headliner&gt;
  &lt;/artists&gt;
  &lt;venue&gt;
    &lt;name&gt;The Colosseum at Caesars Palace&lt;/name&gt;
    &lt;location&gt;
      &lt;city&gt;Las Vegas&lt;/city&gt;
      &lt;country&gt;United States&lt;/country&gt;
      &lt;street&gt;&lt;/street&gt;
      &lt;postalcode&gt;&lt;/postalcode&gt;
      &lt;geo:point&gt;
         &lt;geo:lat&gt;36.2265501474709&lt;/geo:lat&gt;
         &lt;geo:long&gt;-115.0048828125&lt;/geo:long&gt;
      &lt;/geo:point&gt;
      &lt;timezone&gt;PST&lt;/timezone&gt;
     &lt;/location&gt;
    &lt;url&gt;http://www.last.fm/venue/8841108&lt;/url&gt;
  &lt;/venue&gt;
  &lt;startDate&gt;Sat, 16 Aug 2008&lt;/startDate&gt;
  &lt;startTime&gt;19:30&lt;/startTime&gt;
  &lt;description&gt;&lt;/description&gt;
  &lt;image size="small"&gt;...&lt;/image&gt;
  &lt;image size="medium"&gt;...&lt;/image&gt;
  &lt;image size="large"&gt;...&lt;/image&gt;
  &lt;attendance&gt;42&lt;/attendance&gt;
  &lt;reviews&gt;0&lt;/reviews&gt;
  &lt;tag&gt;lastfm:event=669027&lt;/tag&gt;
  &lt;url&gt;http://www.last.fm/event/599858&lt;/url&gt;
  &lt;website&gt;http://...&lt;/website&gt;
  &lt;tickets&gt;
    &lt;ticket supplier="..."&gt;http://...&lt;/ticket&gt;
    ...
  &lt;/tickets&gt;
&lt;/event&gt;
...
&lt;/events&gt;
</pre>
<p><span id="more-989"></span>SharpConnect stellt die folgenden Klassen zur Verfügung:</p>
<ul>
<li><strong>SharpConnect.Apitizer.Apitizer</strong> bzw. <strong>SharpConnect.Xml.XmlApitizer</strong> zur Verwaltung von Methoden</li>
<li><strong>SharpConnect.Apitizer.Method</strong> zur Darstellung von Methoden</li>
<li><strong>SharpConnect.Apitizer.Parameters</strong> zur Darstellung von Parametern</li>
<li><strong>SharpConnect.Apitizer.Accessor</strong> als Zugriffsklasse für Antworten</li>
<li><strong>SharpConnect.Apitizer.Rule</strong> bzw. <strong>SharpConnect.Xml.XmlRule</strong> zur Darstellung von Accessor-Regeln</li>
<li><strong>SharpConnect.Apitizer.Codec</strong> bzw. <strong>SharpConnect.Xml.XmlCodec</strong> zur Umwandlung von Anfragen und Antworten.</li>
</ul>
<p>Mehr brauchen wir eigentlich nicht, und auch, wenn das keine wirklich kurze Liste ist, die Verwendung der einzelnen Klassen ist nicht besonders kritisch. Beginnen wir mit dem schwersten, dem <strong>Codec</strong>.</p>
<p>Wenn wir diesen auf Basis von XmlCodec aufbauen, haben wir die Hälfte schon geschafft:  die Antworten (<em>Response</em>) werden automatisch in einen Accessor für XML umgewandelt und können anschließend anhand der Regeln, die wir noch definieren werden, durchlaufen werden. Was wir noch machen müssen, ist, die Daten, die gesendet werden sollen (<em>Request</em>), vorzubereiten. LastFM erwartet die einzelnen Parameter als Query-String von Name-Wert-Paaren, also z.B.:</p>
<pre class='brush:plain'>method=artist.getEvents&amp;artist=Cher&amp;api_key=b25b959554ed76058ac220b7b2e0a026</pre>
<p>Der API-Key wird immer angehängt, bei Anfragen, die Daten verändern, sogar noch eine Session und ein API-Secret, diese lassen wir jedoch hier unter den Tisch fallen. Man muss es ja nicht gleich übertreiben. Unser Last.FM Codec könnte also folgendermaßen aussehen:</p>
<pre class="brush:c-sharp"> public class LastFMCodec : XmlCodec
 {
   private string apiKey;

   public LastFMCodec(string key)
     : base("lfm", null)
   {
     this.apiKey = key;
   }

   public override Request Encode(Method m, Parameters p)
   {
     string method = m.Name;
     string query = "method=" + method + "&amp;";
     query += p.QueryString("&amp;", "=");
     query += "api_key=" + this.apiKey + "&amp;";
     if (m.IsPOST) return new Request("", query);
     else return new Request(query, "");
   }
 }
</pre>
<p>Das wars auch schon. Das meiste wird ohnehin von SharpConnect erledigt, sodass wir uns nun auf den <strong>Apitizer</strong> konzentrieren können. Auch hier ist die Klasse XmlApitizer eine wunderbare Grundlage, die nicht viele Ergänzungen benötigt:</p>
<pre class="brush:c-sharp"> public class LastFMApitizer : XmlApitizer
 {
   public LastFMApitizer(string key, WebProxy p)
     : base("http://ws.audioscrobbler.com/2.0/", new LastFMCodec(key), p)
   {
      AddMethods(this);
   }

   private static void AddMethods(XmlApitizer api) {
     // Methods here!
   }
 }
</pre>
<p>Wir haben nun also einen Apitizer erstellt, der alle Anfragen an &#8220;http://ws.audioscrobbler.com/2.0/&#8221; sendet und den soeben definierten Codec verwendet. Nun müssen wir dem Apitizer nur noch sagen, was er eigentlich kann.</p>
<p>Eine Methode wird definiert durch</p>
<ul>
<li>einen Namen,</li>
<li>eine Liste von Parameter-Namen,</li>
<li>eine Liste von boolschen Werten, die angibt, welche Parameter benötigt werden, sowie</li>
<li>einen Wert der angibt, ob die Anfrage über HTTP-POST läuft oder nicht.</li>
</ul>
<p>In unserem Fall sieht dies nun folgendermaßen aus (Der API-Key wird im Encoder automatisch hinzugefügt!):</p>
<pre class="brush:c-sharp"> Method artistGetEvents = new Method(
   "artist.getEvents",
   new String[] { "artist" },
   new bool[] { true },
   false
 );</pre>
<p>Was machen wir nun mit der Antwort dieses Aufrufs? Die Antwort: wir verwenden die Klasse <strong>XmlRule</strong>, um Zuordnungen zwischen Attributnamen und Knoten des Antwortdokuments herzustellen. Das Mittel der Wahl ist hierbei XPath.</p>
<pre class="brush:c-sharp;"> XmlRule Image = new XmlRule();
 Image.Set("URL", "self::node()");
 Image.Set("Size", "@size");

 XmlRule Event = new XmlRule();
 Event.Set("Name", "title");
 Event.Set("ID", "id");
 Event.Set("Artists", "artists/artist");
 Event.Set("Headliner", "artists/headliner");
 Event.Set("Date", "startDate");
 Event.Set("Time", "startTime");
 Event.Set("Description", "description");
 Event.Set("Images", "image", Image);             // !!!
 Event.Set("Attendance", "attendance");
 Event.Set("ReviewCount", "reviews");
 Event.Set("UniqueTag", "tag");
 Event.Set("Website", "website");

 XmlRule rule = new XmlRule();
 rule.Set("Count", "/lfm/events/@total");
 rule.Set("Events", "/lfm/events/event", Event); // !!!</pre>
<p>Zuerst wird eine Bild-Regel definiert: das Attribut &#8220;URL&#8221; liefert den Wert des Knotens, &#8220;Size&#8221; den Wert des &#8220;size&#8221;-Attributs. Die beiden blauen Zeilen zeigen die Verwendung von bestehenden Regeln als Unterobjekte: die Regel &#8220;Image&#8221; ist auf alle Knoten anwendbar, die über das Attribut &#8220;Images&#8221; gefunden werden, d.h. jeder Knoten aus &#8220;Images&#8221; erlaubt wieder Zugriff auf &#8220;URL&#8221; und &#8220;Size&#8221;. Gleiches für &#8220;Events&#8221;: alle Knoten, die über den XPath-Ausdruck &#8220;/lfm/events/event&#8221; gefunden werden, haben Attribute &#8220;Name&#8221;, &#8220;ID&#8221;, &#8220;Artists&#8221;, &#8230;</p>
<p>Wir müssen nur noch dem Decoder sagen, dass er diese Regel verwenden soll, wenn er eine Antwort auf eine &#8220;artist.getEvents&#8221;-Anfrage erhält. Dies geschieht durch gleichzeitiges Registrieren der Methode im Apitizer und Codec mittels &#8220;Apitizer.Method&#8221; (Wir befinden uns wieder in der statischen Funktion &#8220;AddMethods&#8221; von oben):</p>
<pre class="brush:c-sharp;">api.Method(artistGetEvents, rule);
</pre>
<p>Und nun sind wir fertig.</p>
<h2>Beispiel gefällig?</h2>
<p>Das folgende Beispiel funktioniert mit meiner aktuellen Implementierung von SharpConnect. Neu im Vergleich zum eben besprochenen ist nur die Klasse &#8220;LastFMValue&#8221;, die den Accessor für XML-Dokumente darstellt:</p>
<pre class="brush:c-sharp;"> LastFMApitizer lfm = new LastFMApitizer("b25b959554ed76058ac220b7b2e0a026", "", null);

 LastFMValue resp = lfm.Execute("artist.getEvents", new Parameters(
   "artist", "Kasabian"
 ));
 if (resp != null)
 {
   Console.Write(resp.ToString()); // oder z.B. Console.Write(resp.All("Events")[2].Value("Date"));
 }
 Console.ReadLine();
</pre>
<p>Die Ausgabe sieht so oder  ähnlich aus (auf all diese Attribute und Array-Elemente könnte man mittels &#8220;resp.Get(attribut)&#8221;  und &#8220;resp.All(attribut)&#8221; zugreifen, vgl. den Concept-Artikel):</p>
<pre>  Status : ok
  Count : 14
  Events[0] :
    Status : ok
    Name : Kasabian
    ID : 1408186
    URL : http://www.last.fm/event/1408186+Kasabian+at+Tivoli+Oudegracht+on+27+May+2010
    Artists : Kasabian
    Headliner : Kasabian
    Date : Thu, 27 May 2010 12:08:01
    Description : ...
    Images[0] :
      URL : http://userserve-ak.last.fm/serve/34/284053.jpg
      Size : small
    Images[1] :
      URL : http://userserve-ak.last.fm/serve/64/284053.jpg
      Size : medium
    Images[2] :
      URL : http://userserve-ak.last.fm/serve/126/284053.jpg
      Size : large
    Images[3] :
      URL : http://userserve-ak.last.fm/serve/252/284053.jpg
      Size : extralarge
    Attendance : 87
    ReviewCount : 0
    UniqueTag : lastfm:event=1408186
    Website : http://www.tivoli.nl/agenda/informatie/datum/do-27-mei-2010/titel/KASABIAN
  Events[1] :
    Status : ok
    Name : Pinkpop 2010
    ID : 932823
    URL : http://www.last.fm/event/932823+Pinkpop+2010
    Artists[0] : Green Day
    Artists[1] : Pixies
    Artists[2] : Kasabian
    Artists[3] : The Prodigy
    Artists[4] : Rammstein
    Artists[5] : John Mayer
    Artists[6] : Editors
    Artists[7] : Mika
    Artists[8] : Wolfmother
    Artists[9] : Kate Nash
    Artists[10] : Mando Diao
    Artists[11] : Paolo Nutini
    Artists[12] : P!nk
    Artists[13] : Florence + The Machine
    Artists[14] : Skunk Anansie
    Artists[15] : Gogol Bordello
    Artists[16] : Biffy Clyro
    Artists[17] : Yeasayer
    Artists[18] : The Temper Trap
    Artists[19] : Danko Jones
    Artists[20] : Gossip
    Artists[21] : 2 Many DJ's
    Artists[22] : Moke
    Artists[23] : Kitty, Daisy &amp; Lewis
    Artists[24] : Caro Emerald
    Artists[25] : Triggerfinger
    Artists[26] : Destine
    Artists[27] : DeWolff
    Artists[28] : Sungrazer
    Headliner : Green Day
    Date : Fri, 28 May 2010 21:24:01
    Description : ...
    Images[0] :
      URL : http://userserve-ak.last.fm/serve/34/42600061.jpg
      Size : small
    Images[1] :
      URL : http://userserve-ak.last.fm/serve/64/42600061.jpg
      Size : medium
    Images[2] :
      URL : http://userserve-ak.last.fm/serve/126/42600061.jpg
      Size : large
    Images[3] :
      URL : http://userserve-ak.last.fm/serve/252/42600061.jpg
      Size : extralarge
    Attendance : 560
    ReviewCount : 1
    UniqueTag : lastfm:event=932823
    Website : http://www.pinkpop.nl
  Events[2] :
    Status : ok
    Name : Kasabian
  ...
</pre>
<p>Ich arbeite im Moment an einer vollständigen Abbildung des Last.FM-APIs, auch wenn ich nicht weiß, ob ich das nicht lieber jedem selbst überlassen sollte. In den Worten von Morpheus:</p>
<p><strong>I can only show you the door, you have to  walk through it!</strong></p>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2010/03/proof-of-concept-sharpconnect/feed/</wfw:commentRss>
		<slash:comments>4</slash:comments>
		</item>
		<item>
		<title>Scripe. Oder: Warum eine Scriptsprache auch nur eine Suppe ist.</title>
		<link>http://dev.xscheme.de/2009/09/scripe-eigene-scriptsprache-ganz-einfach/</link>
		<comments>http://dev.xscheme.de/2009/09/scripe-eigene-scriptsprache-ganz-einfach/#comments</comments>
		<pubDate>Wed, 02 Sep 2009 20:40:33 +0000</pubDate>
		<dc:creator>WordPress</dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[Projekte]]></category>
		<category><![CDATA[Software]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=962</guid>
		<description><![CDATA[Es ist wohl aufgefallen, ich erwähne es trotzdem: Seit einiger Zeit arbeite ich an einem Prinzip, dass es jedem Interessierten ermöglichen soll, eine eigene Script- oder Programmiersprache zu entwerfen. Meine theoretischen Überlegungen, so langweilig sie auch manches Mal seien mögen, habe ich in einer Reihe von Artikeln festgehalten; damit ich diese Texte aber auch schreiben [...]]]></description>
			<content:encoded><![CDATA[<p>Es ist wohl aufgefallen, ich erwähne es trotzdem: Seit <a href="http://dev.xscheme.de/2009/07/eigene-programmiersprache-scriptsprach/">einiger Zeit</a> arbeite ich an einem Prinzip, dass es jedem Interessierten ermöglichen soll, eine eigene Script- oder Programmiersprache zu entwerfen. Meine theoretischen Überlegungen, so langweilig sie auch manches Mal seien mögen, habe ich in einer <a href="http://dev.xscheme.de/2009/08/wie-entwickle-ich-meine-eigene-scriptsprache-teil-1/">Reihe</a> <a href="http://dev.xscheme.de/2009/08/wie-entwickle-ich-meine-eigene-scriptsprache-teil-2/">von</a> <a href="http://dev.xscheme.de/2009/08/wie-entwickle-ich-meine-eigene-scriptsprache-teil-3/">Artikeln</a> festgehalten; damit ich diese Texte aber auch schreiben konnte, musste ich immer ein kleines Stückchen vorausdenken und einiges an implementierungstechnischer Vorarbeit leisten.</p>
<p>Und heute war ich dann soweit, dass ich eine gar nicht so üble Version vor mir hatte, die ich nun (nicht detailliert, eher angeberischerweise <img src='http://dev.xscheme.de/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' />  ) präsentieren will. Vorhang auf für <strong>Scripe</strong>!</p>
<p><span id="more-962"></span></p>
<p>(Der Name hat mehrere Hintergründe: zum einen wäre da der Schreibfehler, der schonmal beim Wort &#8220;Scripte&#8221; auftaucht, zum anderen findet sich ein Ursprung im englischen &#8220;to scribe&#8221;, was soviel wie &#8220;vorzeichnen&#8221; bedeutet. Zuguterletzt sagt einem das allwissende <a href="http://www.urbandictionary.com/define.php?term=scripe">Urban Dictionary</a>, das &#8220;scripe&#8221; das Äquivalent von &#8220;a load of rubbish&#8221;, also etwas nicht ganz hochwertigem ist. Das ist auch in Ordnung so: Scripe ist der Weg, den ich einschlagen würde, wenn ich eine Scriptsprache zu entwickeln hätte. Zwar sind Grundkonzepte, die sich auch im professionelleren Einsatz finden, vorhanden, aber Scripe erhebt in dieser Hinsicht keinerlei Anspruch auf Vollständigkeit oder übermäßige Effizienz. &#8220;Quick and Dirty&#8221; wäre dennoch übertrieben, dann doch eher &#8220;<strong>Quick and Not-So-Clean</strong>&#8220;&#8230;)</p>
<p>Die Suppe im Titel lässt das Prinzip erahnen: Suppen brauchen Gewürze, um zu schmecken, und hierbei ist es wichtig auf die Mischung zu achten. Ebenso wird jeder seine Suppe anders würzen, je nach Vorlieben oder Vorhaben. Eine Scriptsprache ist da nicht anders: hier eine Prise Variablenverwaltung, dort ein Löffel Arithmetik. Voilà.</p>
<p>Dieses Suppenprinzip findet man heutzutage überall, hauptsächlich in der Unix/Linux-Welt, wo man je nach Bedarf auf einfachste Art und Weise ein Paket nachladen kann und das Gesamtsystem somit bis ins letzte Eck modifizierbar bleibt. <strong>Auch Scripe verwendet solche Pakete, allerdings eben für die Features der Programmiersprache!</strong></p>
<p>Folgender C#-Code erzeugt einen Prozessor, der den <a href="http://en.wikipedia.org/wiki/Shunting_yard_algorithm">Shunting-Yard-Algorithmus</a> verwendet, um die Eingabe zu parsen. Anschließend erhält er vier Pakete: eines für arithmetische Operationen, eines für String-Operationen, eines für Ein- und Ausgabe, und eines für Variablendefinition und -verwaltung. Anschließend wird eine Eingabe-Auswertungs-Schleife gestartet.</p>
<pre>// Create processor
Processor p = new Processor(
    new ShuntingYardAlgorithm()
);

// Add packages
p.AddPackage(new Scripe.Evaluators.Arithmetic.ArithmeticPackage(true));
p.AddPackage(new Scripe.Evaluators.Strings.StringPackage("&amp;", "$"));
p.AddPackage(new Lapicon.Evaluators.InputOutput.InputOutputPackage());
p.AddPackage(new Lapicon.Evaluators.Variables.VariablePackage());

// Empty codebase
Codebase c = new Codebase(new Scripe.Env.Environment());

// Read-Eval-Print-Loop
string r = "";
while (true)
{
    try
    {
        Console.Write("Calc: ");
        if ((r = Console.ReadLine()) == "exit") break;
        c.Add(r);
        Console.WriteLine(" ==&gt; " + c.Execute(p).ReturnValue);
    }
    catch (Exception ex)
    {
        Console.WriteLine(" ERROR: " + ex.Message);
    }
}</pre>
<p>In nur 5 Zeilen haben wir hier (mithilfe der Scripe-internen und zweier für Lapicon geschriebenen Pakete) einen Prozessor geschaffen, der jede der folgenden Eingaben auswerten kann:</p>
<pre>1+(4-6)*(3-4^5)+e
def variable = 7*8
def variableDurchZwei = &lt;variable&gt;/2
echo "(7*8)/2 = " &amp; $(&lt;variableDurchZwei&gt;)</pre>
<p>Und nun stelle man sich eine Internetseite vor, die eine gewisse Anzahl solcher Pakete anbietet. <strong>Scriptsprachen entwickeln einfach gemacht</strong>.</p>
<p>Soviel also von mir. Wann Scripe letztlich ganz fertig ist (und ich mich wieder den anderen Projekten wie LastSharp widmen kann), kann ich nur schätzen. Ist ein Monat in Ordnung? <img src='http://dev.xscheme.de/wp-includes/images/smilies/icon_wink.gif' alt=';)' class='wp-smiley' /> </p>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2009/09/scripe-eigene-scriptsprache-ganz-einfach/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>Inside Last.FM: Silent Authentication für API 2.0</title>
		<link>http://dev.xscheme.de/2009/07/inside-lastfm-silent-authentication/</link>
		<comments>http://dev.xscheme.de/2009/07/inside-lastfm-silent-authentication/#comments</comments>
		<pubDate>Wed, 08 Jul 2009 21:47:06 +0000</pubDate>
		<dc:creator></dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[General]]></category>
		<category><![CDATA[Lapicon]]></category>
		<category><![CDATA[Web]]></category>
		<category><![CDATA[lastfm]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=774</guid>
		<description><![CDATA[Update (17.07.2009): Das hier ist (abgesehen von den &#8220;Skills&#8221;, die ich dabei gebraucht hab) wohl ziemlich überflüssig, wenn man die API-Methode auth.getMobileSession mal genauer betrachtet. Wieder was gelernt: RTFM!
Da im Moment anscheinend wieder Änderungen am Last.FM-Webservice gemacht werden, die dazu geführt haben, dass LastSharp höchstens noch mithilfe der &#8220;Modifizierten Verbindung&#8221; (unter &#8220;Erweiterte Einstellungen&#8221; &#62;&#62; &#8220;Verschiedenes&#8221;) [...]]]></description>
			<content:encoded><![CDATA[<p><strong>Update (17.07.2009):</strong> Das hier ist (abgesehen von den &#8220;Skills&#8221;, die ich dabei gebraucht hab) wohl ziemlich überflüssig, wenn man die API-Methode auth.getMobileSession mal genauer betrachtet. Wieder was gelernt: RTFM!</p>
<p>Da im Moment anscheinend wieder Änderungen am Last.FM-Webservice gemacht werden, die dazu geführt haben, dass LastSharp höchstens noch mithilfe der &#8220;Modifizierten Verbindung&#8221; (unter &#8220;Erweiterte Einstellungen&#8221; &gt;&gt; &#8220;Verschiedenes&#8221;) funktioniert, habe ich mir mal das <a href="http://www.lastfm.de/api/radio">Radio-API</a> angesehen, das ja anscheinend die Zukunft des Radiostreamings bei Last.FM ist. Was mich daran allerdings stört, ist die umständliche Anmeldeprozedur: Token holen, Browser öffnen, auf Authentifizierung warten, Session holen.</p>
<p>Der Umweg über den Browser ist einfach unschön, weshalb ich nach einer Lösung gesucht habe, die <strong>den Login sowie die anschließende Bestätigung eines Users im Hintergrund simuliert</strong>, sodass er das eigentliche Programm nicht verlassen muss.</p>
<p><span id="more-774"></span></p>
<p><em>Funktionen, die Authentication-Token und -Session holen, werden im folgenden als gegeben betrachetet, z.B. durch LastFmLib.Net. </em></p>
<p>Wir brauchen nun also zwei Funktionen, die das Abschicken des Login-Formulars und das Klicken auf den &#8220;Zulassen&#8221;-Button vortäuschen. Beide Operationen produzieren eine HTTP-POST-Anfrage, funktionieren aber nur, wenn auch die richtigen Cookies vorhanden sind. (Das hat mich einige Zeit gekostet und dabei war die Lösung so einfach&#8230;) Es bietet sich an, die POST-Anfrage in eine Hilfsfunktion <em>ExecutePOST</em> zu packen, hier z.B. in C#:<br />
(Namespace <em>System.Net</em> muss eingebunden sein)</p>
<pre name="code" class="c-sharp">// Löst das Cookie Problem: Immer denselben Container verwenden!
private static CookieContainer GlobalCookies = new CookieContainer();

// Sendet eine POST-Anfrage
public static void ExecutePOST(string uri, string data) {
    try
    {
        // Bereite die Anfrage vor
        HttpWebRequest request = (HttpWebRequest)WebRequest.Create(uri);
        request.Method = "POST";
        request.CookieContainer = GlobalCookies; // !!
        request.ContentType = "application/x-www-form-urlencoded";
        request.ContentLength = data.Length;

        // Schreiben der Daten
        ASCIIEncoding enc = new ASCIIEncoding();
        byte[] bdata = enc.GetBytes(bodyData);
        Stream str = req.GetRequestStream();
        str.Write(bdata, 0, bdata.Length);
        str.Close();

        // Schicke Anfrage ab und verwirf die Antwort
        HttpWebResponse response = (HttpWebResponse)request.GetResponse();
        response.Close();
    }
    catch { }
}</pre>
<p>Will man überprüfen, ob die gewünschte Operation erfolgreich war, würde es sich vermutlich anbieten, die Antwort auszuwerten, aber darauf verzichte ich jetzt aus Zeitgründen.</p>
<p>Nächster Schritt ist die Untersuchung der Formulare auf der Last.FM-Seite, damit man weiß, welche Daten an welche Adresse gesendet werden müssen:</p>
<ul>
<li>Login: <strong>https://www.last.fm/login/</strong>
<ul>
<li>refererKey (leer)</li>
<li>backto (&#8220;/&#8221;)</li>
<li>username (Benutzername)</li>
<li>password (Passwort)</li>
</ul>
</li>
<li>Zulassen: <strong>http://www.last.fm/api/grantAccess</strong>
<ul>
<li>api_key (Last.FM schickt den sogar zweimal, wir deshalb sicherheitshalber auch&#8230;)</li>
<li>token (Authentication-Token)</li>
<li>referer (&#8220;/&#8221;)</li>
</ul>
</li>
</ul>
<p>Also, die beiden verbleibenden Funktionen in C#:</p>
<pre name="code" class="c-sharp">    // Lasse ein Programm zu
    public static void GrantAccess(string token)
    {
        ExecutePOST(
            "http://www.last.fm/api/grantAccess",
            "api_key=" + apiKey + "&amp;api_key=" + apiKey + "&amp;token=" + token + "&amp;referer=/"
        );
    }

    // Melde den User bei Last.FM an
    public static void Login(string username, string password)
    {
        ExecutePOST(
            "https://www.last.fm/login/",
            "refererKey=&amp;backto=/&amp;username=" + Uri.EscapeDataString(username) +
            "&amp;password=" + Uri.EscapeDataString(password)
        );
    }</pre>
<p>Um jetzt eine gesamte Anmeldung zu simulieren, müsste man nun folgendes machen:</p>
<pre name="code" class="c-sharp">public static void Authenticate(string username, string password)
{
    string token = GetToken(); // irgendeine externe Funktion
    Login(username, password);
    GrantAccess(token);
    string session = GetSession(); // irgendeine externe Funktion
    // Mache etwas...
}</pre>
<p>Das war&#8217;s auch schon. Wie gesagt fehlt hier ein Test, ob die Anmeldung erfolgreich war, aber irgendwie lässt sich das bestimmt einfach überprüfen. (Vllt. liefert Last.FM ja im Hintergrund einen HTTP-Error, der im Browser nicht auffällt?)</p>
<p>Jedenfalls, wenn jemand die <strong>Authentifizierung für das Last.FM-API 2.0 im Hintergrund</strong> ablaufen lassen will, ist das hier der Weg. Ich hoffe, es hilft.</p>
<p>Die Verwendung des Radio-APIs in LastSharp werde ich übrigens so lang wie möglich hinauszögern, da hier wieder einmal diverse Standards (HTTP, XML, &#8230;) unsachgemäß ausgelegt werden&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2009/07/inside-lastfm-silent-authentication/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>Proxy-Server verwenden (C#)</title>
		<link>http://dev.xscheme.de/2008/10/proxy-server-verwenden-c/</link>
		<comments>http://dev.xscheme.de/2008/10/proxy-server-verwenden-c/#comments</comments>
		<pubDate>Mon, 06 Oct 2008 22:14:59 +0000</pubDate>
		<dc:creator>xsc</dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[HowTo]]></category>
		<category><![CDATA[download]]></category>
		<category><![CDATA[proxy verwenden]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=384</guid>
		<description><![CDATA[Einen Proxy kann man sich als Zwischenstation vorstellen: man schickt ihm eine Anfrage (z.B.: &#8220;Hole die Website XY&#8221;), er speichert das entsprechende Ergebnis (z.B. die Seite XY) und überträgt es an den Anfragenden (Client).
Ein ganz klarer Vorteil hiervon ist der Schutz des Clients: ein Proxy-Server bietet aufgrund seines einfacheren Aufbaus viel weniger Angriffsfläche als der [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://dev.xscheme.de/wp-content/uploads/2008/10/proxy.png"><img class="alignleft size-medium wp-image-407" style="margin-right: 1em; margin-bottom: 0.5em; border-style: none;" title="proxy" src="http://dev.xscheme.de/wp-content/uploads/2008/10/proxy-300x118.png" alt="" width="300" height="118" /></a>Einen Proxy kann man sich als Zwischenstation vorstellen: man schickt ihm eine Anfrage (z.B.: &#8220;Hole die Website XY&#8221;), er speichert das entsprechende Ergebnis (z.B. die Seite XY) und überträgt es an den Anfragenden (Client).</p>
<p>Ein ganz klarer Vorteil hiervon ist der Schutz des Clients: ein Proxy-Server bietet aufgrund seines einfacheren Aufbaus viel weniger Angriffsfläche als der eigentlich anfragende Rechner &#8211; man denke nur an die regelmäßig auftretenden Sicherheitslücken in populären Betriebssystemen &#8211; und garantiert außerdem ein gewisses Maß an Anonymität, da nur die IP des Proxies nach außen hin sichtbar ist.</p>
<p>Nachteile wiederum sind, dass man sich nicht sicher sein kann, was der Server protokolliert und speichert, und ob die Daten, die bei einem selbst ankommen, nicht auf dem Weg (vom Proxy) modifiziert wurden. Des weiteren werden viele Proxies von einer großen Anzahl Anwendern genutzt, was einen Geschwindigkeitsverlust im Vergleich zu &#8220;normalen&#8221; Verbindungen bedeuten kann. Ein ebenso großes Problem kann es sein, wenn Funktionen fehlen (z.B. die Möglichkeit, Daten zu übertragen oder Dateien herunterzuladen) oder wenn der Server eine Inkompatibilität zum verwendeten Programm aufweist. (z.B. Server des <a href="http://dev.xscheme.de/2008/10/proxy-host-header-und-codeen-network/">CoDeeN-Networks</a> zu .NET-Programmen)</p>
<p>Trotzdem, genug des Geplänkels, die Verwendung eines Proxies im eigenen Programm ist nicht sonderlich schwer.</p>
<p><span id="more-384"></span></p>
<p><strong>Das WebProxy-Objekt</strong></p>
<p>Erster Schritt hierbei ist die Erstellung eines Objektes der Klasse System.Net.WebProxy. Hierzu brauchen wir die Adresse des Servers (ggf. seine IP) und den Port, über den er erreicht werden kann:</p>
<pre>System.Net.WebProxy proxy =
    new System.Net.WebProxy("http://&lt;Adresse&gt;:&lt;Port&gt;", true);</pre>
<p>Der zweite Parameter des Konstruktors gibt an, ob der Proxy für lokale Verbindungen (z.B. zu Testzwecken auf &#8220;http://localhost/&#8221;) umgangen werden soll.</p>
<p><strong>Authentifizierung</strong></p>
<p>Für die Authentifizierung, also für alle nicht-anonymen Proxies, muss ein System.Net.NetworkCredential-Objekt mit den entsprechenden Zugangsdaten erstellt und dem Proxy-Objekt zugewiesen werden:</p>
<pre>System.Net.NetworkCredential auth =
    new System.Net.NetworkCredential("&lt;Benutzername&gt;", "&lt;Passwort&gt;");
proxy.Credentials = auth;</pre>
<p>Mithilfe eines so konfigurierten Proxy-Objektes kann man nun entweder mithilfe der HttpWebRequest-Klasse HTTP-Anfragen senden, oder mithilfe der WebClient-Klasse Dateien herunterladen &#8211; beides über die von uns gewählte Zwischenstation.</p>
<p><strong>HTTP-Requests über einen Proxy</strong></p>
<pre>System.Net.HttpWebRequest request =
    (System.Net.HttpWebRequest) System.Net.WebRequest.Create("&lt;URI&gt;");
request.Proxy = proxy;<span style="color: #008000;">
</span>System.Net.HttpWebResponse response =
    (System.Net.HttpWebResponse) request.GetResponse();
...</pre>
<p><strong>Datei-Download mit Proxy</strong></p>
<pre>System.Net.WebClient client = new System.Net.WebClient();
client.Proxy = proxy;
client.DownloadFile("&lt;URI&gt;", "&lt;lokaler Pfad&gt;");
...</pre>
<p><strong>Aus.</strong></p>
<p>Ich hoffe, ich habe mit diesem kurzen Beitrag all jenen geholfen, die ihre Programme um Proxy-Funktionalität erweitern wollen.</p>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2008/10/proxy-server-verwenden-c/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
		<item>
		<title>LastFmLib.Net</title>
		<link>http://dev.xscheme.de/2008/09/howto-lastfmlibnet/</link>
		<comments>http://dev.xscheme.de/2008/09/howto-lastfmlibnet/#comments</comments>
		<pubDate>Fri, 05 Sep 2008 15:38:29 +0000</pubDate>
		<dc:creator>xsc</dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[Fundstücke]]></category>
		<category><![CDATA[HowTo]]></category>
		<category><![CDATA[.net]]></category>
		<category><![CDATA[lastfm]]></category>
		<category><![CDATA[library]]></category>
		<category><![CDATA[tutorial]]></category>
		<category><![CDATA[webservice]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=277</guid>
		<description><![CDATA[UPDATE 12.12.2008: Veränderungen in den Namespaces von LastFmLib.Net!
Ich habe mich in letzter Zeit viel mit Last.FM beschäftigt, nicht zuletzt wegen meinen beiden Programmen LastSharp und Lea. Aufgrund eines Problems mit letzterem (Tracks/Künstler mit Umlauten machen Schwierigkeiten) habe ich mich ein wenig umgesehen und bin auf eine (angeblich vollständige) Implementierung des Last.FM-APIs in .NET gestoßen: LastFmLib.Net.

Und [...]]]></description>
			<content:encoded><![CDATA[<p><strong>UPDATE 12.12.2008: Veränderungen in den Namespaces von LastFmLib.Net</strong>!</p>
<p>Ich habe mich in letzter Zeit viel mit <a href="http://www.last.fm">Last.FM</a> beschäftigt, nicht zuletzt wegen meinen beiden Programmen <strong>LastSharp</strong> und <strong>Lea</strong>. Aufgrund eines Problems mit letzterem (Tracks/Künstler mit Umlauten machen Schwierigkeiten) habe ich mich ein wenig umgesehen und bin auf eine (angeblich vollständige) Implementierung des Last.FM-APIs in .NET gestoßen: <strong><a href="http://lastfmlibnet.sourceforge.net/">LastFmLib.Net</a></strong>.</p>
<p><span id="more-277"></span></p>
<p>Und weil diese Bibliothek wirklich einzigartig einfach zu verwenden ist (obwohl auch hier die Umlaute noch nicht extra betrachtet werden), möchte ich meine Erfahrungen gerne teilen und eine winzige, deutschsprachige Erklärung zu ihrer Verwendung bzw. Anwendung auf das <a href="http://www.lastfm.de/api/intro">API 2.0</a> schreiben.</p>
<p><strong>Vorbereitung</strong></p>
<p>Jeder, der den Last.FM-Webservice verwenden will, benötigt zuallererst einen öffentlichen und einen dazugehörigen geheimen API-Schlüssel, den man <a href="http://www.lastfm.de/api/account">hier</a> beantragen kann. Da LastFmLib.Net eine Klassenbibliothek für das .NET-Framework ist, wird eine entsprechende Entwicklungsumgebung (z.B. <a href="www.microsoft.com/germany/Express/">Visual C# Express Edition</a>) benötigt, um damit arbeiten zu können.</p>
<p>Hat man einen öffentlichen und einen geheimen API-Schlüssel (Key und Secret) erhalten, so muss man LastFmLib.Net diese beiden mitteilen. Dies geschieht, indem man das <em>LastFmLib.API20.Settings.AuthData</em>-Objekt neu setzt, in C# beispielsweise:</p>
<pre>LastFmLib.General.MD5Hash key
    = new LastFmLib.General.MD5Hash(<strong>"APIKey</strong>", true, Encoding.UTF8);
LastFmLib.General.MD5Hash secret
    = new LastFmLib.General.MD5Hash(<strong>"APISecret"</strong>, true, Encoding.UTF8);
LastFmLib.API20.Settings20.AuthData
    = new LastFmLib.API20.Types.AuthData(key, secret, null);</pre>
<p>Hat man das nun erledigt, kann man (nach der Authentifizierung) auf die einzelnen API-Methoden zugreifen.</p>
<p><strong>Allgemein: Request schicken</strong></p>
<p>In LastFmLib.Net sind alle Anfragen an das API in Klassen gekapselt. Für jede Art von Anfrage (Request) erstellt man also ein entsprechendes Objekt, startet die Verarbeitung, überprüft ob die Anfrage erfolgreich war und verarbeitet das Ergebnis. Alle Requests befinden sich im Namespace <em>LastFmLib.API20.&lt;Art&gt;</em>, wobei &lt;Art&gt; die entsprechende Obergruppe einer Anfrage ist, z.B. &#8220;Auth&#8221;, &#8220;User&#8221;, &#8220;Playlists&#8221;, etc&#8230; Diese Obergruppen sieht man auf der <a href="http://www.lastfm.de/api/intro">API-Seite</a> fettgedruckt in der Spalte links.</p>
<p>In C# würde das also so aussehen:</p>
<pre>LastFmLib.API20.&lt;Art&gt;.&lt;Irgendeine Request&gt; req
    = new LastFmLib.API20.&lt;Art&gt;.&lt;Irgendeine Request&gt;(&lt;notwendige Parameter&gt;);
req.Start();
if (req.Succeeded) {
    // Mach etwas mit dem Ergebnis
} else {
    // Mach etwas mit dem Fehler (req.ErrorMessage)
}</pre>
<p><strong>Authentifizierung</strong></p>
<p>Alles beginnt mit der Anmeldung des Users. Bei Last.FM geht dies in drei Schritten vonstatten:</p>
<ol>
<li>Holen des sog. &#8220;Tokens&#8221;, der sozusagen das Passwort für alle nun kommenden Anfragen darstellt und nur eine begrenzte Zeit gültig ist. Dies geschieht mit der API-Methode <em>getToken()</em>:
<pre>LastFmLib.API20.Auth.AuthGetToken req
    = new LastFmLib.API20.Auth.AuthGetToken();
req.Start();
if (req.Succeeded) {
    // Token speichern
    LastFmLib.API20.Settings20.AuthData.Token = req.Result;
} else {
    // Mach etwas mit dem Fehler
}</pre>
</li>
<li>Zulassung des Programms für den eigenen Last.FM-Account. Hierfür wird der Browser geöffnet, sodass der User sich einloggen und die entsprechende Berechtigung vergeben kann. LastFmLib.Net stellt hierfür auch gleich eine Funktion bereit (1.Parameter: Anwendung unterbrechen, 2. Parameter: Timeout):
<pre>LastFmLib.API20.Settings.AuthData.AskUserToGrantPermissions(false, 0);</pre>
</li>
<li>Holen der Session-Informationen (Benutzername, Session-Key, Subscriber-Status) mit der Methode <em>getSession(token)</em>. Achten Sie darauf, dass dieser Schritt auch wirklich erst nach der Bestätigung im Browser ausgeführt wird, ansonsten klappt das Holen der Informationen nämlich nicht.
<pre>LastFmLib.API20.Auth.AuthGetSession sk =
    new LastFmLib.API20.Auth.AuthGetSession(LastFmLib.API20.Settings20.AuthData.Token, false);
sk.Start();
if (sk.Succeeded)
{
    // Session speichern
    LastFmLib.API20.Settings20.AuthData.Session = sk.Result;
}
else {
    // Mach etwas mit dem Fehler
}</pre>
</li>
</ol>
<p>Wenn diese drei Schritte erfolgreich ausgeführt wurden, kann man die eigentlichen Anfragen starten, immer nach dem oben genannten Muster.</p>
<p><strong>Beispiel: Playlisten eines Users holen</strong></p>
<pre>LastFmLib.API20.User.UserGetPlaylists req =
    new LastFmLib.API20.User.UserGetPlaylists(LastFmLib.API20.Settings20.AuthData.Session.Username);
gp.Start();
if (gp.Succeeded)
{
    foreach (LastFmLib.API20.Types.SimplePlaylist pl in gp.Result) {
        // Playliste verarbeiten
    }
} else {
    // Mach etwas mit dem Fehler
}</pre>
<p><strong>Anmerkungen</strong></p>
<p>Der Aktualität halber sollte man nicht die vorgefertigte DLL von der Website des Entwicklers laden, sondern die neuesten Quelldateien aus dem SVN-Verzeichnis, um daraus die letztendliche Bibliothek zu erstellen. Dies geht mithilfe von <a href="http://subversion.tigris.org/">Subversion</a> und dem Befehl:</p>
<p><code>svn co https://lastfmlibnet.svn.sourceforge.net/svnroot/lastfmlibnet lastfmlibnet</code></p>
<p>Eine entsprechende Dokumentation der Klassenbibliothek findet sich auch im SVN-Verzeichnis. Vor allem, wenn man keine Möglichkeit hat, über die Entwicklungsumgebung Informationen zu einzelnen Funktionen zu erhalten (Parametertypen/-anzahl, Rückgabetyp), sollte man sich darin vertiefen.</p>
<p>Mein Programm <em>Lea </em>(ab Version 1.2) ist dank LastFmLib.Net keine große Sache mehr, dafür (hoffentlich) um einiges stabiler und wartungsfreundlicher.</p>
<p>So, ich hoffe, dieses HowTo hat ein wenig geholfen. Weitere Informationen und natürlich auch Support gibt es auf der <a href="http://lastfmlibnet.sourceforge.net/">Website des Entwicklers</a>.</p>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2008/09/howto-lastfmlibnet/feed/</wfw:commentRss>
		<slash:comments>6</slash:comments>
		</item>
		<item>
		<title>Audiostreaming mit irrKlang (C#)</title>
		<link>http://dev.xscheme.de/2008/08/audiostreaming-mit-irrklang/</link>
		<comments>http://dev.xscheme.de/2008/08/audiostreaming-mit-irrklang/#comments</comments>
		<pubDate>Thu, 14 Aug 2008 05:59:27 +0000</pubDate>
		<dc:creator>xsc</dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[audio]]></category>
		<category><![CDATA[code]]></category>
		<category><![CDATA[irrklang]]></category>
		<category><![CDATA[part of lastsharp]]></category>
		<category><![CDATA[streaming]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=147</guid>
		<description><![CDATA[
Auf der Suche nach einem Ersatz für die DirectX-Wiedergabe in LastSharp bin ich gestern über irrKlang gestolpert. Soweit so gut: plattformunabhängig, einfach zu bedienen, eigentlich alles in Ordnung. Aber dann gab es da doch noch ein größeres Manko, das ich nicht ignorieren konnte: das Streaming von Dateien, die im Internet liegen, ist nicht möglich &#8211; [...]]]></description>
			<content:encoded><![CDATA[<p><a href="http://www.ambiera.com/irrklang/"><img src="http://dev.xscheme.de/wp-content/uploads/2008/08/irrklang_link_big.png" style="margin-bottom: 1em" /></a><br/><br />
Auf der Suche nach einem Ersatz für die DirectX-Wiedergabe in LastSharp bin ich gestern über <a href="http://www.ambiera.com/irrklang/">irrKlang</a> gestolpert. Soweit so gut: plattformunabhängig, einfach zu bedienen, eigentlich alles in Ordnung. Aber dann gab es da doch noch ein größeres Manko, das ich nicht ignorieren konnte: das Streaming von Dateien, die im Internet liegen, ist nicht möglich &#8211; und genau das bräuchte ich aber für LastSharp. Was also machen?</p>
<p><span id="more-147"></span></p>
<p>Über <a href="http://www.ambiera.com/irrklang/docu/index.html#fileOverriding">dieses Tutorial</a> bin ich der Sache näher gekommen: Man muss eine eigene Implementierung des Interfaces <code>IFileFactory</code> erstellen, und &#8211; falls nötig &#8211; eine eigene <code>Stream</code>-Klasse. Und letzteres war durchaus nötig, da mir keine bereitgestellte <code>Stream</code>-Klasse die entsprechenden Möglichkeit der Pufferung eines Audiostreams geboten hat. So ist die Klasse <code>InternetStream</code> geboren worden, die eine Verbindung zu einer Datei im Internet herstellt (mittels HttpWebRequest), und die empfangenen Daten in einer Datei auf der Festplatte speichern kann, auf die ebenso die Möglichkeit gleichzeitigen Zugriffs besteht:</p>
<pre class="c#" name="code">
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Net;
using System.Threading;
using System.Windows.Forms;
using System.Collections;
using IrrKlang;

public class InternetStream : Stream
{
    // =======================================================
    // Buffer-Daten
    private Stream resp = null;
    private string BufferFile = "streaming.mp3";
    private FileStream OutputStream = null;
    private FileStream BufferStream = null;
    private Thread BufferThread = null;
    // =======================================================
    // Konstruktor
    public InternetStream(string uri, string bfile)
    {
        // Dateiname des lokalen Files
        this.BufferFile = bfile;
        // FileStream zum Schreiben öffnen
        if (File.Exists(this.BufferFile)) { try { File.Delete(this.BufferFile); } catch { } }
        this.BufferStream = new FileStream(this.BufferFile, FileMode.OpenOrCreate, FileAccess.Write, FileShare.Read);
        // ResponseStream zum File im Internet
        this.resp = ((HttpWebRequest)WebRequest.Create(uri)).GetResponse().GetResponseStream();
        // Pufferung starten
        this.BufferThread = new Thread(new ThreadStart(this.FillBuffer));
        this.BufferThread.Start();
    }
    // =======================================================
    // Read: wenn zehn mal 0 zurückgegeben wird, werden alle
    // Streams geschlossen und der Thread beendet
    private int ReadTries = 0;
    public override int Read(byte[] array, int offset, int count)
    {
        // Ausgabe-Stream öffnen
        if (this.OutputStream == null) this.OutputStream = new FileStream(this.BufferFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
        try
        {
            // Dem Puffer Zeit geben, Daten zu holen
            while (this.OutputStream.Length == 0) Thread.Sleep(100);
            // Daten lesen
            int s = this.OutputStream.Read(array, offset, count);
            // Wenn 0: ein erfolgloser Versuch mehr
            if (s == 0) this.ReadTries++;
            if (this.ReadTries &gt; 10)
            {
                this.Close();
            }
            //
            return s;
        }
        catch { return 0; }
    }
    // =======================================================
    // Seek
    public override long Seek(long offset, SeekOrigin loc)
    {
        return this.OutputStream.Seek(offset, loc);
    }
    // =======================================================
    // Puffer-Thread
    private void FillBuffer()
    {
        byte[] buffer = new byte[20 * 1024];
        int rd = 0;
        while ((rd = this.resp.Read(buffer, 0, buffer.Length)) &gt; 0)
        {
            this.BufferStream.Write(buffer, 0, rd);
        }
    }
    // =======================================================
    // Schließen
    public override void Close()
    {
        this.OutputStream.Close();
        this.BufferStream.Close();
        this.BufferThread.Abort();
        if (File.Exists(this.BufferFile))
            try { File.Delete(this.BufferFile); }
            catch { }
        base.Close();
    }
    // =======================================================
    // Length
    public override long Length
    {
        get
        {
            if (this.OutputStream == null) return 0;
            else return this.OutputStream.Length;
        }
    }
    // =======================================================
    // Properties: CanRead, CanSeek, CanWrite
    public override bool CanRead
    {
        get { return true; }
    }

    public override bool CanSeek
    {
        get { return true; }
    }

    public override bool CanWrite
    {
        get { return false; }
    }
    // =======================================================
    // restliche Methoden, damit die Stream-Implementierung
    // komplett ist
    public override void Flush()
    {
        this.OutputStream.Flush();
    }

    public override long Position
    {
        get
        {
            return this.OutputStream.Position;
        }
        set
        {
            this.OutputStream.Position = value;
        }
    }

    public override void SetLength(long value)
    {
        this.OutputStream.SetLength(value);
    }

    public override void Write(byte[] buffer, int offset, int count)
    {
        return;
    }
}</pre>
<p>Die entsprechende <code>IFileFactory</code>-Implementation:</p>
<pre class="c#" name="code">
public class StreamFileFactory : IFileFactory
{
    // =====================================
    // Öffne ein File (von Irrklang verwendete Methode)
    public Stream openFile(string filename)
    {
        if (filename.StartsWith("http://"))
        {
            // neuen Starten
            string fn = Application.StartupPath + "/streaming.mp3";
            try
            {
                InternetStream istream = new InternetStream(filename, fn);
                return istream;
            }
            catch { return null; }
        }
        else
        {
            // lokale Datei
            return new FileStream(filename, FileMode.Open, FileAccess.Read);
        }
    }
}
</pre>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2008/08/audiostreaming-mit-irrklang/feed/</wfw:commentRss>
		<slash:comments>1</slash:comments>
		</item>
		<item>
		<title>mp3Gain-Wrapper (C#)</title>
		<link>http://dev.xscheme.de/2008/08/mp3gain-codesnippet-c-batch/</link>
		<comments>http://dev.xscheme.de/2008/08/mp3gain-codesnippet-c-batch/#comments</comments>
		<pubDate>Tue, 12 Aug 2008 15:48:32 +0000</pubDate>
		<dc:creator>xsc</dc:creator>
				<category><![CDATA[Batch]]></category>
		<category><![CDATA[C#]]></category>
		<category><![CDATA[code]]></category>
		<category><![CDATA[part of lastsharp]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=101</guid>
		<description><![CDATA[mp3Gain ist ein Programm zur (reversiblen!) Normalisierung der Lautstärke in MP3-Dateien. Die folgende Klasse kann mithilfe der Kommandozeilenversion von mp3Gain einen MP3-Titel normalisieren.

using System;
/*
 * [class] mp3Gain: Klasse zur Normalisierung von MP3-Dateien mithilfe von mp3Gain
 *
 * Copyright (C) 2008 Yannick Scherer
 * This program is free software; you can redistribute it and/or modify it [...]]]></description>
			<content:encoded><![CDATA[<p><img src="http://dev.xscheme.de/wp-content/uploads/2008/08/mp3gainlogosmall.gif" align="left" style="margin-right: 1em;" /><a href="http://mp3gain.sourceforge.net">mp3Gain</a> ist ein Programm zur (reversiblen!) Normalisierung der Lautstärke in MP3-Dateien. Die folgende Klasse kann mithilfe der <a href="http://prdownloads.sourceforge.net/mp3gain/mp3gain-dos-1_4_6.zip?download">Kommandozeilenversion von mp3Gain</a> einen MP3-Titel normalisieren.<br />
<span id="more-101"></span></p>
<pre class="c#" name="code">using System;
/*
 * [class] mp3Gain: Klasse zur Normalisierung von MP3-Dateien mithilfe von mp3Gain
 *
 * Copyright (C) 2008 Yannick Scherer
 * This program is free software; you can redistribute it and/or modify it under the terms of the GNU General
 * Public License as published by the Free Software Foundation; either version 3 of the License, or (at your
 * option) any later version.
 *
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the
 * implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
 * for more details.
 *
 * You should have received a copy of the GNU General Public License along with this program;
 * if not, see http://www.gnu.org/licenses/.
 * */

using System.Diagnostics;
using System.Collections.Generic;
using System.Text;
using System.IO;

class mp3Gain
    {
        // mp3Gain-Daten
        private static Process          mp3GainProcess = null;
        private static ProcessStartInfo mp3GainProcessInfo = new ProcessStartInfo(mp3GainPath);
        private static string           mp3GainCurrentFile = "";
        public static bool              mp3GainIsRunning = false;
        public static string            mp3GainPath    { get { return System.Windows.Forms.Application.StartupPath+"/mp3gain/mp3gain.exe"; } }
        public static string            mp3GainParams { get { return "/r /d 20 /k /c /q"; } }
        // Lautstärke normalisieren
        public static void NormalizeVolume(string mp3File) {
            if (!mp3GainIsRunning &amp;&amp; File.Exists(mp3GainPath) &amp;&amp; File.Exists(mp3File)) {
                // Prozess läuft
                mp3GainIsRunning   = true;
                mp3GainCurrentFile = mp3File;
                // Datei, an der gearbeitet wird
                string mp3Work = mp3File+"_work.mp3";
                File.Copy(mp3File, mp3Work);
                // mp3GainProcessInfo initialisieren
                mp3GainProcessInfo.Arguments = mp3GainParams+" \""+mp3Work+"\"";
                mp3GainProcessInfo.WindowStyle = ProcessWindowStyle.Hidden;
                // mp3Gain aufrufen
                mp3GainProcess = new Process();
                mp3GainProcess.StartInfo = mp3GainProcessInfo;
                mp3GainProcess.EnableRaisingEvents = true;
                mp3GainProcess.Exited += new EventHandler(mp3GainExited);
                mp3GainProcess.Start();
            }
        }
        public static void mp3GainExited(object sender, EventArgs e)
        {
            // Daten rückkopieren
            string mp3Work = mp3GainCurrentFile + "_work.mp3";
            if (new FileInfo(mp3Work).Length &gt; 0)
            {
                File.Copy(mp3Work, mp3GainCurrentFile, true);
                File.Delete(mp3Work);
            }
            // Reset
            mp3GainIsRunning = false;
            mp3GainCurrentFile = "";
            return;
        }

    }</pre>
<p>Eine MP3-Datei wird normalisiert, durch den Aufruf <code>mp3Gain.NormalizeVolume(&lt;MP3-Datei&gt;)</code>.</p>
<p>Innerhalb der Klasse müssen noch der Pfad zu mp3gain.exe (<em>mp3GainPath</em>) angepasst werden, sowie die gewünschten Parameter (<em>mp3GainParams</em>). Eine Übersicht über die Parameter von mp3Gain gibt es <a href="http://wiki.ubuntuusers.de/MP3Gain?rev=66766">hier</a>.</p>
<p><strong>Batch</strong></p>
<p>Ein BatchFile, das mp3Gain verwendet, könnte folgendermaßen aussehen (vorrausgesetzt, es befindet sich im selben Verzeichnis wie mp3gain.exe):</p>
<pre>@rem // Ziehen Sie eine MP3-Datei auf dieses Batchfile, um sie zu normalisieren.

@rem // In aktuelles Verzeichnis wechseln
@cd /D %~d0%~p0

@echo --------------------------------------------------------------------
@echo Normalisieren von %1 ...
@echo --------------------------------------------------------------------
@ "%cd%/mp3gain.exe" &lt;Parameter&gt; %1
@echo --------------------------------------------------------------------
@echo Prozess abgeschlossen...
@echo --------------------------------------------------------------------
@pause</pre>
<p>Dieser Code lässt sich natürlich erweitern, sodass ganze Ordner bearbeitet werden. Das überlasse ich aber jedem, der fleißig genug dafür ist&#8230;</p>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2008/08/mp3gain-codesnippet-c-batch/feed/</wfw:commentRss>
		<slash:comments>3</slash:comments>
		</item>
		<item>
		<title>LastFM.XMLProcessor</title>
		<link>http://dev.xscheme.de/2008/08/lastxml/</link>
		<comments>http://dev.xscheme.de/2008/08/lastxml/#comments</comments>
		<pubDate>Sat, 09 Aug 2008 20:00:52 +0000</pubDate>
		<dc:creator>xsc</dc:creator>
				<category><![CDATA[C#]]></category>
		<category><![CDATA[code]]></category>
		<category><![CDATA[part of lastsharp]]></category>

		<guid isPermaLink="false">http://dev.xscheme.de/?p=15</guid>
		<description><![CDATA[Die Klasse LastFM.XMLProcessor kann auf Basis des Last.FM-Protokolls 1.2 eine Verbindung zum Server herstellen, sowie Playlists herunterladen. (Scrobbling unterstütz sie nicht.) Eine leicht modifizierte Version dieser Klasse kommt auch in LastSharp zum Einsatz.

using System;
using System.Xml;
using System.Net;
using System.IO;
using System.Text;
using System.Collections;

namespace LastFM
{
    // ====================================================
    // Klasse mit Hilfsfunktionen
    [...]]]></description>
			<content:encoded><![CDATA[<p>Die Klasse LastFM.XMLProcessor kann auf Basis des Last.FM-Protokolls 1.2 eine Verbindung zum Server herstellen, sowie Playlists herunterladen. (Scrobbling unterstütz sie nicht.) Eine leicht modifizierte Version dieser Klasse kommt auch in LastSharp zum Einsatz.<br />
<span id="more-15"></span></p>
<pre class="c#" name="code">using System;
using System.Xml;
using System.Net;
using System.IO;
using System.Text;
using System.Collections;

namespace LastFM
{
    // ====================================================
    // Klasse mit Hilfsfunktionen
    // ====================================================
    class Utilities
    {
        // MD5 erstellen
        public static string MD5(string password)
        {
            byte[] textBytes = System.Text.Encoding.Default.GetBytes(password);
            try
            {
                System.Security.Cryptography.MD5CryptoServiceProvider cryptHandler;
                cryptHandler = new System.Security.Cryptography.MD5CryptoServiceProvider();
                byte[] hash = cryptHandler.ComputeHash(textBytes);
                string ret = "";
                foreach (byte a in hash)
                {
                    if (a &lt; 16)
                        ret += "0" + a.ToString("x");
                    else
                        ret += a.ToString("x");
                }
                return ret;
            }
            catch
            {
                throw;
            }
        }
        // Ruft eine URI auf und liefert sie als String zurück
        public static string ExecuteURI(string uri)
        {
            HttpWebRequest req = (HttpWebRequest)WebRequest.Create(uri);
            HttpWebResponse resp = (HttpWebResponse)req.GetResponse();
            StreamReader sr = new StreamReader(resp.GetResponseStream());
            string result = sr.ReadToEnd();
            sr.Close();
            return result;
        }
    }

    // ====================================================
    // Datenkapselung für Playlist und Tracks
    // ====================================================
    public class Playlist : ArrayList
    {
        public string PlaylistName = "";
        public DateTime Timestamp = new DateTime();
    }
    public class Track
    {
        public string Title="";
        public string Artist = "";
        public string Location = "";
        public string Album = "";
        public string Image = "";
        public int Duration = 0;
        // Gibt die Dauer des Tracks im Format MM:SS aus
        public string DurationString()
        {
            int sec = (this.Duration / 1000) % 60;
            int min = ((this.Duration / 1000) - sec) / 60;
            return ((min &lt; 10) ? "0" : "") + min + ":" + ((sec &lt; 10) ? "0" : "") + sec;
        }
    }

    // ====================================================
    // Handshake-Klasse für Bereitstellung der Handshake-Daten
    // ====================================================
    public class Handshake : Hashtable
    {
        /// Verarbeitet die Handshake-Rückgabe
        public Handshake(string HandshakeString)
        {
            string[] lines = HandshakeString.Split('\n');
            foreach (string line in lines)
            {
                string[] p = line.Split('=');
                this.Add(p[0], p[1]);
            }
            if (!this.ContainsKey("session") || !this.ContainsKey("base_path") ||
                !this.ContainsKey("base_url"))
            {
                throw new Exception("Ungültiger Handshake! (Passwort falsch?)");
            }
        }
        // Daten (nur die wichtigsten)
        public string SessionID  { get { return (string) this["session"]; } }
        public string BasePath   { get { return (string) this["base_path"]; } }
        public string BaseURL    { get { return (string) this["base_url"]; } }
        public string RequestURL { get { return "http://" + this.BaseURL + this.BasePath + "/"; } }
        // Der Hashtable ist nicht veränderbar
        public override bool IsReadOnly
        {
            get
            {
                return true;
            }
        }
    }

    // ====================================================
    // XMLProcessor-Klasse
    // ====================================================
    public class XMLProcessor
    {
        // ========================================
        // based on: http://code.google.com/p/thelastripper/wiki/LastFM12UnofficialDocumentation
        // ========================================
        // Login-Daten
        private string user;
        private string password;
        //aktuelle Radiostationsdaten
        private bool tuned_in = false;
        private string to_station = null;
        //
        public bool TunedIn { get { return tuned_in; } }
        public string Station { get { return to_station; } }
        public string Username { get { return user; } }
        // ========================================
        public XMLProcessor(string username, string password)
        {
            this.user        = username;
            this.password = Utilities.MD5(password);
        }
        ///
        /// Führt einen Handshake mit LastFM aus.
        ///
        public Handshake DoHandshake()
        {
            // Request ausführen
            string uri =
                "http://ws.audioscrobbler.com/radio/handshake.php?version=1.3.1.1" +
                "&amp;platform=win32&amp;username=" + this.username + "&amp;passwordmd5="
                + this.password + "&amp;language=de&amp;player=lastsharp";
            string resp = Utilities.ExecuteURI(uri);
            // Daten verarbeiten (in Handshake-Klasse)
            return new Handshake(resp);
        }

        ///
        /// Setzt die Radiostation
        ///
        public bool TuneIn(Handshake hs, string station)
        {
            // Operation ausführen
            string uri = hs.RequestURL + "adjust.php?session=" + hs.SessionID + "&amp;url=" + station + "〈=de";
            string resp = Utilities.ExecuteURI(uri);
            // Überprüfen + Status setzen
            if (resp.StartsWith("response=OK"))
            {
                this.tuned_in = true;
                this.to_station = station;
                return true;
            }
            else
            {
                this.tuned_in = false;
                this.to_station = null;
                return false;
            }
        }

        ///
        /// Ruft die Playlist ab
        ///
        public Playlist GetPlaylist(Handshake hs)
        {
            // Radiostation ausgewählt?
            if (!this.tuned_in) throw new Exception("Keine Radiostation angewählt.");
            // Playlist abrufen
            string uri =
                  hs.RequestURL + "xspf.php?sk=" + hs.SessionID +
                  "&amp;discovery=0&amp;desktop=1.3.1.1";
            string resp = Utilities.ExecuteURI(uri);
            XmlDocument doc = new XmlDocument();
            doc.LoadXml(resp);
            // =========================================
            // Verarbeiten
            // =========================================
            Playlist pl = new Playlist();
            XmlNode root = doc.FirstChild;
            if (root.Name != "playlist")
                throw new Exception("Ungültiges XML-Format der Playlist");
            // Titel der Playlist + Timestamp
            XmlNode pl_title = root.SelectSingleNode("title");
            if (pl_title != null) pl.PlaylistName = pl_title.InnerText;
            pl.Timestamp = DateTime.Now;
            // Tracklist
            XmlNode pl_tracks = root.SelectSingleNode("trackList");
            if (pl_tracks != null) for (int i = 0; i &lt; pl_tracks.ChildNodes.Count; i++)
                {
                    {
                        XmlNode trn = pl_tracks.ChildNodes[i];
                        Track tr = new Track();
                        tr.Artist     = trn.SelectSingleNode("creator").InnerText;
                        tr.Album    = trn.SelectSingleNode("album").InnerText;
                        tr.Title      = trn.SelectSingleNode("title").InnerText;
                        tr.Location = trn.SelectSingleNode("location").InnerText;
                        tr.Image    = trn.SelectSingleNode("image").InnerText;
                        tr.Duration = Convert.ToInt32(trn.SelectSingleNode("duration").InnerText);
                        pl.Add(tr);
                    }
                }
            return pl;
        }
    }
}</pre>
]]></content:encoded>
			<wfw:commentRss>http://dev.xscheme.de/2008/08/lastxml/feed/</wfw:commentRss>
		<slash:comments>0</slash:comments>
		</item>
	</channel>
</rss>
