AzuraCast/app/src/Entity/StationMedia.php

733 lines
17 KiB
PHP

<?php
namespace Entity;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
/**
* @Table(name="station_media", indexes={
* @index(name="search_idx", columns={"title", "artist", "album"})
* }, uniqueConstraints={
* @UniqueConstraint(name="path_unique_idx", columns={"path", "station_id"})
* })
* @Entity(repositoryClass="Entity\Repository\StationMediaRepository")
* @HasLifecycleCallbacks
*/
class StationMedia
{
use Traits\UniqueId, Traits\TruncateStrings;
/**
* @Column(name="id", type="integer")
* @Id
* @GeneratedValue(strategy="IDENTITY")
* @var int
*/
protected $id;
/**
* @Column(name="station_id", type="integer")
* @var int
*/
protected $station_id;
/**
* @ManyToOne(targetEntity="Station", inversedBy="media")
* @JoinColumns({
* @JoinColumn(name="station_id", referencedColumnName="id", onDelete="CASCADE")
* })
* @var Station
*/
protected $station;
/**
* @Column(name="song_id", type="string", length=50, nullable=true)
* @var int|null
*/
protected $song_id;
/**
* @ManyToOne(targetEntity="Song")
* @JoinColumns({
* @JoinColumn(name="song_id", referencedColumnName="id", onDelete="SET NULL")
* })
* @var Song|null
*/
protected $song;
/**
* @Column(name="title", type="string", length=200, nullable=true)
* @var string|null
*/
protected $title;
/**
* @Column(name="artist", type="string", length=200, nullable=true)
* @var string|null
*/
protected $artist;
/**
* @Column(name="album", type="string", length=200, nullable=true)
* @var string|null
*/
protected $album;
/**
* @Column(name="lyrics", type="text", nullable=true)
* @var string|null
*/
protected $lyrics;
/**
* @OneToOne(targetEntity="StationMediaArt", mappedBy="media", cascade={"persist"})
* @var StationMediaArt
*/
protected $art;
/**
* @Column(name="isrc", type="string", length=15, nullable=true)
* @var string|null The track ISRC (International Standard Recording Code), used for licensing purposes.
*/
protected $isrc;
/**
* @Column(name="length", type="smallint")
* @var int
*/
protected $length;
/**
* @Column(name="length_text", type="string", length=10, nullable=true)
* @var string|null
*/
protected $length_text;
/**
* @Column(name="path", type="string", length=500, nullable=true)
* @var string|null
*/
protected $path;
/**
* @Column(name="mtime", type="integer", nullable=true)
* @var int|null
*/
protected $mtime;
/**
* @Column(name="fade_overlap", type="decimal", precision=3, scale=1, nullable=true)
* @var float|null
*/
protected $fade_overlap;
/**
* @Column(name="fade_in", type="decimal", precision=3, scale=1, nullable=true)
* @var float|null
*/
protected $fade_in;
/**
* @Column(name="fade_out", type="decimal", precision=3, scale=1, nullable=true)
* @var float|null
*/
protected $fade_out;
/**
* @Column(name="cue_in", type="decimal", precision=5, scale=1, nullable=true)
* @var float|null
*/
protected $cue_in;
/**
* @Column(name="cue_out", type="decimal", precision=5, scale=1, nullable=true)
* @var float|null
*/
protected $cue_out;
/**
* @OneToMany(targetEntity="StationPlaylistMedia", mappedBy="media")
* @var Collection
*/
protected $playlist_items;
/**
* @OneToMany(targetEntity="StationMediaCustomField", mappedBy="media")
* @var Collection
*/
protected $custom_fields;
public function __construct(Station $station, string $path)
{
$this->station = $station;
$this->path = $path;
$this->length = 0;
$this->length_text = '0:00';
$this->mtime = 0;
$this->playlist_items = new ArrayCollection;
$this->custom_fields = new ArrayCollection;
}
/**
* @return int
*/
public function getId(): int
{
return $this->id;
}
/**
* @return Station
*/
public function getStation(): Station
{
return $this->station;
}
/**
* @return Song|null
*/
public function getSong(): ?Song
{
return $this->song;
}
/**
* @param Song|null $song
*/
public function setSong(Song $song = null)
{
$this->song = $song;
}
/**
* @return null|string
*/
public function getTitle(): ?string
{
return $this->title;
}
/**
* @param null|string $title
*/
public function setTitle(string $title = null)
{
$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)
{
$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)
{
$this->album = $this->_truncateString($album, 200);
}
/**
* @return null|string
*/
public function getLyrics()
{
return $this->lyrics;
}
/**
* @param null|string $lyrics
*/
public function setLyrics($lyrics)
{
$this->lyrics = $lyrics;
}
/**
* @return null|resource
*/
public function getArt()
{
if ($this->art instanceof StationMediaArt) {
return $this->art->getArt();
}
return null;
}
/**
* @param resource $source_image_path A GD image manipulation resource.
* @return bool
*/
public function setArt($source_gd_image = null)
{
if (!($this->art instanceof StationMediaArt)) {
$this->art = new StationMediaArt($this);
}
return $this->art->setArt($source_gd_image);
}
/**
* @return null|string
*/
public function getIsrc(): ?string
{
return $this->isrc;
}
/**
* @param null|string $isrc
*/
public function setIsrc(string $isrc = null)
{
$this->isrc = $isrc;
}
/**
* @return int
*/
public function getLength(): int
{
return $this->length;
}
/**
* @param $length
*/
public function setLength($length)
{
$length_min = floor($length / 60);
$length_sec = $length % 60;
$this->length = 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)
{
$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)
{
$this->path = $path;
}
/**
* @return int|null
*/
public function getMtime(): ?int
{
return $this->mtime;
}
/**
* @param int|null $mtime
*/
public function setMtime(int $mtime = null)
{
$this->mtime = $mtime;
}
/**
* @return float|null
*/
public function getFadeOverlap(): ?float
{
return $this->fade_overlap;
}
/**
* @param float|null $fade_overlap
*/
public function setFadeOverlap(float $fade_overlap = null)
{
$this->fade_overlap = $fade_overlap;
}
/**
* @return float|null
*/
public function getFadeIn(): ?float
{
return $this->fade_in;
}
/**
* @param float|null $fade_in
*/
public function setFadeIn(float $fade_in = null)
{
$this->fade_in = $fade_in;
}
/**
* @return float|null
*/
public function getFadeOut(): ?float
{
return $this->fade_out;
}
/**
* @param float|null $fade_out
*/
public function setFadeOut(float $fade_out = null)
{
$this->fade_out = $fade_out;
}
/**
* @return float|null
*/
public function getCueIn(): ?float
{
return $this->cue_in;
}
/**
* @param float|null $cue_in
*/
public function setCueIn(float $cue_in = null)
{
$this->cue_in = $cue_in;
}
/**
* @return float|null
*/
public function getCueOut(): ?float
{
return $this->cue_out;
}
/**
* @param float|null $cue_out
*/
public function setCueOut(float $cue_out = null)
{
$this->cue_out = $cue_out;
}
/**
* Get the length with cue-in and cue-out points included.
*
* @return int
*/
public function getCalculatedLength()
{
$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 Collection
*/
public function getPlaylistItems(): Collection
{
return $this->playlist_items;
}
public function getItemForPlaylist(StationPlaylist $playlist): ?StationPlaylistMedia
{
$item = $this->playlist_items->filter(function($spm) use ($playlist) {
/** @var StationPlaylistMedia $spm */
return ($spm->getPlaylist()->getId() == $playlist->getId());
});
return $item->first() ?? 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;
}
/**
* Assemble a list of annotations for LiquidSoap.
*
* @return array
*/
public function getAnnotations(): array
{
$annotations = [];
$annotation_types = [
'title' => 'title',
'artist' => 'artist',
'fade_overlap' => 'liq_start_next',
'fade_in' => 'liq_fade_in',
'fade_out' => 'liq_fade_out',
'cue_in' => 'liq_cue_in',
'cue_out' => 'liq_cue_out',
];
foreach ($annotation_types as $annotation_property => $annotation_name) {
if ($this->$annotation_property !== null) {
$prop = $this->$annotation_property;
$prop = mb_convert_encoding($prop, "UTF-8");
$prop = str_replace(['"', "\n", "\t", "\r"], ["'", '', '', ''], $prop);
if ($annotation_property === 'cue_out' && $prop < 0) {
$prop = max(0, $this->getLength() - abs($prop));
}
$annotations[$annotation_property] = $annotation_name . '="' . $prop . '"';
}
}
return $annotations;
}
/**
* Process metadata information from media file.
*
* @param bool $force
* @return array|bool
* - Array containing song information, if one is detected and needs updating
* - False if information was not updated
*/
public function loadFromFile($force = false)
{
if (empty($this->path)) {
return false;
}
$media_base_dir = $this->station->getRadioMediaDir();
$media_path = $media_base_dir . '/' . $this->path;
$path_parts = pathinfo($media_path);
// Only update metadata if the file has been updated.
$media_mtime = filemtime($media_path);
// Check for a hash mismatch.
$expected_song_hash = Song::getSongHash([
'artist' => $this->artist,
'title' => $this->title,
]);
if ($media_mtime > $this->mtime
|| null === $this->song_id
|| $this->song_id != $expected_song_hash
|| $force) {
$this->mtime = $media_mtime;
// Load metadata from supported files.
$id3 = new \getID3();
$id3->option_md5_data = true;
$id3->option_md5_data_source = true;
$id3->encoding = 'UTF-8';
$file_info = $id3->analyze($media_path);
if (empty($file_info['error'])) {
$this->setLength($file_info['playtime_seconds']);
$tags_to_set = ['title', 'artist', 'album'];
if (!empty($file_info['tags'])) {
foreach ($file_info['tags'] as $tag_type => $tag_data) {
foreach ($tags_to_set as $tag) {
if (!empty($tag_data[$tag][0])) {
$this->{$tag} = mb_convert_encoding($tag_data[$tag][0], "UTF-8");
}
}
if (!empty($tag_data['unsynchronized_lyric'][0])) {
$this->lyrics = $tag_data['unsynchronized_lyric'][0];
}
}
}
if (!empty($file_info['comments']['picture'][0])) {
$picture = $file_info['comments']['picture'][0];
$this->setArt(imagecreatefromstring($picture['data']));
}
}
// Attempt to derive title and artist from filename.
if (empty($this->title)) {
$filename = str_replace('_', ' ', $path_parts['filename']);
$string_parts = explode('-', $filename);
// If not normally delimited, return "text" only.
if (count($string_parts) == 1) {
$this->title = trim($filename);
$this->artist = '';
} else {
$this->title = trim(array_pop($string_parts));
$this->artist = trim(implode('-', $string_parts));
}
}
return [
'artist' => $this->artist,
'title' => $this->title,
];
}
return false;
}
/**
* Write modified metadata directly to the file as ID3 information.
*/
public function writeToFile()
{
$getID3 = new \getID3;
$getID3->setOption(['encoding' => 'UTF8']);
require_once(APP_INCLUDE_VENDOR . '/james-heinrich/getid3/getid3/write.php');
$tagwriter = new \getid3_writetags;
$tagwriter->filename = $this->getFullPath();
$tagwriter->tagformats = ['id3v1', 'id3v2.3'];
$tagwriter->overwrite_tags = true;
$tagwriter->tag_encoding = 'UTF8';
$tagwriter->remove_other_tags = true;
$tag_data = [
'title' => [$this->title],
'artist' => [$this->artist],
'album' => [$this->album],
];
if (is_resource($this->art)) {
$tag_data['attached_picture'][0] = [
'data' => stream_get_contents($this->art),
'picturetypeid' => 'image/jpeg',
'mime' => 'image/jpeg',
];
$tag_data['comments']['picture'][0] = $tag_data['attached_picture'][0];
}
$tagwriter->tag_data = $tag_data;
// write tags
if ($tagwriter->WriteTags()) {
$this->mtime = time();
return true;
}
return false;
}
public function getFullPath()
{
$media_base_dir = $this->station->getRadioMediaDir();
return $media_base_dir . '/' . $this->path;
}
/**
* Indicates whether this media is a part of any "requestable" playlists.
*
* @return bool
*/
public function isRequestable(): bool
{
$playlists = $this->getPlaylistItems();
foreach($playlists as $playlist_item) {
$playlist = $playlist_item->getPlaylist();
/** @var StationPlaylist $playlist */
if ($playlist->isRequestable()) {
return true;
}
}
return false;
}
/**
* Retrieve the API version of the object/array.
*
* @return Api\Song
*/
public function api(\AzuraCast\ApiUtilities $api_utils): 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 = $api_utils->getAlbumArtUrl($this->station_id, $this->unique_id);
$response->custom_fields = $api_utils->getCustomFields($this->id);
return $response;
}
}