Workshop Audio (Schritt 1)

Status
Für weitere Antworten geschlossen.

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Seit ein paar Tagen hab ich mich wieder mehr mit meinen 23 MP3-Files beschäftigt :D und wollte dazu hier mal was erzählen ...

Nachdem man seine MP3-Dateien auf eine DS kopiert hat, beschäftigt man sich zunehmend mehr damit, sie irgendwie wieder von der DS wegzukopieren: Meist will man sie sehr kreativ anhören ;).

Ein Verfahren, eine MP3 anzuhören, würde zum Beispiel dadurch möglich, dass man sie in eine Web-Seite als Link integriert. Bei den meisten Browsern gibt es auch die Möglichkeit, sie als Hintergrund-Musik einzubetten. Oft wird aber ein Plug-In verwendet, um sich dann im Browser die Musik anzuhören bzw. die Lautstärke zu regulieren. Allerdings muss man sich schon was einfallen lassen, um mehrere MP3s geschickt ablaufen zu lassen.

Komfortabler geht es, wenn man seine MP3-Files als PlayList einem Browser-Plug-In zur Verfügung stellen kann. Ein offener PlayList-Standard ist XSPF ("spiff"). XSPF wird als XML-Strecke genutzt, z. B. sieht eine kleine PlayList da so aus:

Rich (BBCode):
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
<title>PlayList-Title</title>
    <trackList>
        <track><location>http://example.com/song_1.mp3</location></track>
        <track><location>http://example.com/song_2.mp3</location></track>
        <track><location>http://example.com/song_3.mp3</location></track>
    </trackList>
</playlist>

Natürlich kann man da noch viel mehr Zeugs (Informationen) einbetten.

Es gibt zu XSPF eine ganze Reihe von schönen Tools und auch einen recht nett anzuschauenden Player als Flash-Plug-In.

Der Player besteht genau genommen aus zwei Teilen, einmal was für die PlayLists hier und einmal was zum Abspielen einer MPS hier. Die Einbettung ist recht einfach als Objekt.

Angenommen der Server wäre eine DS mit DDNS-Namen syno.de und das Verzeichnis wäre /volume1/web aus dem diese HTML-Seite aufgerufen würde und die beiden Download-Flash-Dateien sowie die XML-Playliste wären auch in diesem Verzeichnis, also so in etwa:

Rich (BBCode):
/volume1/web/XSPF.swf
/volume1/web/FMP3.swf
/volume1/web/playlist.xml
dann würde das Objekt wie folgt aussehen:

Rich (BBCode):
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" 
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" 
width="260" height="300" id="XSPF-FMP3" align="middle">
<param name="allowScriptAccess" value="sameDomain" />
<param name="movie" value="http://syno.de/XSPF.swf?action=play&playlist=http://syno.de/playlist.xml&folder=http://syno.de/&textcolor=033066&color=E6E9FB&loop=playlist&lma=yes&display=1@. @0@ - @" />
<param name="quality" value="high" />
<param name="bgcolor" value="#ffffff" />
<embed src="http://syno.de/XSPF.swf?action=play&playlist=http://syno.de/playlist.xml&folder=http://syno.de/&textcolor=033066&color=E6E9FB&loop=playlist&lma=no&display=@. @0@ - @" 
quality="high" bgcolor="#ffffff" width="260" height="300" name="XSPF" align="middle" 
allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" />
</object>

Man kann nun die PlayList händisch erstellen, per Anwendung basteln oder sich aus einer Dateistruktur oder Datenbank-Struktur generieren lassen (Das mach ich mal im nächsten Schritt). Wichtig ist nur, dass sie irgendwie per URL erreichbar ist und dass die MP3-Files in ihr auch tatsächlich existieren (und möglichst hübsche Tags haben).

Zusammenfassung:
Es gibt 4 Baustellen: (1) irgendwo müssen die MP3 gespeichert sein, (2) irgendwo muss sich jemand mal eine PlayList ausgedacht haben (3) irgendwo braucht man einen MP3-Player, z. B. als Flash-Programm im Browser und (4) irgendwo muss eine PlayList für einen MP3-Player aufbereitet werden. Zu Baustelle (3) und (4) haben wir hier was gemacht.

Wie immer gilt natürlich, es geht alles auf eigene Kappe :D

Itari
.
 

Anhänge

  • xspf.jpg
    xspf.jpg
    16,3 KB · Aufrufe: 877
Zuletzt bearbeitet:

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Workshop Audio (Schritt 2)

Wie versprochen, geht es nun um die PlayLists. Im Folgenden geht es um die Konvertierung der m3u-PlayLists, welche die Audio-Station verwendet.

Die Audio-Station legt ein paar Infos zu den PlayLists, welche man mit ihr erstellen und pflegen kann in der Postgres-Datenbank ab, im Wesentlichen: Name, Größe, Speicherort. Die eigentlichen PlayLists werden im Order /volume1/music/playlists abgelegt und sie sehen so aus (z. B. myplaylist.m3u):

Rich (BBCode):
../song1.mp3
../song2.mp3
../song3.mp3
../verzeichnis1/song4.mp3
Die Verzeichnisstruktur kann dabei natürlich noch tiefer gehen.

Um auf den Ordner music mit dem Unterverzeichnis playlists aus einem PHP-Skript gut zugreifen zu können, kann man die PHP-Option 'open_basedir' beeinflussen (was aber nicht unbedingt der Sicherheit förderlich ist) oder sich einen Link auf das Verzeichnis /volume1/music ins Verzeichnis /volume1/web setzen. Als erstes legt man ein Verzeichnis an:
Rich (BBCode):
mkdir /volume1/web/MP3
Dann kann man in der /etc/rc.local die Bindung herstellen:
Rich (BBCode):
mount --bind /volume1/music /volume1/web/MP3
Eventuell muss man sich noch um den Eigentümer bzw. die Zugriffsrechte kümmern.

Jetzt sollte ein Zugriff auf die MP3-Files auch vom Browser aus möglich sein:
Rich (BBCode):
http://syno/MP3/song1.mp3
Auch die PlayLists dürften sich nun im Browser anfassen lassen:
Rich (BBCode):
http://syno/MP3/playlists/playlist1.m3u

Um die PlayLists nun in das XSPF-Format zu konvertieren, gibt es bereits fertige Lösungen, z. B. in Phyton geschrieben *guck*. Ich hab mir gedacht, dass wir es auch selbst mit PHP hinbekommen, weil ich da was Schönes entdeckt habe, mit dem man auch MP3-Dateizweige ohne fertige m3u-PlayLists abarbeiten kann (Das werd ich in einem der nächsten Schritte dann mal erläutern). Das Teil, was wir nun brauchen, ist der PHP XSPF Playlist Generator, den man sich hier downloaden kann. Wichtig ist für uns das Verzeichnis getid3, welches hier residieren sollte: /volume1/web/getid3.

Das Skript für die m3u-nach-XSPF-Konvertierung hab ich hier hingelegt: /volume1/web/playlist_converter.php:

PHP:
<?php
require_once('getid3/getid3.php');
define("ROOT", 					$_SERVER["HTTP_HOST"]);
define("MP3_PATH",			dirname($_REQUEST['path']));
$getid3 = new getID3;
$getid3->encoding = 'ISO 8859-1';

function utf8_mydecode($html){                      // reverse simple translation of german letters
  $a = array("\xC3\xA4"=>"\xE4","\xC3\xB6"=>"\xF6","\xC3\xBC"=>"\xFC",
             "\xC3\x84"=>"\xC4","\xC3\x96"=>"\xD6","\xC3\x9C"=>"\xDC","\xC3\x9F"=>"\xDF",
             "\xC3\xB2"=>"\xF2","\xC3\xBA"=>"\xFA","\xC3"=>"\xC2","\xC2"    =>""
             );          
  return strtr($html,$a);
}

$files=@file($_REQUEST['path']);
if ($files) {
  $playlist_title=preg_replace('/^.*\/(.*).m3u/','\\1',$_REQUEST['path']);
  print <<< END_HEADER
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
 	<title>$playlist_title</title>   
    <trackList>
    
END_HEADE
	$counter = $i = 0;
    foreach ($files as $full_name => $short_name) {
      $getid3->Analyze($file);		    
        $artist = $title = '';
            if (@$getid3->info['tags']) {
            foreach ($getid3->info['tags'] as $tag => $tag_info) {
                if (@$getid3->info['tags'][$tag]['title']) {
                    $artist = @$getid3->info['tags'][$tag]['artist'][0];
                    $title  = @$getid3->info['tags'][$tag]['title'][0];
                    $album  = @$getid3->info['tags'][$tag]['album'][0];
                    break;
                } else {
				        $title  =  utf8_mydecode(basename($short_name, ".mp3"));
				}
            }
        } else {
		    $title =  utf8_mydecode(basename($short_name, ".mp3"));
				}
        $url = ROOT . "/" . MP3_PATH . "/" . $short_name;
        
        if (strpos($url, "http://") === false) {
	        $url = "http://" . $url;
        }       
        $info = ROOT;        
        if (strpos($info, "http://") === false) {
	        $info = "http://" . $info;
        }        
        print <<< END_TRACK
		<track>
			<location>{$url}</location>		
			<!-- artist or band name -->
			<creator>{$artist}</creator>	
			<!-- album name -->
			<album>{$album}</album>
			<!-- name of the song -->
			<title>{$title}</title>
		</track>	
END_TRACK;
	}
	print <<< END_FOOTER
	</trackList>
</playlist>
END_FOOTER;
}
?>
Aufgerufen wird das Skript immer mit einem URL-Search-String, welcher auf den Pfad einer PlayList weist:
Rich (BBCode):
playlist_converter.php?path=MP3/playlists/myplaylist.m3u
 
Zuletzt bearbeitet:

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio Workshop (Schritt 3)

Jetzt basteln wir uns das mal in unserem netten Player zusammen:

Rich (BBCode):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
	<head>
		<title>XSPF-Player</title>
	<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /></head>
<body>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000" 
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" 
width="260" height="300" id="XSPF-FMP3" align="middle">
<param name="allowScriptAccess" value="sameDomain" />
<param name="movie" value="XSPF.swf?action=play&playlist=playlist_converter.php?path=MP3/playlists/<?= $_GET['playlist'] ?>.m3u&folder=./&textcolor=033066&color=E6E9FB&loop=playlist&lma=yes&display=1@. @0@ - @" />
<param name="quality" value="high" />
<param name="bgcolor" value="#ffffff" />
<embed src="../XSPF.swf?action=play&playlist=playlist_converter.php?path=MP3/playlists/<?= $_GET['playlist'] ?>.m3u&folder=./&textcolor=033066&color=E6E9FB&loop=playlist&lma=no&display=@. @0@ - @" 
quality="high" bgcolor="#ffffff" width="520" height="600" name="XSPF" align="middle" 
allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" />
</object>
</body></html>

Schon die Zeilen mit der PlayList entdeckt? Es gibt sie 2x ... einmal für eine Sorte Browser und einmal für andere Sorte :rolleyes:

Rich (BBCode):
playlist=playlist_converter.php?path=MP3/playlists/<?= $_GET['playlist'] ?>.m3u

Hier wird also noch ein wenig verschlankt, damit man den Player direkt mit dem Namen einer PlayList aufrufen kann. Man hätte das auch in dem Skript 'palylis_converter.php' so machen können, aber ich denke, dass wir das mit dem Pfad allgemeiner halten sollten, also nicht nur für m3u-PlayLists der Audio-Station, sondern im Grunde auch für andere PlayLists, wie z. B. /tmp/--DynaRandom100--.m3u oder Twonky-PlayLists ...

Rich (BBCode):
http://syno/player.php?playlist=myplaylist

Jetzt ist es auch nicht mehr schwer, sich was vorzustellen, was alle PlayLists anzeigt und wo man sich dann eine auswählen könnte. Genauso kann man natürlich auch die Web-Seite mit dem Player noch anpassen: Farbe, Größe usw. ... fast alles lässt sich ja einstellen.

So das wars wieder mal. Alles geht wir immer auf eigne Kappe :D. Alle verwendeten Skripte sind GPL oder ähnlicher Natur. Und selbstverständlich stelle ich meine kleinen Erweiterungen und Anpassungen auch wieder unter GPL3, damit es sich auch jeder anschauen, modifizieren und benutzen kann.

Itari
 
Zuletzt bearbeitet:

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio-Workshop (Schritt 4)

Manchmal hat man keine Lust, eine PlayList anzulegen und weil man so hübsch Verzeichnisstrukturen in /volume1/music hat (z. B. nach Album strukturiert), will man einfach mal ein Album (= CD) anhören, so wie früher auf dem CD_Player :D

Jetzt wäre ein PHP-Skript, welche automatisch eine PlayList von einem Verzeichnis macht, hübsch. Wir nennen das PHP-Skript mal /volume1/web/playlist_generator.php. Es bekommt wie playlist_converter.php auch beim Aufruf per Search-String (in diesem Fall '?path=') mit, welches Verzeichnis durchsucht werden soll.

PHP:
<?php
require_once('getid3/getid3.php');
define("ROOT", 					$_SERVER["HTTP_HOST"]);
$getid3 = new getID3;
$getid3->encoding = 'ISO 8859-1';

function iso8859_mydecode($html){                   // simple translation of german letters
  $a = array("\xE4"=>"\xC3\xA4","\xF6"=>"\xC3\xB6","\xFC"=>"\xC3\xBC",
             "\xC4"=>"\xC3\x84","\xD6"=>"\xC3\x96","\DC"=>"\xC3\x9C","\xDF"=>"\xC3\x9F");
  return strtr($html,$a);
}
function utf8_mydecode($html){                      // reverse simple translation of german letters
  $a = array("\xC3\xA4"=>"\xE4","\xC3\xB6"=>"\xF6","\xC3\xBC"=>"\xFC",
             "\xC3\x84"=>"\xC4","\xC3\x96"=>"\xD6","\xC3\x9C"=>"\xDC","\xC3\x9F"=>"\xDF",
             "\xC3\xB2"=>"\xF2","\xC3\xBA"=>"\xFA","\xC3"=>"\xC2","\xC2"    =>""
             );          
  return strtr($html,$a);
}

$dir = iso8859_mydecode($_REQUEST['path']);
$dirname = basename($_REQUEST['path']);
exec('ls "'.$dir.'"/*.mp3',$files);
//var_dump($files);
if ($files) {
	print <<< END_HEADER
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
	<!-- Playlist generated automatically using script from www.lasmit.com -->
 	<title>{$dirname}</title>   
    <trackList>
END_HEADER;
foreach($files as $file) $farray[preg_replace('/^(.*) - .* - (..).mp3$/U','\\2',$file)]=$file;
ksort($farray);
unset($files);
foreach($farray as $file) $files[]=$file;   
	$counter = $i = 0;
    foreach ($files as $full_name => $short_name) {
	    $getid3->Analyze($dir . "/" . $short_name);		    
        $artist = $title = '';
            if (@$getid3->info['tags']) {
            foreach ($getid3->info['tags'] as $tag => $tag_info) {
                if (@$getid3->info['tags'][$tag]['title']) {
                    $artist = @$getid3->info['tags'][$tag]['artist'][0];
                    $title  = @$getid3->info['tags'][$tag]['title'][0];
                    $album  = @$getid3->info['tags'][$tag]['album'][0];
                    break;
                } else {
				$title  =  utf8_mydecode(basename($short_name, ".mp3"));
				}
            }
        } else {
		$title  =  utf8_mydecode(basename($short_name, ".mp3"));
		}		
        $url = ROOT . "/" . $short_name;        
        if (strpos($url, "http://") === false) {
	        $url = "http://" . $url;
        }
        $info = ROOT;        
        if (strpos($info, "http://") === false) {
	        $info = "http://" . $info;
        }
        print <<< END_TRACK
		<track>
			<location>{$url}</location>			
			<!-- artist or band name -->
			<creator>{$artist}</creator>			
			<!-- album name -->
			<album>{$album}</album>		
			<!-- name of the song -->
			<title>{$title}</title>
		</track>	

END_TRACK;
	}
	print <<< END_FOOTER
	</trackList>
</playlist>
END_FOOTER;
}
?>

Also der Aufruf wäre dann:

Rich (BBCode):
http://syno/playlist_generator.php?path=MP3/Verzeichnis1

Das Ergebnis wäre eine XSPF-Playlist, generiert aus den mp3-Files in betreffendem Verzeichnis. Allerdings werden nur die Dateien des Verzeichnisses genommen; ein Rekursion für das Abklappern ganzer Bäume ist nicht eingebaut. Also man darf zwar tiefer in die Verzeichnis-Hierarchie gehen, aber es wird immer nur eine Ebene nach Dateien gescannt. Könnte man noch ausbauen, muss man aber nicht unbedingt.

Meine MP3-Dateinamen sind in der Regel wie folgt aufgebaut:

Rich (BBCode):
/volume1/music/Interpret/Album - Titel - Tracknummer.mp3

Ich schreib das deswegen hier hier hin, weil es ja sein kann, dass irgendwelche Dateinamen mal Ärger machen könnten und dann kann man sich daran orientieren, mit was ich das getestet hab. Ümläüte ;) dürfen in Maßen in den Dateinamen drin sein; vielleicht werden nicht alle spanischen, französischen, dänischen usw. Sondersymbole richtig angezeigt ....
 
Zuletzt bearbeitet:

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio Workshop (Schritt 5)

Jetzt brauchen wir nur noch eine leicht verbesserte Version des players.php, damit wir auch bequem das richtige Verzeichnis auswählen können. Ich hab diese Player-Version /volume1/web/player2.php genannt.

PHP:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
	<head>
		<title>XSPF-Player</title>
	<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /></head>
<body>
<?php
function utf8_mydecode($html){                      // reverse simple translation of german letters
  $a = array("\xC3\xA4"=>"\xE4","\xC3\xB6"=>"\xF6","\xC3\xBC"=>"\xFC",
             "\xC3\x84"=>"\xC4","\xC3\x96"=>"\xD6","\xC3\x9C"=>"\xDC","\xC3\x9F"=>"\xDF",
             "\xC3\xB2"=>"\xF2","\xC3\xBA"=>"\xFA","\xC3"=>"\xC2","\xC2"    =>""
             );          
  return strtr($html,$a);
}
function myscandir($dir,$path) {
  global $dirs;
  $dirarray = @scandir($path.'/'.$dir); 
  natcasesort($dirarray);
  foreach($dirarray as $entry) {
    $item=$path.'/'.$dir.'/'.$entry;
    if (@filetype($item)=="dir" && $entry != "." && $entry != ".." && $entry != "#recycle" && $entry != "@eaDir") {
      $dirs[] = $item;
      myscandir($entry,$path.'/'.$dir);    
    }}
}
if (!isset($_REQUEST['path'])) {
  $dirs = array();
  myscandir("MP3",".");
  print '<form name="f" method="get" action="player2.php" /><select name="path">';
  foreach($dirs as $entry) print '<option value="'.$entry.'">'.utf8_mydecode($entry).'</option>';  
  print '</select><input type="submit"></form>' ; 
  }
else { ?>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" 
width="260" height="300" id="XSPF-FMP3" align="middle">
<param name="allowScriptAccess" value="sameDomain" />
<param name="movie" value="XSPF.swf?action=play&playlist=playlist_generator.php?path=MP3/<?= $_GET['path'] ?>&folder=./&textcolor=033066&color=E6E9FB&loop=playlist&lma=yes&display=1@. @0@ - @" />
<param name="quality" value="high" />
<param name="bgcolor" value="#ffffff" />
<embed src="../XSPF.swf?action=play&playlist=playlist_generator.php?path=<?= $_GET['path'] ?>&folder=./&textcolor=033066&color=E6E9FB&loop=playlist&lma=no&display=@. @0@ - @" 
quality="high" bgcolor="#ffffff" width="260" height="300" name="XSPF" id="XSPF" align="middle" liveConnect="true"
allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" 
/>
</object>
<?php } ?>
</body></html>

Das player2.php-Skript kann direkt mit einem Search-String, zum Beispiel:

Rich (BBCode):
http://syno/player2.php?path=MP3/verzeichnis1

aufgerufen werden, oder eben ohne:

Rich (BBCode):
http://syno/player2.php

Und dann macht sich das Skript die Mühe, alle Verzeichnisse unter MP3 bzw. weils ja ein Link ist unter /volume1/music abzuklappern und das auch noch rekursiv ... und zeigt sie in einem select-Auswahlfeld an. Nun kann man sich was aussuchen und damit wird das Skript sozusagen selbst wieder aufgerufen, nur diesmal mit gesetztem Search-String (?path=). Auch diesmal könnte es sein, dass nicht alle Verzeichnisnamen total Sonderzeichengerecht angezeigt werden, aber so 95% sollte es schon sein ;)

Viel Spaß beim Spielen ...

Itari
 

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio Workshop (Schritt 6)

Wenn das nun Revue passieren lässt, dass man ein Verzeichnis als Basis für eine PlayList-Generierung nutzen kann, dann könnte man das natürlich auch ausbauen. Zum Beispiel da durch, dass man Verzeichnisse einrichtet und dort hinein genau die Kopien seiner MP3 ablegt, für die man sich eine PlayList wünscht ... ein eigener Sampler sozusagen.

Jetzt hör ich die Schreie, Platzverschwendung ... wer will Dublette verwalten usw. :D Muss ja nicht sein, kann man ja mit Links erledigen ... ;) Hardlinks!

Beispiel:

Rich (BBCode):
mkdir /volume1/music/Sampler1
ln /volume1/music/Verzeichnis1/song_7.mp3 /volume1/music/Sampler1
ln /volume1/music/Verzeichnis7/song_3.mp3 /volume1/music/Sampler1
...

Naja wird nun mancher sagen ... ich spiele sonst per Windows meine MP3-Songs in die Share 'music' auf der DS ... jetzt auf die Kommandozeile wegen ein paar Links ... da mach ich doch lieber wie bislang PlayListen mit der Audio-Station oder so ...

Ja kann man denken, wenn ... es gibt auch unter Windows Hardlinks ... und ein tolles Programm als Explorer-Extension zum Anlegen von Hardlinks (und noch mehr) und das gilt natürlich auch für Samba-Shares ... also alles kein Problem. Wo gibts das Teil? Hier: http://schinagl.priv.at/nt/hardlinkshellext/hardlinkshellext.html

Man sollte ein wenig damit Erfahrung sammeln, bevor man produktiv wird. Danach kann man sich seine Sampler-Verzeichnisse anlegen und genauso wie in Schritt 5 damit weiterarbeiten ... es ist wie ein Verzeichnis zu handhaben. Allerdings muss man manchmal bei Verzeichnisse darauf achten, dass man auch schreibend zugreifen darf .... das müsste man dann über den DS-File-Manager erledigen ... oder auch der Kommandozeile. Wird aber eher selten passieren.


Wie immer geht alles auf eigene Kappe :D

Itari

PS. Die Verzeichnisse müssen nicht wirklich 'Sampler' heißen ... :D
 

Anhänge

  • hardlink.jpg
    hardlink.jpg
    29,4 KB · Aufrufe: 835

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio Workshop (Schritt 7)

So, nun hab ich noch mal meine Frau testen lassen und wie immer ... voller Erfolg ... :rolleyes:

Bei den MP3-Dateinamen sind Sonderzeichen ein Problem und ich hab im Moment keine Lust, das zu lösen.
Folgende Sonderzeichen gehen in den Dateinamen vermutlich nicht: & ' " ; * \ / < > und vielleicht noch welche.

Also entweder die Dateien umbenennen, oder sich eine Lösung einfallen lassen.

Itari
 

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio Workshop (Schritt 8)

Nachdem wir uns nun PlayLists und Player angeschaut haben, bleibt noch die Frage offen, wie kommen wir an MP3 dran ... irgendwelche Ideen? :rolleyes:

Ich grab da mal den Streamripper aus (ipkg install streamripper), welcher Webradio-Streams als MP3s abspeichert. Man kann sich hier im Wiki noch ein wenig darüber informieren.

Erstmal hab ich ein Verzeichnis angelegt, wo die gerippten Dateien liegen sollen:
Rich (BBCode):
mkdir /volume1/web/STREAMRIPPER; chmod 777 /volume1/web/STREAMRIPPER

Dann machen wir das PHP-Skript /volume1/web/streamripper.php, um eine Oberfläche für die erforderlichen Eingaben zu haben, dabei werden die WebRadioURLs aus der Datei /usr/syno/etc/audio/default_radio_station.asx gelesen, das ist die Datei, die von der Audio-Station für aktive Stationen verwaltet wird. Dort wäre auch die Möglichkeit vorhanden, neue WebRadioStationen einzutragen.

PHP:
<?php
if ($_REQUEST['action'] == 'go') {
  $cmd='streamripper '.$_REQUEST['stream'].' -s -d '
      .$_REQUEST['path'].' '
      .($_REQUEST['duration']=='(unendlich)'?'':'-l '.$_REQUEST['duration'])
      .' --codeset-filesys=UTF-8 --codeset-id3=UTF-8 --codeset-metadata=UTF-8';
ob_implicit_flush();
?>
<html><head>
<script>function $(e){return document.getElementById(e);}</script>
<style>
body,input,option,select,td {font:12px consolas;}
select,input {width:300px}
</style>
</head>
<body>
<?= $cmd ?>
</body>
<?php 
$ret = exec($cmd,$stdout);
print '<br/>';
print $stdout[0].'<br/>'.$stdout[1].'<br/>'.$stdout[2].'<br/>'.$stdout[3].'<br/>'.$stdout[4].'<br/>'.$stdout[5].'<br/>';
print $stdout[7].'<br/>'.$stdout[8].'<br/>'.$stdout[9].'<br/>';
ob_start();
}
else {
  exec("cat /usr/syno/etc/audio/default_radio_station.asx",$webradio);
  foreach($webradio as $item) {
  $out.=preg_match('/<ASX/',$item)     ?preg_replace('/<ASX.*>/','<select name="stream">',$item)       :'';
  $out.=preg_match('/<\/ASX/',$item)   ?preg_replace('/<\/ASX.*>/','</select>',$item)    :'';  
  $out.=preg_match('/<Entry>/',$item)  ?preg_replace('/<Entry>/','<option ',$item)       :'';
  $out.=preg_match('/<\/Entry>/',$item)?preg_replace('/<\/Entry>/','</option>',$item)    :'';
  $title=preg_match('/<TITLE>/',$item) ?preg_replace('/<TITLE>(.*)<\/TITLE>/','\\1',$item):$title;
  $out.=preg_match('/<Ref href = /',$item)?preg_replace('/<Ref href = (.*)\/>/','value=\\1>'.$title,$item):''; 
  $out.=preg_match('/<Play>/',$item)   ?preg_replace('/<Play>.*<\/Play>/','',$item)      :'';
  $out.=preg_match('/<Default>/',$item)?preg_replace('/<Default>.*<\/Default>/','',$item):'';;                
  }
  $select_webradio=$out;
  $date=date("Ymd_Hi");
?>
<html><head>
<script>function $(e){return document.getElementById(e);}</script>
<style>
body,input,option,select,td {font:14px verdana;}
select,input {width:300px}
</style>
</head>
<body>
<form name="f" method="get" action="streamripper.php">
<input type="hidden" name="action" id="action" value=""/>
<table><tr><td>URL des Streams:</td><td><?= $select_webradio ?></td></tr>
<tr><td>Aufzeichnungsdauer in Sekunden:</td><td><input type="text" name="duration" size="30" value="(unendlich)" /></td></tr>
<tr><td>Speicherpfad:</td><td><input type="text" name="path" size="30" value="/volume1/web/STREAMRIPPER" /></td></tr>
<tr><td></td><td><input type="button" value="aktivieren" onclick="$('action').value='go';f.submit()"></td></tr></table>
</form></body>
<?php } ?>

Ich hab mich da ein wenig um die XML-Geschichte gedrückt, weil ich nicht genau weiß, wie viel davon vom eingebauten PHP auch gekonnt wird.

Wenn man dann den Streamripper gestartet hat, werden die MP3 im Verzeichnis /volume1/web/STREAMRIPPER/incomplete abgelegt. Darauf können wir wieder mit dem Skript /volume1/web/playlist_generator.php zugreifen und eine XSPF-PlayList erzeugen.

Tipp: Gibt man als Dauer 0 ein, dann wird nicht wirklich was gemacht und man kann den angezeigten Aufruf leicht in die Zwischenablage kopieren und damit seine Crontab füttern ... dann wären auch zeitversetze Aufzeichnungen möglich.

Damit dann die einzelnen Dateien nach Download-Datum/Zeit absteigend geordnet werden, hab ich das Skript playlist_generator.php noch ein wenig angepasst (Zeile 22 und Zeile 32):

PHP:
exec('ls -t "'.$dir.'"/*.mp3',$files); 
...
if ($dirname != 'STREAMRIPPER' && $dirname != 'incomplete' ) ksort($farray);

Das player2.php-Skript wird nun als /volume1/web/player3.php auf die neuen Verhältnisse (der Pfad in myscandir und action) angepasst:

Rich (BBCode):
...
if (!isset($_REQUEST['path'])) {
  $dirs[]='STREAMRIPPER';
  myscandir("STREAMRIPPER",".");
  print '<form name="f" method="get" action="player3.php" /><select name="path">';
...

Und dann geht es auch schon los ... Wenn man den player3.php im Browser aktualisiert, sieht man nach einiger Zeit, wie immer neue Stücke in der PlayList auftauchen. Wenn man den Streamripper ohne Zeitbegrenzung laufen lässt, dann muss man ihn händisch irgendwann stoppen (Anleitung steht im Wiki). Ich denke, dass man das noch per PHP-Skript weiter ausbauen kann und vielleicht auch sowas wie eine Kopier-Station hin bekommt .... aber das sind ja keine wirklichen notwendigen dinge, denn man kann selbstverständlich die gerippten MP3 auch über den Windows-Explorer oder die File-Station verschieben usw. Klar, man kann noch mehr Parameter beim Aufrufen des Streamrippers angeben und vielleicht gibt es auch den ein oder anderen noch, der in das Skript streamripper.php hineingehört ...

Da ich nun alle 4 Baustellen abgegrast hab und man sich sicherlich noch viele darum liegenden Dinge anschauen kann, ist zwar der Workshop nicht wirklich zu Ende, aber sozusagen ist erstmal die Luft raus. Deswegen wünsche ich viel Spaß beim Ausprobieren. Und dran denken: es geht alles auf eigene Kappe :D und es steht alles unter GPL3.

Itari
 

Anhänge

  • streamripper.jpg
    streamripper.jpg
    20,7 KB · Aufrufe: 829
Zuletzt bearbeitet:

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Audio Workshop (Schritt 9)

Die Playlisten von Kamils (aka JMFox) Jukebox-Projekt sind in einer MySQL-Datenbank. Daraus lassen sich auch recht einfach XSPF-PlayLists generieren.

Das folgende PHP-Skript /volume1/web/playlist_generator_db.php bekommt als Search-String den jeweilige Playlisten-Namen für die Datenbank-Abfrage mit:

PHP:
<?php
define("ROOT", 					$_SERVER["HTTP_HOST"]);
function iso8859_mydecode($html){                   // simple translation of german letters
  $a = array("\xE4"=>"\xC3\xA4","\xF6"=>"\xC3\xB6","\xFC"=>"\xC3\xBC",
             "\xC4"=>"\xC3\x84","\xD6"=>"\xC3\x96","\DC"=>"\xC3\x9C","\xDF"=>"\xC3\x9F");
  return strtr($html,$a);
}
$playlist=$_REQUEST['playlist'];
$conn = @mysql_connect("localhost","root");
mysql_select_db("jukebox",$conn);
$query = "select filename,full_path,artist,track "
        ."from playlist a, playlist_song b, song c "
        ."where a.ID=b.playlistID AND b.songID=c.ID AND a.name='".$playlist."' "
        ."order by artist, cast(track as decimal)";
$recs = mysql_query($query);
if ($recs) {
	print <<< END_HEADER
<?xml version="1.0" encoding="UTF-8"?>
<playlist version="1" xmlns="http://xspf.org/ns/0/">
	<!-- Playlist generated automatically using script from www.lasmit.com -->
 	<title>{$playlist}</title>   
    <trackList>
END_HEADER;
  while ($rec = mysql_fetch_assoc($recs)) {
    $url="http://" .ROOT. '/MP3/'. iso8859_mydecode($rec['full_path']).'/'.iso8859_mydecode($rec['filename']);
    $title=iso8859_mydecode(preg_replace('/^(.*)\.mp3$/U','\\1',$rec['filename']));
print <<< END_TRACK
		<track>
			<location>{$url}</location>			
			<title>{$title}</title>
		</track>	
END_TRACK;
	}
	print <<< END_FOOTER
	</trackList>
</playlist>
END_FOOTER;
}
?>

Ich habe es mir hier relativ einfach mit der Titelübergabe die XSPF-PlayList gemacht ... wer da mehr aus den Tags zusammenbasteln mag, in der Datenbank steht ja alles recht schön drin, der ergänzt einfach den SQL-Aufruf um die Felder.

Als Player-Aufrufs-Version habe ich /volume1/web/player4.php mit dem Search-String ?playlist= eine leicht modifizierte Version zusammenstellt. Aufruf also:

Rich (BBCode):
http://syno/player4.php?playlist=dbplaylist1

und das Skript:

Rich (BBCode):
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html>
	<head>
		<title>XSPF-Player</title>
	<meta http-equiv="Content-Type" content="text/html; charset=ISO-8859-1" /></head>
<body>
<object classid="clsid:d27cdb6e-ae6d-11cf-96b8-444553540000"
codebase="http://fpdownload.macromedia.com/pub/shockwave/cabs/flash/swflash.cab#version=7,0,0,0" 
width="260" height="300" id="XSPF-FMP3" align="middle">
<param name="allowScriptAccess" value="sameDomain" />
<param name="movie" value="XSPF.swf?action=play&playlist=playlist_generator_db.php?playlist=<?= $_GET['playlist'] ?>&folder=./&textcolor=033066&color=E6E9FB&loop=playlist&lma=yes&vol=40&display=1@. @0@ - @" />
<param name="quality" value="high" />
<param name="bgcolor" value="#ffffff" />
<embed src="../XSPF.swf?action=play&playlist=playlist_generator_db.php?playlist=<?= $_GET['playlist'] ?>&folder=./&textcolor=033066&color=E6E9FB&loop=playlist&lma=no&vol=40&display=@. @0@ - @" 
quality="high" bgcolor="#ffffff" width="260" height="300" name="XSPF" id="XSPF" align="middle" liveConnect="true"
allowScriptAccess="sameDomain" type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" 
/>
</object>
</body></html>

So damit hätten wir auch das Thema "Wie generiere ich aus einer Datenbank eine PlayList" abgefackelt und Kamil kann sich freuen, dass er mit seiner Lösung einen super Input für XSPF-Player liefert :)

Damit hätten wie jetzt 5 XSPF-Quellen für einen XSPF-Player aufbereitet:
1] Code-Fragment -> direktes Abspielen einer XSPF-XML-Playliste
2] player.php -> Abspielen einer konvertierten m3u-Playliste
3] player2.php -> Abspielen einer aus einem Verzeichnis generierten Playliste
4] player3.php -> Abspielen einer aus einem Streamripper-Verzeichnis generierten Playliste
5] player4.php -> Abspielen einer aus einer Datenbank-Abfrage generierten Playliste

Viel Spass

Itari
 
Zuletzt bearbeitet:

coolhot

Benutzer
Mitglied seit
01. Mrz 2009
Beiträge
926
Punkte für Reaktionen
0
Punkte
0
Hi Itari,

danke für den tollen Workshop. Ich habe Schritt 8 (streamripper) in abgewandelter Form umgesetzt. Hier die einzelnen Schritte und Vorbedingungen auf meiner DS:

- Audiostation läuft nicht
- Web Station läuft nicht
- Streamripper = /opt/bin/streamripper (im PATH) läuft auf Kommandozeile
- Ordner /usr/syno/synoman/webman/3rdparty/sr/ verlinkt auf
- Ordner /volume1/@appstore/sr_web/ mit Inhalt

sr_web.php
Rich (BBCode):
<?php
if ($_REQUEST['action'] == 'go') {
  $cmd='streamripper '.$_REQUEST['stream'].' -s -d '
      .$_REQUEST['path'].' '
      .($_REQUEST['duration']=='(unendlich)'?'':'-l '.$_REQUEST['duration'])
      .' --codeset-filesys=UTF-8 --codeset-id3=UTF-8 --codeset-metadata=UTF-8';
ob_implicit_flush();
?>
<html><head>
<script>function $(e){return document.getElementById(e);}</script>
<style>
body,input,option,select,td {font:12px consolas;}
select,input {width:300px}
</style>
</head>
<body>
<?= $cmd ?>
</body>
<?php 
$ret = exec($cmd,$stdout);
print '<br/>';
print $stdout[0].'<br/>'.$stdout[1].'<br/>'.$stdout[2].'<br/>'.$stdout[3].'<br/>'.$stdout[4].'<br/>'.$stdout[5].'<br/>';
print $stdout[7].'<br/>'.$stdout[8].'<br/>'.$stdout[9].'<br/>';
ob_start();
}
else {
  exec("cat /volume1/@appstore/sr_web/stations.asx",$webradio);
  foreach($webradio as $item) {
  $out.=preg_match('/<ASX/',$item)     ?preg_replace('/<ASX.*>/','<select name="stream">',$item)       :'';
  $out.=preg_match('/<\/ASX/',$item)   ?preg_replace('/<\/ASX.*>/','</select>',$item)    :'';  
  $out.=preg_match('/<Entry>/',$item)  ?preg_replace('/<Entry>/','<option ',$item)       :'';
  $out.=preg_match('/<\/Entry>/',$item)?preg_replace('/<\/Entry>/','</option>',$item)    :'';
  $title=preg_match('/<TITLE>/',$item) ?preg_replace('/<TITLE>(.*)<\/TITLE>/','\\1',$item):$title;
  $out.=preg_match('/<Ref href = /',$item)?preg_replace('/<Ref href = (.*)\/>/','value=\\1>'.$title,$item):''; 
  $out.=preg_match('/<Play>/',$item)   ?preg_replace('/<Play>.*<\/Play>/','',$item)      :'';
  $out.=preg_match('/<Default>/',$item)?preg_replace('/<Default>.*<\/Default>/','',$item):'';;                
  }
  $select_webradio=$out;
  $date=date("Ymd_Hi");
?>
<html><head>
<script>function $(e){return document.getElementById(e);}</script>
<style>
body,input,option,select,td {font:14px verdana;}
select,input {width:300px}
</style>
</head>
<body>
<form name="f" method="get" action="sr_web.php">
<input type="hidden" name="action" id="action" value=""/>
<table><tr><td>URL des Streams:</td><td><?= $select_webradio ?></td></tr>
<tr><td>Aufzeichnungsdauer in Sekunden:</td><td><input type="text" name="duration" size="30" value="(unendlich)" /></td></tr>
<tr><td>Speicherpfad:</td><td><input type="text" name="path" size="30" value="/volume1/public/incoming/sr/" /></td></tr>
<tr><td></td><td><input type="button" value="aktivieren" onclick="$('action').value='go';f.submit()"></td></tr></table>
</form></body>
<?php } ?>
Deine PHP angepasst im Dateinamen auf sr_web.php, Speicherpfad und Pfad für den Stationsspeicher (stations.asx).

stations.asx

Rich (BBCode):
<ASX version = "3.0">

<Entry>
        <TITLE>DI.fm Vocal Trance</TITLE>
        <Ref href = "http://scfire-dtc-aa07.stream.aol.com:80/stream/1065" />
</Entry> 


</ASX>
application.cfg
Rich (BBCode):
text = Streamripper
description = ermöglicht das aufnehmen von Webradio
icon_16 = tree_audiostation.png
icon_32 = tree_audiostation_32.png
type = embedded
path = /webman/3rdparty/sr/sr_web.php
Der Aufruf der sr_web.php funktioniert als 3rd-PartyApp genauso wie über den direkten Pfad https://<DS>:5001/webman/3rdparty/sr/sr_web.php

Bis zum Aufruf der PHP-Seite als Oberfläche funktioniert es, die stations.asx wird korrekt ausgewertet und der Speicherpfad richtig dargestellt. Beim aktivieren wird die Zeile

Rich (BBCode):
streamripper http://scfire-dtc-aa07.stream.aol.com:80/stream/1065 -s -d /volume1/public/incoming/sr/ --codeset-filesys=UTF-8 --codeset-id3=UTF-8 --codeset-metadata=UTF-8
im Browser ausgegeben, es startet aber nichts. Gebe ich diesen Befehl in ash ein startet die Aufnahme.

Ich hoffe du kannst mir helfen das zum laufen zu bringen.
 

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Geht denn der exec() in deinem PHP prinzipiell? Also so was wie:
PHP:
<?php exec('ps',$out); var_dump($out); ?>

Itari
 

coolhot

Benutzer
Mitglied seit
01. Mrz 2009
Beiträge
926
Punkte für Reaktionen
0
Punkte
0
Ja der exec funktioniert, ich habs mit ps und mit cat /proc/cpuinfo in einer neuen PHP-Datei getestet. Beides bringt eine plausible Bildschirmausgabe.
 

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Ergänze doch den Aufruf vom streamripper im Skript mit dem dazugehörigen Pfad ...

Itari
 

coolhot

Benutzer
Mitglied seit
01. Mrz 2009
Beiträge
926
Punkte für Reaktionen
0
Punkte
0
Jetzt arbeitet streamripper - Danke. Warum muss ich den Pfad angeben? /opt/bin ist im Pfad eingetragen in /root/.profile und /etc/profile.

Ist es normal das die aufrufende Seite endlos leer stehen bleibt mit "Laden..." im Tab vom FF?
 

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Jetzt arbeitet streamripper - Danke. Warum muss ich den Pfad angeben? /opt/bin ist im Pfad eingetragen in /root/.profile und /etc/profile.

Das gilt für dich, aber nicht für den Apache, der ja dein Skript ausführt. Dieser muss zusätzllich in der httpd.conf die Pfade angesagt bekommen ... aus Gründen der Sicherheit, damit niemand auf alles in deinem System von außen herumturnen kann.

Ja und das andere ist glaube ich normal so.

Itari
 

coolhot

Benutzer
Mitglied seit
01. Mrz 2009
Beiträge
926
Punkte für Reaktionen
0
Punkte
0
Das gilt für dich, aber nicht für den Apache, der ja dein Skript ausführt. Dieser muss zusätzllich in der httpd.conf die Pfade angesagt bekommen.

Ok, klingt logisch - hätte ich drauf kommen können das der Apache seinen eigenen Pfad braucht.

Vielen Dank für die schnelle Hilfe.
 

coolhot

Benutzer
Mitglied seit
01. Mrz 2009
Beiträge
926
Punkte für Reaktionen
0
Punkte
0
Hast du vielleicht noch eine Idee wie Umlaute in Dateinamen und ID3-Tags richtig berücksichtigt werden können? Bislang wird ein Umlaut umgesetzt zu - oder fehlt ganz. Meine Experimente mit den --codeset-Schaltern waren wenig erfolgreich.

Mit =ISO-8859-15 wie hier angegeben oder =ISO-8859-1 wollte sr nicht arbeiten.
 

itari

Benutzer
Mitglied seit
15. Mai 2008
Beiträge
21.900
Punkte für Reaktionen
14
Punkte
0
Hast du vielleicht noch eine Idee wie Umlaute in Dateinamen und ID3-Tags richtig berücksichtigt werden können? Bislang wird ein Umlaut umgesetzt zu - oder fehlt ganz. Meine Experimente mit den --codeset-Schaltern waren wenig erfolgreich.

Mit =ISO-8859-15 wie hier angegeben oder =ISO-8859-1 wollte sr nicht arbeiten.

Schau mal in die ersten Skripte rein, da hab ich so eine Konvertierfunktion drinne. Vielleicht lässt sich damit schon einiges anstellen. Da das mit dem Umlauten eine Neverending-Story ist, hab ich mir angewöhnt, keine mehr zu verwenden und alles auf Englisch zu schreiben; es ist mir einfach zu blöd geworden, mich immer stundenlang mit Zeichensätzen auseinander zu setzen ...

Itari
 

coolhot

Benutzer
Mitglied seit
01. Mrz 2009
Beiträge
926
Punkte für Reaktionen
0
Punkte
0
Ich möchte eigentlich eine nachträgliche Konvertierung vermeiden und gleich richtig aufnehmen - dafür gibt´s ja die --codeset Schalter. Leider funktionieren sie nicht wie erwartet oder ich stell mich zu blöd an :eek:.

Grds. gebe ich dir bzgl. Umlauten völlig recht - sie nerven einfach nur. Ich würde auch gern komplett drauf verzichten. Sie machen in der IT dauernd irgendwo Ärger (schön wieder ein Wort mit Umlaut).

Leider kann ich die Austrahlung der Dateinamen/ID2-Tags beim Webradio nicht
beeinflussen.
 

sierracharlie

Benutzer
Mitglied seit
20. Jun 2011
Beiträge
5
Punkte für Reaktionen
0
Punkte
0
Hallo Coolhot, Hallo Itari!

Tolle Idee habt ihr da gehabt, also habe ich es auch mal Versucht den Streamripper in DSM einzubinden. Leider bekomme ich das Script nicht eingebunden. Es kommt nur die Standard Fehlerseite von Synology "Seite nicht gefunden". Hat einer von euch evtl. eine Idee woran es liegen kann?

Könnte es sein das ich unter DSM3.1 etwas zusätzlich beachten machen muss?

Danke, Gruß SC
 
Zuletzt bearbeitet:
Status
Für weitere Antworten geschlossen.
 

Kaffeautomat

Wenn du das Forum hilfreich findest oder uns unterstützen möchtest, dann gib uns doch einfach einen Kaffee aus.

Als Dankeschön schalten wir deinen Account werbefrei.

:coffee:

Hier gehts zum Kaffeeautomat