AzuraCast/src/Controller/Api/Stations/Reports/SoundExchangeAction.php

173 lines
5.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations\Reports;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Service\MusicBrainz;
use Carbon\CarbonImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
/**
* Produce a report in SoundExchange (the US webcaster licensing agency) format.
*/
final class SoundExchangeAction
{
public function __construct(
private readonly EntityManagerInterface $em,
private readonly MusicBrainz $musicBrainz
) {
}
public function __invoke(
ServerRequest $request,
Response $response,
string $station_id
): ResponseInterface {
$station = $request->getStation();
$tzObject = $station->getTimezoneObject();
$defaultStartDate = CarbonImmutable::parse('first day of last month', $tzObject)->format('Y-m-d');
$defaultEndDate = CarbonImmutable::parse('last day of last month', $tzObject)->format('Y-m-d');
$data = $request->getParams();
$data['start_date'] ??= $defaultStartDate;
$data['end_date'] ??= $defaultEndDate;
$startDate = CarbonImmutable::parse($data['start_date'] . ' 00:00:00', $tzObject);
$endDate = CarbonImmutable::parse($data['end_date'] . ' 23:59:59', $tzObject);
$fetchIsrc = 'true' === ($data['fetch_isrc'] ?? 'false');
$export = [
[
'NAME_OF_SERVICE',
'TRANSMISSION_CATEGORY',
'FEATURED_ARTIST',
'SOUND_RECORDING_TITLE',
'ISRC',
'ALBUM_TITLE',
'MARKETING_LABEL',
'ACTUAL_TOTAL_PERFORMANCES',
],
];
$all_media = $this->em->createQuery(
<<<'DQL'
SELECT sm, spm, sp, smcf
FROM App\Entity\StationMedia sm
LEFT JOIN sm.custom_fields smcf
LEFT JOIN sm.playlists spm
LEFT JOIN spm.playlist sp
WHERE sm.storage_location = :storageLocation
AND sp.station IS NULL OR sp.station = :station
DQL
)->setParameter('station', $station)
->setParameter('storageLocation', $station->getMediaStorageLocation())
->getArrayResult();
$media_by_id = array_column($all_media, null, 'id');
$history_rows = $this->em->createQuery(
<<<'DQL'
SELECT sh.song_id AS song_id, sh.text, sh.artist, sh.title, sh.media_id, COUNT(sh.id) AS plays,
SUM(sh.unique_listeners) AS unique_listeners
FROM App\Entity\SongHistory sh
WHERE sh.station = :station
AND sh.timestamp_start <= :time_end
AND sh.timestamp_end >= :time_start
GROUP BY sh.song_id
DQL
)->setParameter('station', $station)
->setParameter('time_start', $startDate->getTimestamp())
->setParameter('time_end', $endDate->getTimestamp())
->getArrayResult();
// TODO: Fix this (not all song rows have a media_id)
$history_rows_by_id = array_column($history_rows, null, 'media_id');
// Remove any reference to the "Stream Offline" song.
$offline_song_hash = Entity\Song::createOffline()->getSongId();
unset($history_rows_by_id[$offline_song_hash]);
// Assemble report items
$station_name = $station->getName();
$set_isrc_query = $this->em->createQuery(
<<<'DQL'
UPDATE App\Entity\StationMedia sm
SET sm.isrc = :isrc
WHERE sm.id = :media_id
DQL
);
foreach ($history_rows_by_id as $song_id => $history_row) {
$song_row = $media_by_id[$song_id] ?? $history_row;
// Try to find the ISRC if it's not already listed.
if ($fetchIsrc && empty($song_row['isrc'])) {
$isrc = $this->findISRC($song_row);
$song_row['isrc'] = $isrc;
if (null !== $isrc && isset($song_row['media_id'])) {
$set_isrc_query->setParameter('isrc', $isrc)
->setParameter('media_id', $song_row['media_id'])
->execute();
}
}
$export[] = [
$station_name,
'A',
$song_row['artist'] ?? '',
$song_row['title'] ?? '',
$song_row['isrc'] ?? '',
$song_row['album'] ?? '',
'',
$history_row['unique_listeners'],
];
}
// Assemble export into SoundExchange format
$export_txt_raw = [];
foreach ($export as $export_row) {
foreach ($export_row as $i => $export_col) {
if (!is_numeric($export_col)) {
$export_row[$i] = '^' . str_replace(['^', '|'], ['', ''], strtoupper($export_col)) . '^';
}
}
$export_txt_raw[] = implode('|', $export_row);
}
$export_txt = implode("\n", $export_txt_raw);
// Example: WABC01012009-31012009_A.txt
$export_filename = strtoupper($station->getShortName())
. $startDate->format('dmY') . '-'
. $endDate->format('dmY') . '_A.txt';
return $response->renderStringAsFile($export_txt, 'text/plain', $export_filename);
}
private function findISRC(array $song_row): ?string
{
$song = Entity\Song::createFromArray($song_row);
try {
foreach ($this->musicBrainz->findRecordingsForSong($song, 'isrcs') as $recording) {
if (!empty($recording['isrcs'])) {
return $recording['isrcs'][0];
}
}
return null;
} catch (Throwable) {
return null;
}
}
}