Skip to content

Instantly share code, notes, and snippets.

@sanderdewijs
Created March 22, 2020 16:34
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save sanderdewijs/5eb38e4f4ce7fca631fb44a182e2b97a to your computer and use it in GitHub Desktop.
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.
<?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;
}
}
@Sebouier
Copy link

Sebouier commented Dec 1, 2020

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

@milanjoepie
Copy link

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

@sanderdewijs
Copy link
Author

sanderdewijs commented Jan 26, 2022

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