AzuraCast/app/src/Entity/Repository/StationMediaRepository.php

431 lines
15 KiB
PHP

<?php
namespace Entity\Repository;
use Doctrine\ORM\NoResultException;
use Entity;
class StationMediaRepository extends BaseRepository
{
/**
* @param Entity\Station $station
* @return array
*/
public function getRequestable(Entity\Station $station)
{
return $this->_em->createQuery('SELECT sm FROM ' . $this->_entityName . ' sm WHERE sm.station_id = :station_id ORDER BY sm.artist ASC, sm.title ASC')
->setParameter('station_id', $station->getId())
->getArrayResult();
}
/**
* @param Entity\Station $station
* @param $artist_name
* @return array
*/
public function getByArtist(Entity\Station $station, $artist_name)
{
return $this->_em->createQuery('SELECT sm FROM ' . $this->_entityName . ' sm WHERE sm.station_id = :station_id AND sm.artist LIKE :artist ORDER BY sm.title ASC')
->setParameter('station_id', $station->getId())
->setParameter('artist', $artist_name)
->getArrayResult();
}
/**
* @param Entity\Station $station
* @param $query
* @return array
*/
public function search(Entity\Station $station, $query)
{
$db = $this->_em->getConnection();
$table_name = $this->_em->getClassMetadata(__CLASS__)->getTableName();
$stmt = $db->executeQuery('SELECT sm.* FROM ' . $db->quoteIdentifier($table_name) . ' AS sm WHERE sm.station_id = ? AND CONCAT(sm.title, \' \', sm.artist, \' \', sm.album) LIKE ?',
[$station->getId(), '%' . addcslashes($query, "%_") . '%']);
return $stmt->fetchAll();
}
/**
* @param Entity\Station $station
* @param $path
* @return Entity\StationMedia
* @throws \Exception
*/
public function getOrCreate(Entity\Station $station, $path)
{
$short_path = ltrim(str_replace($station->getRadioMediaDir(), '', $path), '/');
$record = $this->findOneBy(['station_id' => $station->getId(), 'path' => $short_path]);
if (!($record instanceof Entity\StationMedia)) {
$record = new Entity\StationMedia($station, $short_path);
}
$song_info = $record->loadFromFile();
if (is_array($song_info)) {
/** @var SongRepository $song_repo */
$song_repo = $this->_em->getRepository(Entity\Song::class);
$record->setSong($song_repo->getOrCreate($song_info));
}
return $record;
}
/**
* Determine the next-playing song for this station based on its playlist rotation rules.
*
* @param Entity\Station $station
* @return Entity\SongHistory|null
*/
public function getNextSong(Entity\Station $station)
{
// Process requests first (if applicable)
if ($station->getEnableRequests()) {
$min_minutes = (int)$station->getRequestDelay();
$threshold_minutes = $min_minutes + mt_rand(0, $min_minutes);
$threshold = time() - ($threshold_minutes * 60);
// Look up all requests that have at least waited as long as the threshold.
$request = $this->_em->createQuery('SELECT sr, sm
FROM Entity\StationRequest sr JOIN sr.track sm
WHERE sr.played_at = 0 AND sr.station_id = :station_id AND sr.timestamp <= :threshold
ORDER BY sr.id ASC')
->setParameter('station_id', $station->getId())
->setParameter('threshold', $threshold)
->setMaxResults(1)
->getOneOrNullResult();
if ($request instanceof Entity\StationRequest) {
return $this->_playSongFromRequest($request);
}
}
// Pull all active, non-empty playlists and sort by type.
$playlists_by_type = [];
foreach($station->getPlaylists() as $playlist) {
/** @var Entity\StationPlaylist $playlist */
// Don't include empty playlists
if ($playlist->canBeCued()) {
$playlists_by_type[$playlist->getType()][$playlist->getId()] = $playlist;
}
}
// Pull all recent cued songs for easy referencing below.
$cued_song_history = $this->_em->createQuery('SELECT sh FROM Entity\SongHistory sh
WHERE sh.station_id = :station_id
AND (sh.timestamp_cued != 0 AND sh.timestamp_cued IS NOT NULL)
AND sh.timestamp_cued >= :threshold
ORDER BY sh.timestamp_cued DESC')
->setParameter('station_id', $station->getId())
->setParameter('threshold', time()-86399)
->getArrayResult();
// Once per day playlists
if (!empty($playlists_by_type['once_per_day'])) {
foreach ($playlists_by_type['once_per_day'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
if ($playlist->canPlayOnce()) {
// Check if already played
$relevant_song_history = array_slice($cued_song_history, 0, 15);
$was_played = false;
foreach($relevant_song_history as $sh_row) {
if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
}
// Once per X songs playlists
if (!empty($playlists_by_type['once_per_x_songs'])) {
foreach($playlists_by_type['once_per_x_songs'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$relevant_song_history = array_slice($cued_song_history, 0, $playlist->getPlayPerSongs());
$was_played = false;
foreach($relevant_song_history as $sh_row) {
if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
// Once per X minutes playlists
if (!empty($playlists_by_type['once_per_x_minutes'])) {
foreach($playlists_by_type['once_per_x_minutes'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
$threshold = time() - ($playlist->getPlayPerMinutes() * 60);
$was_played = false;
foreach($cued_song_history as $sh_row) {
if ($sh_row['timestamp_cued'] < $threshold) {
break;
} else if ($sh_row['playlist_id'] == $playlist->getId()) {
$was_played = true;
break;
}
}
if (!$was_played) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
reset($cued_song_history);
}
}
// Time-block scheduled playlists
if (!empty($playlists_by_type['scheduled'])) {
foreach ($playlists_by_type['scheduled'] as $playlist) {
/** @var Entity\StationPlaylist $playlist */
if ($playlist->canPlayScheduled()) {
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
}
}
// Default rotation playlists
if (!empty($playlists_by_type['default'])) {
$playlist_weights = [];
foreach($playlists_by_type['default'] as $playlist_id => $playlist) {
/** @var Entity\StationPlaylist $playlist */
$playlist_weights[$playlist_id] = $playlist->getWeight();
}
$rand = random_int(1, (int)array_sum($playlist_weights));
foreach ($playlist_weights as $playlist_id => $weight) {
$rand -= $weight;
if ($rand <= 0) {
$playlist = $playlists_by_type['default'][$playlist_id];
$sh = $this->_playSongFromPlaylist($playlist);
if ($sh) {
return $sh;
}
}
}
}
return null;
}
protected function _playSongFromRequest(Entity\StationRequest $request)
{
// Log in history
$sh = new Entity\SongHistory($request->getTrack()->getSong(), $request->getStation());
$sh->setRequest($request);
$sh->setMedia($request->getTrack());
$sh->setDuration($request->getTrack()->getCalculatedLength());
$sh->setTimestampCued(time());
$this->_em->persist($sh);
$request->setPlayedAt(time());
$this->_em->persist($request);
$this->_em->flush();
return $sh;
}
protected function _playSongFromPlaylist(Entity\StationPlaylist $playlist)
{
if ($playlist->getSource() === Entity\StationPlaylist::SOURCE_SONGS) {
if ($playlist->getOrder() === Entity\StationPlaylist::ORDER_SEQUENTIAL) {
$media_to_play = $this->_playSequentialSongFromPlaylist($playlist);
} else {
$media_to_play = $this->_playRandomSongFromPlaylist($playlist);
}
if ($media_to_play instanceof Entity\StationMedia) {
$spm = $media_to_play->getItemForPlaylist($playlist);
$spm->played();
$this->_em->persist($spm);
// Log in history
$sh = new Entity\SongHistory($media_to_play->getSong(), $playlist->getStation());
$sh->setPlaylist($playlist);
$sh->setMedia($media_to_play);
$sh->setDuration($media_to_play->getCalculatedLength());
$sh->setTimestampCued(time());
$this->_em->persist($sh);
$this->_em->flush();
return $sh;
}
} else {
return $playlist->getRemoteUrl();
}
return null;
}
protected function _playRandomSongFromPlaylist(Entity\StationPlaylist $playlist)
{
// Get some random songs from playlist.
$random_songs = $this->_em->createQuery('SELECT sm, spm, s, st FROM Entity\StationMedia sm
JOIN sm.song s
JOIN sm.station st
JOIN sm.playlist_items spm
JOIN spm.playlist sp
WHERE spm.playlist_id = :playlist_id
GROUP BY sm.id ORDER BY RAND()')
->setParameter('playlist_id', $playlist->getId())
->setMaxResults(15)
->execute();
/** @var bool Whether to use "last song ID played" or "genuine random shuffle" mode. */
$use_song_ids = true;
// Get all song IDs from the random songs.
$song_timestamps = [];
$songs_by_id = [];
foreach($random_songs as $media_row) {
/** @var Entity\StationMedia $media_row */
if ($media_row->getLength() == 0) {
$use_song_ids = false;
break;
} else {
$playlist_item = $media_row->getItemForPlaylist($playlist);
$song_timestamps[$media_row->getSong()->getId()] = $playlist_item->getLastPlayed();
$songs_by_id[$media_row->getSong()->getId()] = $media_row;
}
}
if ($use_song_ids) {
asort($song_timestamps);
reset($song_timestamps);
$id_to_play = key($song_timestamps);
$random_song = $songs_by_id[$id_to_play];
} else {
shuffle($random_songs);
$random_song = array_pop($random_songs);
}
return $random_song;
}
protected function _playSequentialSongFromPlaylist(Entity\StationPlaylist $playlist)
{
// Fetch the most recently played song
try {
/** @var Entity\StationPlaylistMedia $last_played_media */
$last_played_media = $this->_em->createQuery('SELECT spm FROM Entity\StationPlaylistMedia spm
WHERE spm.playlist_id = :playlist_id
ORDER BY spm.last_played DESC')
->setParameter('playlist_id', $playlist->getId())
->setMaxResults(1)
->getSingleResult();
} catch(NoResultException $e) {
return null;
}
$last_weight = (int)$last_played_media->getWeight();
// Try to find a song of greater weight. If none exists, start back with zero.
$next_song_query = $this->_em->createQuery('SELECT spm, sm, s, st FROM Entity\StationPlaylistMedia spm
JOIN spm.media sm
JOIN sm.song s
JOIN sm.station st
WHERE spm.weight >= :weight
ORDER BY spm.weight ASC')
->setMaxResults(1);
try {
$next_song = $next_song_query
->setParameter('weight', $last_weight+1)
->getSingleResult();
} catch(NoResultException $e) {
$next_song = $next_song_query
->setParameter('weight', 0)
->getSingleResult();
}
/** @var Entity\StationPlaylistMedia $next_song */
return $next_song->getMedia();
}
/**
* Retrieve a key-value representation of all custom metadata for the specified media.
*
* @param Entity\StationMedia $media
* @return array
*/
public function getCustomFields(Entity\StationMedia $media)
{
$metadata_raw = $this->_em->createQuery('SELECT e FROM Entity\StationMediaCustomField e WHERE e.media_id = :media_id')
->setParameter('media_id', $media->getId())
->getArrayResult();
$result = [];
foreach($metadata_raw as $row) {
$result[$row['field_id']] = $row['value'];
}
return $result;
}
/**
* Set the custom metadata for a specified station based on a provided key-value array.
*
* @param Entity\StationMedia $media
* @param array $custom_fields
*/
public function setCustomFields(Entity\StationMedia $media, array $custom_fields)
{
$this->_em->createQuery('DELETE FROM Entity\StationMediaCustomField e WHERE e.media_id = :media_id')
->setParameter('media_id', $media->getId())
->execute();
foreach ($custom_fields as $field_id => $field_value) {
/** @var Entity\CustomField $field */
$field = $this->_em->getReference(Entity\CustomField::class, $field_id);
$record = new Entity\StationMediaCustomField($media, $field);
$record->setValue($field_value);
$this->_em->persist($record);
}
$this->_em->flush();
}
}