4
0
mirror of https://github.com/AzuraCast/AzuraCast.git synced 2024-06-17 22:47:04 +00:00
AzuraCast/src/Entity/StationMedia.php

831 lines
20 KiB
PHP
Raw Normal View History

<?php
namespace App\Entity;
use App\Annotations\AuditLog;
2019-09-04 18:00:51 +00:00
use App\ApiUtilities;
use App\Radio\Backend\Liquidsoap;
use App\Normalizer\Annotation\DeepNormalize;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
2019-04-22 11:19:21 +00:00
use OpenApi\Annotations as OA;
use Psr\Http\Message\UriInterface;
use Symfony\Component\Serializer\Annotation as Serializer;
/**
* @ORM\Table(name="station_media", indexes={
* @ORM\Index(name="search_idx", columns={"title", "artist", "album"})
* }, uniqueConstraints={
* @ORM\UniqueConstraint(name="path_unique_idx", columns={"path", "station_id"})
* })
* @ORM\Entity()
2019-04-22 11:19:21 +00:00
*
* @OA\Schema(type="object")
*/
class StationMedia
{
use Traits\UniqueId, Traits\TruncateStrings;
2019-10-11 01:22:02 +00:00
public const UNIQUE_ID_LENGTH = 24;
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=1)
*
* @var int
*/
protected $id;
/**
* @ORM\Column(name="station_id", type="integer")
* @var int
*/
protected $station_id;
/**
* @ORM\ManyToOne(targetEntity="Station", inversedBy="media")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="station_id", referencedColumnName="id", onDelete="CASCADE")
* })
* @var Station
*/
protected $station;
/**
* @ORM\Column(name="song_id", type="string", length=50, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="098F6BCD4621D373CADE4E832627B4F6")
*
* @var string|null
*/
protected $song_id;
/**
* @ORM\ManyToOne(targetEntity="Song")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="song_id", referencedColumnName="id", onDelete="SET NULL")
* })
* @var Song|null
*/
protected $song;
/**
* @ORM\Column(name="title", type="string", length=200, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="Test Song")
*
* @var string|null The name of the media file's title.
*/
protected $title;
/**
* @ORM\Column(name="artist", type="string", length=200, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="Test Artist")
*
* @var string|null The name of the media file's artist.
*/
protected $artist;
/**
* @ORM\Column(name="album", type="string", length=200, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="Test Album")
*
* @var string|null The name of the media file's album.
*/
protected $album;
/**
* @ORM\Column(name="lyrics", type="text", nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="...Never gonna give you up...")
*
* @var string|null Full lyrics of the track, if available.
*/
protected $lyrics;
/**
* @ORM\Column(name="isrc", type="string", length=15, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="GBARL0600786")
*
* @var string|null The track ISRC (International Standard Recording Code), used for licensing purposes.
*/
protected $isrc;
/**
* @ORM\Column(name="length", type="integer")
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=240)
*
* @var int The song duration in seconds.
*/
protected $length = 0;
/**
* @ORM\Column(name="length_text", type="string", length=10, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="4:00")
*
* @var string|null The formatted song duration (in mm:ss format)
*/
protected $length_text = '0:00';
/**
* @ORM\Column(name="path", type="string", length=500, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example="test.mp3")
*
* @var string|null The relative path of the media file.
*/
protected $path;
/**
* @ORM\Column(name="mtime", type="integer", nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=SAMPLE_TIMESTAMP)
*
* @var int|null The UNIX timestamp when the database was last modified.
*/
protected $mtime = 0;
/**
* @ORM\Column(name="amplify", type="decimal", precision=3, scale=1, nullable=true)
*
* @OA\Property(example=-14.00)
*
* @var float|null The amount of amplification (in dB) to be applied to the radio source;
* equivalent to Liquidsoap's "liq_amplify" annotation.
*/
protected $amplify;
/**
* @ORM\Column(name="fade_overlap", type="decimal", precision=3, scale=1, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=2.00)
*
* @var float|null The length of time (in seconds) before the next song starts in the fade;
* equivalent to Liquidsoap's "liq_start_next" annotation.
*/
protected $fade_overlap;
/**
* @ORM\Column(name="fade_in", type="decimal", precision=3, scale=1, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=3.00)
*
* @var float|null The length of time (in seconds) to fade in the next track;
* equivalent to Liquidsoap's "liq_fade_in" annotation.
*/
protected $fade_in;
/**
* @ORM\Column(name="fade_out", type="decimal", precision=3, scale=1, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=3.00)
*
* @var float|null The length of time (in seconds) to fade out the previous track;
* equivalent to Liquidsoap's "liq_fade_out" annotation.
*/
protected $fade_out;
/**
* @ORM\Column(name="cue_in", type="decimal", precision=5, scale=1, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=30.00)
*
* @var float|null The length of time (in seconds) from the start of the track to start playing;
* equivalent to Liquidsoap's "liq_cue_in" annotation.
*/
protected $cue_in;
/**
* @ORM\Column(name="cue_out", type="decimal", precision=5, scale=1, nullable=true)
2019-04-22 11:19:21 +00:00
*
* @OA\Property(example=30.00)
*
* @var float|null The length of time (in seconds) from the CUE-IN of the track to stop playing;
* equivalent to Liquidsoap's "liq_cue_out" annotation.
*/
protected $cue_out;
/**
* @ORM\Column(name="art_updated_at", type="integer")
* @AuditLog\AuditIgnore()
*
* @OA\Property(example=SAMPLE_TIMESTAMP)
* @var int The latest time (UNIX timestamp) when album art was updated.
*/
protected $art_updated_at = 0;
/**
* @ORM\OneToMany(targetEntity="StationPlaylistMedia", mappedBy="media")
2019-04-22 11:19:21 +00:00
*
* @DeepNormalize(true)
* @Serializer\MaxDepth(1)
*
2019-04-22 11:19:21 +00:00
* @OA\Property(@OA\Items())
*
* @var Collection
*/
protected $playlists;
/**
* @ORM\OneToMany(targetEntity="StationMediaCustomField", mappedBy="media")
*
* @var Collection
*/
protected $custom_fields;
public function __construct(Station $station, string $path)
{
$this->station = $station;
$this->playlists = new ArrayCollection;
$this->custom_fields = new ArrayCollection;
$this->setPath($path);
$this->generateUniqueId();
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return Station
*/
public function getStation(): Station
{
return $this->station;
}
/**
* @return string|null
*/
public function getSongId(): ?string
{
return $this->song_id;
}
/**
* @return null|string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param null|string $title
*/
public function setTitle(string $title = null): void
{
$this->title = $this->_truncateString($title, 200);
}
/**
* @return null|string
*/
public function getArtist(): ?string
{
return $this->artist;
}
/**
* @param null|string $artist
*/
public function setArtist(string $artist = null): void
{
$this->artist = $this->_truncateString($artist, 200);
}
/**
* @return null|string
*/
public function getAlbum(): ?string
{
return $this->album;
}
/**
* @param null|string $album
*/
public function setAlbum(string $album = null): void
{
$this->album = $this->_truncateString($album, 200);
}
/**
* @return null|string
*/
public function getLyrics(): ?string
{
return $this->lyrics;
}
/**
* @param null|string $lyrics
*/
public function setLyrics($lyrics): void
{
$this->lyrics = $lyrics;
}
/**
* Get the Flysystem URI for album artwork for this item.
*
* @return string
*/
public function getArtPath(): string
{
2019-09-04 18:00:51 +00:00
return 'albumart://' . $this->unique_id . '.jpg';
}
/**
* @return null|string
*/
public function getIsrc(): ?string
{
return $this->isrc;
}
/**
* @param null|string $isrc
*/
public function setIsrc(string $isrc = null): void
{
$this->isrc = $isrc;
}
/**
* @return int
*/
public function getLength(): int
{
return $this->length;
}
/**
* @param int $length
*/
public function setLength($length): void
{
$length_min = floor($length / 60);
$length_sec = $length % 60;
$this->length = (int)round($length);
$this->length_text = $length_min . ':' . str_pad($length_sec, 2, '0', STR_PAD_LEFT);
}
/**
* @return null|string
*/
public function getLengthText(): ?string
{
return $this->length_text;
}
/**
* @param null|string $length_text
*/
public function setLengthText(string $length_text = null): void
{
$this->length_text = $length_text;
}
/**
* @return null|string
*/
public function getPath(): ?string
{
return $this->path;
}
/**
* @param null|string $path
*/
public function setPath(string $path = null): void
{
$this->path = $path;
}
/**
* Return the abstracted "full path" filesystem URI for this record.
*
* @return string
*/
public function getPathUri(): string
{
2019-09-04 18:00:51 +00:00
return 'media://' . $this->path;
}
/**
* @return int|null
*/
public function getMtime(): ?int
{
return $this->mtime;
}
/**
* @param int|null $mtime
*/
public function setMtime(int $mtime = null): void
{
$this->mtime = $mtime;
}
/**
* @return float|null
*/
public function getAmplify(): ?float
{
return $this->amplify;
}
/**
* @param float|null $amplify
*/
public function setAmplify($amplify = null): void
{
if ($amplify === '') {
$amplify = null;
}
$this->amplify = (null === $amplify) ? null : (float)$amplify;
}
/**
* @return float|null
*/
public function getFadeOverlap(): ?float
{
return $this->fade_overlap;
}
/**
* @param float|null $fade_overlap
*/
public function setFadeOverlap($fade_overlap = null): void
{
if ($fade_overlap === '') {
$fade_overlap = null;
}
$this->fade_overlap = $fade_overlap;
}
/**
* @return float|null
*/
public function getFadeIn(): ?float
{
return $this->fade_in;
}
/**
* @param string|float|null $fade_in
*/
public function setFadeIn($fade_in = null): void
{
$this->fade_in = $this->parseSeconds($fade_in);
}
/**
* @return float|null
*/
public function getFadeOut(): ?float
{
return $this->fade_out;
}
/**
* @param string|float|null $fade_out
*/
public function setFadeOut($fade_out = null): void
{
$this->fade_out = $this->parseSeconds($fade_out);
}
/**
* @return float|null
*/
public function getCueIn(): ?float
{
return $this->cue_in;
}
/**
* @param string|float|null $cue_in
*/
public function setCueIn($cue_in = null): void
{
$this->cue_in = $this->parseSeconds($cue_in);
}
/**
* @return float|null
*/
public function getCueOut(): ?float
{
return $this->cue_out;
}
/**
* @param string|float|null $cue_out
*/
public function setCueOut($cue_out = null): void
{
$this->cue_out = $this->parseSeconds($cue_out);
}
/**
* @param string|float|null $seconds
*
* @return float|null
*/
2019-11-07 07:07:12 +00:00
protected function parseSeconds($seconds = null): ?float
{
if ($seconds === '') {
return null;
}
if (false !== strpos($seconds, ':')) {
$sec = 0;
foreach (array_reverse(explode(':', $seconds)) as $k => $v) {
2019-11-07 07:20:09 +00:00
$sec += (60 ** (int)$k) * (int)$v;
}
return $sec;
}
return $seconds;
}
/**
* Get the length with cue-in and cue-out points included.
*
* @return int
*/
public function getCalculatedLength(): int
{
$length = (int)$this->length;
if ((int)$this->cue_out > 0) {
$length_removed = $length - (int)$this->cue_out;
$length -= $length_removed;
}
if ((int)$this->cue_in > 0) {
$length -= $this->cue_in;
}
return $length;
}
/**
* @return int
*/
public function getArtUpdatedAt(): int
{
return $this->art_updated_at;
}
/**
* @param int $art_updated_at
*/
public function setArtUpdatedAt(int $art_updated_at): void
{
$this->art_updated_at = $art_updated_at;
}
/**
* @param StationPlaylist $playlist
*
* @return StationPlaylistMedia|null
*/
public function getItemForPlaylist(StationPlaylist $playlist): ?StationPlaylistMedia
{
2019-09-04 18:00:51 +00:00
$item = $this->playlists->filter(function ($spm) use ($playlist) {
/** @var StationPlaylistMedia $spm */
return $spm->getPlaylist()->getId() === $playlist->getId();
});
2020-01-20 23:08:12 +00:00
$firstItem = $item->first();
return ($firstItem instanceof StationPlaylistMedia)
? $firstItem
: null;
}
/**
* @return Collection
*/
public function getCustomFields(): Collection
{
return $this->custom_fields;
}
/**
* @param Collection $custom_fields
*/
public function setCustomFields(Collection $custom_fields): void
{
$this->custom_fields = $custom_fields;
}
/**
* Indicate whether this media needs reprocessing given certain factors.
*
* @param int $current_mtime
*
* @return bool
*/
public function needsReprocessing($current_mtime = 0): bool
{
if ($current_mtime > $this->mtime) {
return true;
}
if (!$this->songMatches()) {
return true;
}
return false;
}
2019-09-04 18:00:51 +00:00
/**
* Check if the hash of the associated Song record matches the hash that would be
* generated by this record's artist and title metadata. Used to determine if a
* record should be reprocessed or not.
*
* @return bool
*/
public function songMatches(): bool
{
return (null !== $this->song_id)
&& ($this->song_id === $this->getExpectedSongHash());
}
/**
* Get the appropriate song hash for the title and artist specified here.
*
* @return string
*/
protected function getExpectedSongHash(): string
{
return Song::getSongHash([
'artist' => $this->artist,
'title' => $this->title,
]);
}
/**
* Assemble a list of annotations for LiquidSoap.
*
* Liquidsoap expects a string similar to:
* annotate:type="song",album="$ALBUM",display_desc="$FULLSHOWNAME",
* liq_start_next="2.5",liq_fade_in="3.5",liq_fade_out="3.5":$SONGPATH
*
* @return array
*/
public function getAnnotations(): array
{
$annotations = [];
$annotation_types = [
2019-09-04 18:00:51 +00:00
'title' => $this->title,
'artist' => $this->artist,
'duration' => $this->length,
'song_id' => $this->getSong()->getId(),
'media_id' => $this->id,
'liq_amplify' => $this->amplify,
2019-11-13 01:53:20 +00:00
'liq_cross_duration' => $this->fade_overlap,
2019-09-04 18:00:51 +00:00
'liq_fade_in' => $this->fade_in,
'liq_fade_out' => $this->fade_out,
'liq_cue_in' => $this->cue_in,
'liq_cue_out' => $this->cue_out,
];
// Safety checks for cue lengths.
if ($annotation_types['liq_cue_out'] < 0) {
$cue_out = abs($annotation_types['liq_cue_out']);
if (0 === $cue_out || $cue_out > $annotation_types['duration']) {
$annotation_types['liq_cue_out'] = null;
} else {
$annotation_types['liq_cue_out'] = max(0, $annotation_types['duration'] - $cue_out);
}
}
if (($annotation_types['liq_cue_in'] + $annotation_types['liq_cue_out']) > $annotation_types['duration']) {
$annotation_types['liq_cue_out'] = null;
}
if ($annotation_types['liq_cue_in'] > $annotation_types['duration']) {
$annotation_types['liq_cue_in'] = null;
}
foreach ($annotation_types as $annotation_name => $prop) {
if (null === $prop) {
continue;
}
$prop = mb_convert_encoding($prop, 'UTF-8');
$prop = str_replace(['"', "\n", "\t", "\r"], ["'", '', '', ''], $prop);
// Convert Liquidsoap-specific annotations to floats.
if ('duration' === $annotation_name || 0 === strpos($annotation_name, 'liq')) {
$prop = Liquidsoap::toFloat($prop);
}
$annotations[$annotation_name] = $prop;
}
return $annotations;
}
2019-09-04 18:00:51 +00:00
/**
* @return Song|null
*/
public function getSong(): ?Song
{
return $this->song;
}
/**
* @param Song|null $song
*/
public function setSong(Song $song = null): void
{
$this->song = $song;
}
/**
* Indicates whether this media is a part of any "requestable" playlists.
*
* @return bool
*/
public function isRequestable(): bool
{
$playlists = $this->getPlaylists();
2019-09-04 18:00:51 +00:00
foreach ($playlists as $playlist_item) {
$playlist = $playlist_item->getPlaylist();
/** @var StationPlaylist $playlist */
if ($playlist->isRequestable()) {
return true;
}
}
return false;
}
2019-09-04 18:00:51 +00:00
/**
* @return Collection
*/
public function getPlaylists(): Collection
{
return $this->playlists;
}
/**
* @return string A string identifying this entity.
*/
public function __toString(): string
{
return $this->unique_id . ': ' . $this->artist . ' - ' . $this->title;
}
/**
* Retrieve the API version of the object/array.
*
* @param ApiUtilities $apiUtils
* @param UriInterface|null $baseUri
*
* @return Api\Song
*/
public function api(ApiUtilities $apiUtils, UriInterface $baseUri = null): Api\Song
{
$response = new Api\Song;
$response->id = (string)$this->song_id;
$response->text = $this->artist . ' - ' . $this->title;
$response->artist = (string)$this->artist;
$response->title = (string)$this->title;
$response->album = (string)$this->album;
$response->lyrics = (string)$this->lyrics;
$response->art = $apiUtils->getAlbumArtUrl(
$this->station_id,
$this->unique_id,
$this->art_updated_at,
$baseUri
);
$response->custom_fields = $apiUtils->getCustomFields($this->id);
return $response;
}
}