Created
March 22, 2020 16:34
-
-
Save sanderdewijs/5eb38e4f4ce7fca631fb44a182e2b97a to your computer and use it in GitHub Desktop.
This is an idea to create a playlist from an Icecast server storing the songdata and cover image into a database.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Class IceCastPlaylist | |
* See https://stackoverflow.com/questions/60502076/how-to-get-icecast-server-songs-history/60798774#60798774 for context | |
* | |
* This class can be used as follows: | |
* global $wpdb | |
* $playlistClass = new IceCastPlaylist($wpdb); | |
* $playlistClass->fetchSongInfo(); | |
* $playlist = $songInfoClass->getPlaylist(); | |
* header('Content-Type: application/json;charset=utf-8'); | |
* echo $playlist; | |
* die(); | |
*/ | |
class IceCastPlaylist { | |
/** | |
* @var wpdb | |
*/ | |
protected $db; | |
/** | |
* @var string | |
*/ | |
protected $table = '[replace with playlist table]'; | |
/** | |
* Replace this with your server URL | |
* @var string | |
*/ | |
protected $songDataUrl = 'https://serveraddress:portnumber/stream.xspf'; | |
/** | |
* @var string | |
*/ | |
protected $albumCoverUrl = 'https://itunes.apple.com/search?entity=musicTrack&term='; | |
/** | |
* Replace this with your storage path | |
* @var string | |
*/ | |
protected $albumCoverPath = '[absolute_path_to_cover_images_folder e.g /media/covers/]'; | |
/** | |
* Replace with your site URL | |
* Most of the time you will want to set the URL to the site dynamically, | |
* so we do this in the constructor | |
*/ | |
protected $siteUrl = ''; | |
/** | |
* Replace this with the desired length of your playlist | |
* @var int | |
*/ | |
protected $playlistLength = 20; | |
/** | |
* IceCastPlaylist constructor. | |
* In this case, I used WordPress wpdb, but it can be replaced with another DB instance | |
* When using something else than wpdb, the query functions will need to be adjusted | |
* | |
* @param $db | |
*/ | |
public function __construct(wpdb $db) { | |
$this->db = $db; | |
// If you need to set the site URL via a function, you can set it here in the constructor | |
$this->siteUrl = get_site_url() . '/wp-content/media/covers/'; | |
} | |
public function getPlaylist() { | |
$result = $this->db->get_results("SELECT * FROM {$this->table} as songs ORDER BY `songs`.`id` DESC LIMIT {$this->playlistLength}"); | |
return json_encode($result); | |
} | |
/** | |
* [ | |
* 'playedat' => 'H:i', | |
* 'title' => 'title', | |
* 'artist' => 'bla', | |
* 'coverImage' => 'linkToCoverArt', | |
* 'created_at' => 'Y-m-d', | |
* ] | |
* @param array $entry | |
*/ | |
public function storeEntry(array $entry) { | |
$this->db->insert($this->table, $entry); | |
} | |
/** | |
* @return mixed | |
* @throws Exception | |
*/ | |
public function fetchSongInfo() { | |
$rawData = file_get_contents($this->songDataUrl); | |
$now = new DateTime('now'); | |
if(!$rawData) { | |
throw new Exception('Error retrieving song data'); | |
} | |
$xml = new SimpleXMLElement($rawData); | |
$titleArtist = preg_split("/\s-\s/", (string) $xml->trackList->track->title); | |
if($this->isNewSong($titleArtist[1])) { | |
$stream['playedAt'] = $now->format('H:i'); | |
$stream['title'] = $titleArtist[1]; | |
$stream['artist'] = $titleArtist[0]; | |
$stream['coverImage'] = $this->getCoverArt($xml->trackList->track->title); | |
$stream['created_at'] = $now->format('Y-m-d H:i:s'); | |
$this->storeEntry($stream); | |
} | |
} | |
/** | |
* Use in your daily cleanup cron | |
*/ | |
public function dailyCleanup() { | |
$this->db->query("TRUNCATE TABLE {$this->table}"); | |
} | |
/** | |
* @param string $title | |
* | |
* @return bool | |
*/ | |
private function isNewSong(string $title) { | |
$result = $this->db->get_results("SELECT * FROM {$this->table} as songs ORDER BY `songs`.`id` DESC LIMIT 1"); | |
if(empty($result)) { | |
return true; | |
} | |
return $result[0]->title !== $title; | |
} | |
/** | |
* Take the raw 'songtitle - artist' and try to find cover art | |
* @param string $title | |
* | |
* @return string|null | |
*/ | |
private function getCoverArt(string $title) { | |
$itunesData = wp_remote_get($this->albumCoverUrl . urlencode($title), array( | |
'timeout' => 45, | |
'redirection' => 5, | |
'httpversion' => '1.0', | |
'blocking' => true, | |
'headers' => array( | |
'accept-encoding' => 'gzip, deflate, br', | |
'pragma' => 'no-cache', | |
'accept-language' => 'en-US,en;q=0.8', | |
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36', | |
'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8' | |
), | |
'cookies' => array(), | |
)); | |
$songInfo = json_decode(trim(wp_remote_retrieve_body($itunesData))); | |
$imageHash = ''; | |
if(isset($songInfo->results[0])) { | |
$coverImage = str_replace('100x100bb.jpg', '', $songInfo->results[0]->artworkUrl100); | |
$imageHash = md5($title) . '.jpg'; | |
if(!file_exists($this->albumCoverPath . $imageHash) && $coverImage !== '') { | |
$img = file_get_contents($coverImage . '200x200bb.jpg'); | |
file_put_contents($this->albumCoverPath . $imageHash, $img); | |
} | |
} | |
return (empty($imageHash)) ? null : $this->siteUrl . $imageHash; | |
} | |
} |
Hello Sander,
Thank you for the script! I was just wondering if you could help me, I am developing a radio streaming website on wordpress. And I am unable to figure out how to implement this script in my website so that it will show a short playlist history. Could you perhaps enlighten me on what changes I have to make to the script?
Thanks in advance.
Milan
For a working example, I have this class I use in a Laravel application. It should work with the following steps:
- add the classes in the example to your Laravel application
- Install WideImage if you do not have it already (http://wideimage.sourceforge.net/)
- add the playlist:update command to /Console/Kernel.php
- fill in the stream URL and portnumber
- make sure the directory exists to store the cover images
<?php
namespace App\Services;
use App\PlaylistItem;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Storage;
use SimpleXMLElement;
use WideImage\WideImage;
/**
* Class PlaylistService
* @package App\Services
*/
class PlaylistService {
/**
* Replace this with your server URL
* @var string
*/
protected $songDataUrl = 'https://[address]:[postnumber]/stream.xspf';
/**
* @var string
*/
protected $albumCoverUrl = 'https://itunes.apple.com/search?entity=musicTrack&term=';
/**
* Replace with your site URL
*/
protected $siteUrl = '';
/**
* @var Client
*/
protected $client;
/**
* @var string
*/
protected $albumCoverArt = 'app/public/covers/';
/**
* IceCastPlaylist constructor
*
* @param Client
*/
public function __construct(Client $client) {
$this->client = $client;
$this->siteUrl = config('app.url') . Storage::url('public/covers/');
}
/**
* @return PlaylistItem[]|\Illuminate\Database\Eloquent\Builder[]|\Illuminate\Database\Eloquent\Collection|\Illuminate\Database\Query\Builder[]|\Illuminate\Support\Collection
*/
public function getPlaylist() {
return PlaylistItem::query()
->orderByDesc('id')
->take(24)
->get()->map(function (PlaylistItem $item) {
return [
'id' => $item->id,
'playedat' => $item->played_at,
'title' => $item->title,
'artist' => $item->artist,
'coverImage' => $item->cover_image,
'created_at' => carbon($item->created_at)->format('Y-m-d H:i:s'),
];
});
}
/**
* @return mixed
* @throws ClientException
*/
public function fetchSongInfo() {
$songData = Http::get($this->songDataUrl);
$xml = new SimpleXMLElement($songData->body());
$titleArtist = preg_split("/\s-\s/", (string)$xml->trackList->track->title);
if (isset($titleArtist[1]) && $this->isNewSong($titleArtist[1])) {
$cover = $this->getCoverArt($xml->trackList->track->title);
if ($cover) {
$item = new PlaylistItem([
'played_at' => carbon()->format('H:i'),
'title' => $titleArtist[1],
'artist' => $titleArtist[0],
'cover_image' => $cover,
]);
$item->save();
}
}
}
/**
* Use in your daily cleanup cron
*/
public function dailyCleanup() {
\DB::table('playlist_items')->truncate();
}
/**
* @param string $title
*
* @return bool
*/
private function isNewSong(string $title) {
$result = PlaylistItem::query()->orderByDesc('id')->first();
if (empty($result)) {
return true;
}
return $result->title !== $title;
}
/**
* @param string $title
*
* @return string|null
*/
private function getCoverArt(string $title) {
$itunesData = Http::withHeaders([
'accept-encoding' => 'gzip, deflate, br',
'pragma' => 'no-cache',
'accept-language' => 'en-US,en;q=0.8',
'user-agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3112.101 Safari/537.36',
'accept' => 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,image/apng,*/*;q=0.8'
])
->get($this->albumCoverUrl . urlencode($title));
if ($itunesData->failed()) {
return false;
}
$songInfo = $itunesData->object();
if (isset($songInfo->results[0])) {
$coverImage = str_replace('100x100bb.jpg', '', $songInfo->results[0]->artworkUrl100);
$imageHash = md5($title) . '.jpg';
if ( ! Storage::exists(storage_path($this->albumCoverArt) . $imageHash) && $coverImage !== '') {
$img = WideImage::load($coverImage . '200x200bb.jpg');
$img->saveToFile(storage_path($this->albumCoverArt) . $imageHash);
}
return $this->siteUrl . $imageHash;
}
return null;
}
}
// Model for the PlaylistItem:
<?php
namespace App;
use DateTimeInterface;
use Illuminate\Database\Eloquent\Model;
class PlaylistItem extends Model {
protected $fillable = [
'played_at',
'title',
'artist',
'cover_image',
];
}
// Cronjob/Command to fetch new playlist items each minute (if there is a new song)
<?php
namespace App\Console\Commands;
use App\Services\PlaylistService;
use GuzzleHttp\Exception\ClientException;
use Illuminate\Console\Command;
/**
* Class UpdatePlaylist
* @package App\Console\Commands
*/
class UpdatePlaylist extends Command
{
/**
* The name and signature of the console command.
*
* @var string
*/
protected $signature = 'playlist:update';
/**
* The console command description.
*
* @var string
*/
protected $description = 'Checks for new somg info ans saves new entry in database';
public PlaylistService $playlist_service;
/**
* UpdatePlaylist constructor.
*
* @param PlaylistService $playlist_service
*/
public function __construct(PlaylistService $playlist_service)
{
parent::__construct();
$this->playlist_service = $playlist_service;
}
/**
* Execute the console command.
*
* @return int
*/
public function handle()
{
try {
$this->playlist_service->fetchSongInfo();
} catch (ClientException $e) {
// do nothing
}
return 0;
}
}
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi Sander,
Many thanks for this script! This is exactly what I was looking for my website (https://toutes-les-radios.fr), since many audio stream use Icecast2.
I'm not familiar with php class and I hope I won't struggle too much converting wp query into non wp query.
Best regards,
Sebastien