4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-19 15:37:09 +00:00

Implement existing playlist/media import into station setup, fix issues with deployment (to allow for production deployment).

This commit is contained in:
Buster Silver 2016-05-16 08:21:51 -05:00
parent df6910158b
commit d5840467bf
17 changed files with 159 additions and 380 deletions

View File

@ -89,6 +89,8 @@ class LiquidSoap extends AdapterAbstract
'host = "localhost"',
'port = '.$icecast_port,
'password = "'.$icecast_source_pw.'"',
'name = "'.str_replace('"', '\'', $this->station->name).'"',
'description = "'.str_replace('"', '\'', $this->station->description).'"',
'mount = "radio.mp3"',
'radio', // Required
];

View File

@ -114,6 +114,10 @@ class Manager
public static function initSync($script_timeout = 60)
{
// Immediately halt if setup is not complete.
if (Settings::getSetting('setup_complete', 0) == 0)
die('Setup not complete; halting synchronized task.');
set_time_limit($script_timeout);
ini_set('memory_limit', '256M');

View File

@ -3,6 +3,7 @@ namespace App\Sync;
use Entity\Station;
use Entity\StationMedia;
use Entity\StationPlaylist;
class Media
{
@ -10,10 +11,10 @@ class Media
{
$stations = Station::fetchAll();
foreach($stations as $station)
self::processStation($station);
self::importMusic($station);
}
public static function processStation(Station $station)
public static function importMusic(Station $station)
{
$base_dir = $station->getRadioMediaDir();
if (empty($base_dir))
@ -69,6 +70,70 @@ class Media
$em->flush();
}
public static function importPlaylists(Station $station)
{
$base_dir = $station->getRadioPlaylistsDir();
if (empty($base_dir))
return false;
// Create a lookup cache of all valid imported media.
$media_lookup = array();
foreach($station->media as $media)
{
$media_path = $media->getFullPath();
$media_hash = md5($media_path);
$media_lookup[$media_hash] = $media;
}
// Iterate through playlists.
$di = \Phalcon\Di::getDefault();
$em = $di->get('em');
$playlist_files_raw = self::globDirectory($base_dir.'/*.{m3u,pls}', \GLOB_BRACE);
foreach($playlist_files_raw as $playlist_file_path)
{
// Create new StationPlaylist record.
$record = new StationPlaylist;
$record->station = $station;
$record->weight = 5;
$path_parts = pathinfo($playlist_file_path);
$playlist_name = str_replace('playlist_', '', $path_parts['filename']);
$record->name = $playlist_name;
$playlist_file = file_get_contents($playlist_file_path);
$playlist_lines = explode("\n", $playlist_file);
$em->persist($record);
foreach($playlist_lines as $line_raw)
{
$line = trim($line_raw);
if (substr($line, 0, 1) == '#' || empty($line))
continue;
if (file_exists($line))
{
$line_hash = md5($line);
if (isset($media_lookup[$line_hash]))
{
$media_record = $media_lookup[$line_hash];
$media_record->playlists->add($record);
$record->media->add($media_record);
$em->persist($media_record);
}
}
}
@unlink($playlist_file_path);
}
$em->flush();
}
public static function globDirectory($pattern, $flags = 0)
{
$files = (array)glob($pattern, $flags);

View File

@ -1,355 +0,0 @@
<?php
namespace PVL;
use \Entity\Station;
use \Entity\StationMedia;
use \Entity\StationRequest;
use \Entity\Song;
class CentovaCast
{
public static function isStationSupported(Station $station)
{
if (APP_APPLICATION_ENV !== 'production')
return false;
if (!$station->requests_enabled)
return FALSE;
$account_username = trim($station->requests_ccast_username);
if (empty($account_username))
return FALSE;
return TRUE;
}
public static function getStationID(Station $station)
{
if (!self::isStationSupported($station))
return NULL;
$db = self::getDatabase();
$account_username = trim($station->requests_ccast_username);
$id_raw = $db->fetchAssoc('SELECT id FROM accounts WHERE username = ?', array($account_username));
if ($id_raw)
return (int)$id_raw['id'];
else
return NULL;
}
public static function request(Station $station, $track_id)
{
if (APP_APPLICATION_ENV !== 'production')
return false;
$db = self::getDatabase();
$em = self::getEntityManager();
$settings = self::getSettings();
// Forbid web crawlers from using this feature.
if (\App\Utilities::isCrawler())
throw new \App\Exception('Search engine crawlers are not permitted to use this feature.');
// Verify that the station supports CentovaCast requests.
$station_id = self::getStationID($station);
if (!$station_id)
throw new \App\Exception('This radio station is not capable of handling requests at this time.');
// Verify that Track ID exists with station.
$media_item = StationMedia::getRepository()->findOneBy(array('id' => $track_id, 'station_id' => $station->id));
if (!($media_item instanceof StationMedia))
throw new \App\Exception('The song ID you specified could not be found in the station playlist.');
// Check the most recent song history.
try
{
$last_play_time = $em->createQuery('SELECT sh.timestamp FROM Entity\SongHistory sh WHERE sh.song_id = :song_id AND sh.station_id = :station_id ORDER BY sh.timestamp DESC')
->setParameter('song_id', $media_item->song_id)
->setParameter('station_id', $station->id)
->setMaxResults(1)
->getSingleScalarResult();
}
catch(\Exception $e)
{
$last_play_time = 0;
}
if ($last_play_time && $last_play_time > (time() - 60*30))
throw new \App\Exception('This song has been played too recently on the station.');
// Get or create a "requests" playlist for the station.
$request_playlist_raw = $db->fetchAssoc('SELECT p.id FROM playlists AS p WHERE p.type = ? AND p.accountid = ?', array('request', $station_id));
if ($request_playlist_raw)
{
$playlist_id = $request_playlist_raw['id'];
}
else
{
$new_playlist = array(
'title' => 'Automated Song Requests',
'type' => 'request',
'scheduled_repeat' => 'never',
'scheduled_monthdays' => 'date',
'interval_type' => 'songs',
'interval_length' => '0',
'general_weight' => '0',
'status' => 'disabled',
'general_order' => 'random',
'interval_style' => 'playall',
'stateid' => '0',
'accountid' => $station_id,
'scheduled_interruptible' => '0',
'scheduled_duration' => '0',
'scheduled_style' => 'sequential',
'general_starttime' => '00:00:00',
'general_endtime' => '00:00:00',
'track_interruptible' => '0',
);
$db->insert('playlists', $new_playlist);
$playlist_id = $db->lastInsertId('playlists');
}
// Check for an existing request from this user.
$user_ip = $_SERVER['REMOTE_ADDR'];
$existing_request = $db->fetchAll('SELECT ptr.* FROM playlist_tracks_requests AS ptr WHERE ptr.playlistid = ? AND ptr.senderip = ?', array($playlist_id, $user_ip));
if (count($existing_request) > 0)
throw new \App\Exception('You already have a pending request with this station! Please try again later.');
// Check for any request (on any station) within 5 minutes.
$recent_threshold = time()-(60*5);
$recent_requests = $em->createQuery('SELECT sr FROM Entity\StationRequest sr WHERE sr.ip = :user_ip AND sr.timestamp >= :threshold')
->setParameter('user_ip', $user_ip)
->setParameter('threshold', $recent_threshold)
->getArrayResult();
if (count($recent_requests) > 0)
throw new \App\Exception('You have submitted a request too recently! Please wait a while before submitting another one.');
// Enable the "Automated Song Requests" playlist.
$db->update('playlists', array('status' => 'enabled'), array('id' => $playlist_id));
$requesttime = new \DateTime('NOW');
$requesttime->setTimezone(new \DateTimeZone($settings['timezone']));
// Create a new request if all other checks pass.
$new_request = array(
'playlistid' => $playlist_id,
'trackid' => $track_id,
'requesttime' => $requesttime->format('Y-m-d h:i:s'),
'sendername' => 'Ponyville Live!',
'senderemail' => 'requests@ponyvillelive.com',
'dedication' => '',
'senderip' => $user_ip,
);
$db->insert('playlist_tracks_requests', $new_request);
$request_id = $db->lastInsertId('playlist_tracks_requests');
$media_item->logRequest();
$media_item->save();
return $request_id;
}
// Routine synchronization of CentovaCast settings
public static function sync()
{
if (APP_APPLICATION_ENV !== 'production')
return false;
$db = self::getDatabase();
$em = self::getEntityManager();
$settings = self::getSettings();
// Force correct account settings (enable global unified request system).
$account_values = array(
'allowrequests' => '1',
'autoqueuerequests' => '1',
'requestprobability' => '50',
'requestdelay' => '0',
'emailunknownrequests' => '0',
);
$db->update('accounts', $account_values, array('expectedstate' => 'up'));
// Clear out old logs.
$threshold = strtotime('-1 month');
$threshold_date = date('Y-m-d', $threshold).' 00:00:00';
$db->executeQuery('DELETE FROM playbackstats_tracks WHERE endtime <= ?', array($threshold_date));
$db->executeQuery('DELETE FROM visitorstats_sessions WHERE endtime <= ?', array($threshold_date));
// Delete old requests still listed as pending.
$requesttime = new \DateTime('NOW');
$requesttime->modify('-3 hours');
$requesttime->setTimezone(new \DateTimeZone($settings['timezone']));
$threshold_requests = $requesttime->format('Y-m-d h:i:s');
$db->executeQuery('DELETE FROM playlist_tracks_requests WHERE requesttime <= ?', array($threshold_requests));
// Force playlist enabling for existing pending requests.
$request_playlists_raw = $db->fetchAll('SELECT DISTINCT ptr.playlistid AS pid FROM playlist_tracks_requests AS ptr');
foreach($request_playlists_raw as $pl)
{
$pl_id = $pl['pid'];
$db->update('playlists', array('status' => 'enabled'), array('id' => $pl_id));
}
// Preload all station media locally.
$stations = $em->createQuery('SELECT s FROM Entity\Station s WHERE s.requests_enabled = 1')->execute();
foreach($stations as $station)
{
$account_id = self::getStationID($station);
if (!$account_id)
continue;
// Clear existing items.
$existing_ids_raw = $em->createQuery('SELECT sm FROM Entity\StationMedia sm WHERE sm.station_id = :station_id')
->setParameter('station_id', $station->id)
->execute();
$existing_records = array();
foreach($existing_ids_raw as $row)
$existing_records[$row['id']] = $row;
// Find all tracks in active playlists.
$new_records_raw = self::fetchTracks($station);
$records_by_hash = array();
foreach($new_records_raw as $track_info)
{
if ($track_info['length'] < 60)
continue;
if ($track_info['playlist_status'] !== 'enabled')
continue;
$row = array(
'id' => $track_info['id'],
'title' => $track_info['title'],
'artist' => $track_info['artist'],
'album' => $track_info['album'],
'length' => $track_info['length'],
);
$song_hash = Song::getSongHash(array(
'text' => $row['artist'].' - '.$row['title'],
'artist' => $row['artist'],
'title' => $row['title'],
));
$records_by_hash[$song_hash] = $row;
}
$new_records = array();
foreach($records_by_hash as $row)
{
$new_records[$row['id']] = $row;
}
// Reconcile differences.
$existing_guids = array_keys($existing_records);
$new_guids = array_keys($new_records);
$guids_to_delete = array_diff($existing_guids, $new_guids);
if ($guids_to_delete)
{
foreach($guids_to_delete as $guid)
{
$record = $existing_records[$guid];
$em->remove($record);
}
}
$guids_to_add = array_diff($new_guids, $existing_guids);
if ($guids_to_add)
{
foreach($guids_to_add as $guid)
{
$record = new StationMedia;
$record->station = $station;
$record->fromArray($new_records[$guid]);
$em->persist($record);
}
}
$em->flush();
}
return true;
}
public static function fetchTracks(Station $station)
{
$account_id = self::getStationID($station);
if ($account_id === null)
return null;
$db = self::getDatabase();
$tracks = $db->fetchAll('SELECT DISTINCT t.id, t.title, tartist.name AS artist, talbum.name AS album, t.pathname, t.bitrate, t.length, p.title AS playlist_name, p.status AS playlist_status
FROM tracks AS t
INNER JOIN track_artists AS tartist ON t.artistid = tartist.id
INNER JOIN track_albums AS talbum ON t.albumid = talbum.id
INNER JOIN playlist_tracks AS pt ON pt.trackid = t.id
INNER JOIN playlists AS p ON pt.playlistid = p.id
WHERE p.accountid = ?
ORDER BY playlist_status ASC, playlist_name ASC, artist ASC, t.title', array($account_id));
return $tracks;
}
public static function getEntityManager()
{
$di = \Phalcon\Di::getDefault();
return $di->get('em');
}
public static function getDatabase()
{
static $db;
if (!$db)
{
$settings = self::getSettings();
// CentovaCast isn't configured in apis.conf.php
if (empty($settings['db_pass']))
throw new \App\Exception('CentovaCast is not set up.');
$config = new \Doctrine\DBAL\Configuration;
$connectionParams = array(
'host' => $settings['host'],
'dbname' => $settings['db_name'],
'user' => $settings['db_user'],
'password' => $settings['db_pass'],
'driver' => 'pdo_mysql',
);
$db = \Doctrine\DBAL\DriverManager::getConnection($connectionParams, $config);
}
return $db;
}
public static function getSettings()
{
static $settings;
if (!$settings)
{
$di = \Phalcon\Di::getDefault();
$config = $di->get('config');
$settings = $config->apis->centovacast->toArray();
}
return $settings;
}
}

View File

@ -133,6 +133,7 @@ class Station extends \App\Doctrine\Entity
/**
* @OneToMany(targetEntity="StationPlaylist", mappedBy="station")
* @OrderBy({"weight" = "DESC"})
*/
protected $playlists;

View File

@ -37,10 +37,10 @@ class StationMedia extends \App\Doctrine\Entity
/** @Column(name="song_id", type="string", length=50, nullable=true) */
protected $song_id;
/** @Column(name="title", type="string", length=200) */
/** @Column(name="title", type="string", length=200, nullable=true) */
protected $title;
/** @Column(name="artist", type="string", length=200) */
/** @Column(name="artist", type="string", length=200, nullable=true) */
protected $artist;
/** @Column(name="album", type="string", length=200, nullable=true) */
@ -64,6 +64,12 @@ class StationMedia extends \App\Doctrine\Entity
/** @Column(name="path", type="string", length=255, nullable=true) */
protected $path;
public function getFullPath()
{
$media_base_dir = $this->station->getRadioMediaDir();
return $media_base_dir.'/'.$this->path;
}
/** @Column(name="mtime", type="integer", nullable=true) */
protected $mtime;

View File

@ -111,9 +111,18 @@ class SetupController extends BaseController
// Generate station ID.
$station->save();
// Scan directory for any existing files.
set_time_limit(600);
\App\Sync\Media::importMusic($station);
$this->em->refresh($station);
\App\Sync\Media::importPlaylists($station);
$this->em->refresh($station);
// Load configuration from adapter to pull source and admin PWs.
$frontend_adapter = $station->getFrontendAdapter();
$frontend_adapter->read();
$frontend_adapter->restart();
// Write an empty placeholder configuration.
$backend_adapter = $station->getBackendAdapter();

View File

@ -4,4 +4,8 @@ $title = 'Set Up Your First Station';
<p>To continue the setup process, enter the basic details of your first station below. Some technical details will automatically be retrieved from the system after you complete this step.</p>
<?=$form->render() ?>
<?=$form->render() ?>
<div class="alert alert-info m-b-0 m-t-25">
Note: If you are not starting with an empty station directory, it may take a while to process any existing MP3 files or playlists that are in this station's directory.
</div>

View File

@ -12,21 +12,16 @@ return array(
'weight' => array('radio', array(
'label' => 'Playlist Weight',
'description' => 'How often the playlist\'s songs will be played. 1 is the most infrequent, 10 is the most frequent.',
'description' => 'How often the playlist\'s songs will be played. 1 is the most infrequent, 5 is the most frequent.',
'default' => 5,
'required' => true,
'class' => 'inline',
'options' => array(
1 => '1 - Lowest',
2 => '2',
3 => '3',
3 => '3 - Default',
4 => '4',
5 => '5 - Default',
6 => '6',
7 => '7',
8 => '8',
9 => '9',
10 => '10 - Highest',
5 => '5 - Highest',
),
)),

View File

@ -108,10 +108,17 @@ class FilesController extends BaseController
$media = ['name' => 'File Not Processed', 'playlists' => []];
$stat = stat($i);
$max_length = 60;
$shortname = basename($i);
if (strlen($shortname) > $max_length)
$shortname = substr($shortname, 0, $max_length-15).'...'.substr($shortname, -12);
$result[] = array(
'mtime' => $stat['mtime'],
'size' => $stat['size'],
'name' => basename($i),
'text' => $shortname,
'path' => $short,
'is_dir' => is_dir($i),
'media' => $media,

View File

@ -284,7 +284,8 @@ $(function() {
var $link = $('<a class="name" />')
.attr('href', data.is_dir ? '#' + data.path : '<?=$this->url->routeFromHere(['action' => 'download']) ?>?file='+encodeURIComponent(data.path))
.text(data.name);
.attr('title', data.name)
.text(data.text);
var $html = $('<tr />')
.addClass(data.is_dir ? 'is_dir' : 'is_file')

View File

@ -8,8 +8,8 @@ export tmp_base=$app_base/www_tmp
export app_env="production"
cd $util_base
chmod a+x ./install_radio.sh
chmod a+x ./install_app.sh
chmod a+x ./util/install_radio.sh
chmod a+x ./util/install_app.sh
./install_radio.sh
./install_app.sh
./util/install_radio.sh
./util/install_app.sh

View File

@ -95,12 +95,11 @@ echo "Customizing nginx..."
service nginx stop
mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
cp $util_base/vagrant_nginx /etc/nginx/nginx.conf
# mv /etc/nginx/nginx.conf /etc/nginx/nginx.conf.bak
cp $util_base/vagrant_nginx_site /etc/nginx/sites-enabled/azuracast
sedeasy "AZURABASEDIR" $app_base /etc/nginx/sites-enabled/azuracast
sedeasy "AZURABASEDIR" $app_base /etc/nginx/nginx.conf
unlink /etc/nginx/sites-enabled/
unlink /etc/nginx/sites-enabled/default
# Set up MySQL server.
echo "Customizing MySQL..."

39
util/vagrant_nginx_site Normal file
View File

@ -0,0 +1,39 @@
server {
listen 80 default_server;
root AZURABASEDIR/www/web;
index index.php;
server_name localhost;
access_log AZURABASEDIR/www_tmp/access.log;
error_log AZURABASEDIR/www_tmp/error.log;
location / {
try_files $uri @clean_url;
}
location @clean_url {
rewrite ^(.*)$ /index.php last;
}
location ~ \.php$ {
fastcgi_hide_header Cache-Control;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
# NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
fastcgi_param SCRIPT_FILENAME AZURABASEDIR/www/web$fastcgi_script_name;
include fastcgi_params;
fastcgi_read_timeout 1800;
}
# deny access to .htaccess files, if Apache's document root
# concurs with nginx's one
#
location ~ /\.ht {
deny all;
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -27,6 +27,8 @@
}
&.first {
white-space: normal;
i {
float: left;
width: 30px;