Vuetify automation, clean up SoundExchange report.

This commit is contained in:
Buster "Silver Eagle" Neece 2021-11-04 19:50:13 -05:00
parent 408d4c6a4b
commit 5caa21ba33
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
17 changed files with 484 additions and 357 deletions

View File

@ -1,46 +0,0 @@
<?php
return [
'method' => 'post',
'elements' => [
'is_enabled' => [
'radio',
[
'label' => __('Enable Automated Assignment'),
'description' => __('Allow the system to periodically automatically assign songs to playlists based on their performance. This process will run in the background, and will only run if this option is set to "Enabled" and at least one playlist is set to "Include in Automated Assignment".'),
'default' => '0',
'choices' => [
0 => __('Disabled'),
1 => __('Enabled'),
],
],
],
'threshold_days' => [
'radio',
[
'label' => __('Days Between Automated Assignments'),
'description' => __('Based on this setting, the system will automatically reassign songs every (this) days using data from the previous (this) days.'),
'class' => 'inline',
'default' => App\Sync\Task\RunAutomatedAssignmentTask::DEFAULT_THRESHOLD_DAYS,
'choices' => [
7 => sprintf(__('%d days'), 7),
14 => sprintf(__('%d days'), 14),
30 => sprintf(__('%d days'), 30),
60 => sprintf(__('%d days'), 60),
],
],
],
'submit' => [
'submit',
[
'type' => 'submit',
'label' => __('Save Changes'),
'class' => 'btn btn-lg btn-primary',
],
],
],
];

View File

@ -319,6 +319,26 @@ return static function (RouteCollectorProxy $app) {
->setName('api:stations:index')
->add(new Middleware\RateLimit('api', 5, 2));
$group->group(
'/automation',
function (RouteCollectorProxy $group) {
$group->get(
'/settings',
Controller\Api\Stations\Automation\GetSettingsAction::class
)->setName('api:stations:automation:settings');
$group->put(
'/settings',
Controller\Api\Stations\Automation\PutSettingsAction::class
);
$group->put(
'/run',
Controller\Api\Stations\Automation\RunAction::class
)->setName('api:stations:automation:run');
}
)->add(new Middleware\Permissions(Acl::STATION_AUTOMATION, true));
$group->get('/nowplaying', Controller\Api\NowplayingController::class . ':indexAction');
$group->map(
@ -731,6 +751,11 @@ return static function (RouteCollectorProxy $app) {
'/overview/most-played',
Controller\Api\Stations\Reports\Overview\MostPlayedAction::class
)->setName('api:stations:reports:most-played');
$group->get(
'/soundexchange',
Controller\Api\Stations\Reports\SoundExchangeAction::class
)->setName('api:stations:reports:soundexchange');
}
)->add(new Middleware\Permissions(Acl::STATION_REPORTS, true));

View File

@ -20,16 +20,11 @@ return static function (RouteCollectorProxy $app) {
}
)->setName('stations:index:index');
$group->group(
$group->get(
'/automation',
function (RouteCollectorProxy $group) {
$group->map(['GET', 'POST'], '', Controller\Stations\AutomationController::class . ':indexAction')
->setName('stations:automation:index');
$group->get('/run', Controller\Stations\AutomationController::class . ':runAction')
->setName('stations:automation:run');
}
)->add(new Middleware\Permissions(Acl::STATION_AUTOMATION, true));
Controller\Stations\AutomationAction::class
)->setName('stations:automation:index')
->add(new Middleware\Permissions(Acl::STATION_AUTOMATION, true));
$group->get('/files', Controller\Stations\FilesAction::class)
->setName('stations:files:index')

View File

@ -0,0 +1,159 @@
<template>
<div>
<section class="card mb-3" role="region">
<b-card-header header-bg-variant="primary-dark">
<h2 class="card-title">
<translate key="lang_hdr_automated_assignment">Automated Playlist Assignment</translate>
</h2>
</b-card-header>
<div class="card-body">
<p class="card-text">
<translate key="lang_automated_1">Based on the previous performance of your station's songs, AzuraCast can automatically distribute songs evenly among your playlists, placing the highest performing songs in the highest-weighted playlists.</translate>
</p>
<p class="card-text">
<translate key="lang_automated_2">Once you have configured automated assignment, click the button below to run the automated assignment process.</translate>
</p>
<b-button variant="warning" :disabled="!settings.is_enabled" @click.prevent="doRun">
<translate key="lang_btn_run">Run Automated Assignment</translate>
</b-button>
</div>
</section>
<form class="form vue-form" @submit.prevent="submit">
<section class="card mb-3" role="region">
<b-card-header header-bg-variant="primary-dark">
<h2 class="card-title">
<translate key="lang_hdr_configure">Configure Automated Assignment</translate>
</h2>
</b-card-header>
<b-overlay variant="card" :show="settingsLoading">
<div class="card-body">
<b-form-fieldset>
<b-wrapped-form-checkbox id="edit_form_is_enabled"
:field="$v.settings.is_enabled">
<template #label="{lang}">
<translate :key="lang">Enable Automated Assignment</translate>
</template>
<template #description="{lang}">
<translate :key="lang">Allow the system to periodically automatically assign songs to playlists based on their performance. This process will run in the background, and will only run if this option is set to "Enabled" and at least one playlist is set to "Include in Automated Assignment".</translate>
</template>
</b-wrapped-form-checkbox>
<b-wrapped-form-group id="edit_form_threshold_days" :field="$v.settings.threshold_days">
<template #label="{lang}">
<translate :key="lang">Days Between Automated Assignments</translate>
</template>
<template #description="{lang}">
<translate :key="lang">Based on this setting, the system will automatically reassign songs every (this) days using data from the previous (this) days.</translate>
</template>
<template #default="props">
<b-form-radio-group stacked :id="props.id" v-model="props.field.$model"
:options="thresholdDaysOptions"></b-form-radio-group>
</template>
</b-wrapped-form-group>
</b-form-fieldset>
<b-button size="lg" type="submit" variant="primary" :disabled="$v.settings.$invalid">
<slot name="submitButtonName">
<translate key="lang_btn_save_changes">Save Changes</translate>
</slot>
</b-button>
</div>
</b-overlay>
</section>
</form>
</div>
</template>
<script>
import {validationMixin} from "vuelidate";
import BFormFieldset from "~/components/Form/BFormFieldset";
import BWrappedFormGroup from "~/components/Form/BWrappedFormGroup";
import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
import mergeExisting from "~/functions/mergeExisting";
export default {
name: 'StationsAutomation',
components: {BFormFieldset, BWrappedFormGroup, BWrappedFormCheckbox},
mixins: [
validationMixin
],
props: {
settingsUrl: String,
runUrl: String
},
data() {
return {
settingsLoading: true,
settings: {
is_enabled: false,
threshold_days: 7
}
}
},
validations: {
settings: {
is_enabled: {},
threshold_days: {}
}
},
computed: {
thresholdDaysOptions() {
const langDays = this.$gettext('%{ days } Days');
return [
{value: '7', text: this.$gettextInterpolate(langDays, {days: 7})},
{value: '14', text: this.$gettextInterpolate(langDays, {days: 14})},
{value: '30', text: this.$gettextInterpolate(langDays, {days: 30})},
{value: '60', text: this.$gettextInterpolate(langDays, {days: 60})}
];
}
},
mounted() {
this.relist();
},
methods: {
relist() {
this.$v.settings.$reset();
this.settingsLoading = true;
this.axios.get(this.settingsUrl).then((resp) => {
this.settings = mergeExisting(this.settings, resp.data);
this.settingsLoading = false;
});
},
submit() {
this.$v.settings.$touch();
if (this.$v.settings.$anyError) {
return;
}
this.$wrapWithLoading(
this.axios({
method: 'PUT',
url: this.settingsUrl,
data: this.settings
})
).then((resp) => {
this.$notifySuccess();
this.relist();
});
},
doRun() {
this.$wrapWithLoading(
this.axios({
method: 'PUT',
url: this.runUrl
})
).then((resp) => {
this.$notifySuccess();
});
}
}
}
</script>

View File

@ -6,9 +6,7 @@
</h3>
</div>
<form id="report-form" class="form vue-form" method="POST" action="">
<input type="hidden" name="csrf" :value="csrf"/>
<form id="report-form" class="form vue-form" method="GET" :action="apiUrl" target="_blank">
<div class="card-body">
<b-form-fieldset>
<p>This report is intended for licensing in the United States only, for webcasters paying royalties
@ -86,7 +84,7 @@ import BWrappedFormCheckbox from "~/components/Form/BWrappedFormCheckbox";
export default {
name: 'StationsReportsSoundExchange',
props: {
csrf: String,
apiUrl: String,
startDate: String,
endDate: String
},

View File

@ -0,0 +1,7 @@
import initBase from '~/base.js';
import '~/vendor/bootstrapVue.js';
import Automation from '~/components/Stations/Automation.vue';
export default initBase(Automation);

View File

@ -30,6 +30,7 @@ module.exports = {
SetupRegister: '~/pages/Setup/Register.js',
SetupSettings: '~/pages/Setup/Settings.js',
SetupStation: '~/pages/Setup/Station.js',
StationsAutomation: '~/pages/Stations/Automation.js',
StationsMedia: '~/pages/Stations/Media.js',
StationsMounts: '~/pages/Stations/Mounts.js',
StationsPlaylists: '~/pages/Stations/Playlists.js',

View File

@ -0,0 +1,23 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations\Automation;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
class GetSettingsAction
{
public function __invoke(
ServerRequest $request,
Response $response
): ResponseInterface {
$station = $request->getStation();
return $response->withJson(
(array)$station->getAutomationSettings()
);
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations\Automation;
use App\Doctrine\ReloadableEntityManagerInterface;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
class PutSettingsAction
{
public function __invoke(
ServerRequest $request,
Response $response,
ReloadableEntityManagerInterface $em
): ResponseInterface {
$station = $request->getStation();
$station = $em->refetch($station);
$station->setAutomationSettings((array)$request->getParsedBody());
$em->persist($station);
$em->flush();
return $response->withJson(Entity\Api\Status::updated());
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Controller\Api\Stations\Automation;
use App\Controller\Api\Admin\StationsController;
use App\Entity;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Sync\Task\RunAutomatedAssignmentTask;
use Psr\Http\Message\ResponseInterface;
class RunAction extends StationsController
{
public function __invoke(
ServerRequest $request,
Response $response,
RunAutomatedAssignmentTask $syncTask
): ResponseInterface {
$station = $request->getStation();
try {
$syncTask->runStation($station, true);
return $response->withJson(Entity\Api\Status::success());
} catch (\Throwable $e) {
return $response->withStatus(400)->withJson(Entity\Api\Error::fromException($e));
}
}
}

View File

@ -15,7 +15,7 @@ use Symfony\Component\Serializer\Normalizer\AbstractNormalizer;
/**
* This controller handles the specific "Edit Profile" function on a station's profile, which has different permissions
* and possible actions than
* and possible actions than the Admin Station Edit function.
*/
class ProfileEditController extends StationsController
{

View File

@ -0,0 +1,169 @@
<?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.
*/
class SoundExchangeAction
{
public function __construct(
protected EntityManagerInterface $em,
protected MusicBrainz $musicBrainz
) {
}
public function __invoke(ServerRequest $request, Response $response): 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);
}
protected 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;
}
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace App\Controller\Stations;
use App\Http\Response;
use App\Http\ServerRequest;
use Psr\Http\Message\ResponseInterface;
class AutomationAction
{
public function __invoke(
ServerRequest $request,
Response $response
): ResponseInterface {
$router = $request->getRouter();
return $request->getView()->renderVuePage(
response: $response,
component: 'Vue_StationsAutomation',
id: 'station-automation',
title: __('Automated Playlist Assignment'),
props: [
'settingsUrl' => (string)$router->fromHere('api:stations:automation:settings'),
'runUrl' => (string)$router->fromHere('api:stations:automation:run'),
],
);
}
}

View File

@ -1,73 +0,0 @@
<?php
declare(strict_types=1);
namespace App\Controller\Stations;
use App\Config;
use App\Form\Form;
use App\Http\Response;
use App\Http\ServerRequest;
use App\Session\Flash;
use App\Sync\Task\RunAutomatedAssignmentTask;
use Doctrine\ORM\EntityManagerInterface;
use Exception;
use Psr\Http\Message\ResponseInterface;
class AutomationController
{
protected array $form_config;
public function __construct(
protected EntityManagerInterface $em,
protected RunAutomatedAssignmentTask $sync_task,
Config $config
) {
$this->form_config = $config->get('forms/automation');
}
public function indexAction(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
$automation_settings = (array)$station->getAutomationSettings();
$form = new Form($this->form_config);
$form->populate($automation_settings);
if ($form->isValid($request)) {
$data = $form->getValues();
$station->setAutomationSettings($data);
$this->em->persist($station);
$this->em->flush();
$request->getFlash()->addMessage(__('Changes saved.'), Flash::SUCCESS);
return $response->withRedirect((string)$request->getUri());
}
return $request->getView()->renderToResponse($response, 'stations/automation/index', [
'form' => $form,
]);
}
public function runAction(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
try {
if ($this->sync_task->runStation($station, true)) {
$request->getFlash()->addMessage('<b>' . __('Automated assignment complete!') . '</b>', Flash::SUCCESS);
}
} catch (Exception $e) {
$request->getFlash()->addMessage(
'<b>' . __('Automated assignment error') . ':</b><br>' . $e->getMessage(),
Flash::ERROR
);
}
return $response->withRedirect((string)$request->getRouter()->fromHere('stations:automation:index'));
}
}

View File

@ -4,157 +4,22 @@ declare(strict_types=1);
namespace App\Controller\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.
*/
class SoundExchangeAction
{
public function __construct(
protected EntityManagerInterface $em,
protected MusicBrainz $musicBrainz
) {
}
public function __invoke(ServerRequest $request, Response $response): ResponseInterface
{
$station = $request->getStation();
$tzObject = $station->getTimezoneObject();
$csrf = $request->getCsrf();
$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');
if ($request->isPost()) {
$data = (array)$request->getParsedBody();
$csrf->verify($data['csrf'] ?? '', 'soundexchange');
$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);
}
$router = $request->getRouter();
return $request->getView()->renderVuePage(
response: $response,
@ -162,26 +27,10 @@ class SoundExchangeAction
id: 'station-report-soundexchange',
title: __('SoundExchange Report'),
props: [
'csrf' => $csrf->generate('soundexchange'),
'apiUrl' => (string)$router->fromHere('api:stations:reports:soundexchange'),
'startDate' => $defaultStartDate,
'endDate' => $defaultEndDate,
'endDate' => $defaultEndDate,
]
);
}
protected 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;
}
}
}

View File

@ -1,25 +0,0 @@
<?php
$this->layout('main', ['title' => __('Automated Playlist Assignment'), 'manual' => true]);
?>
<div class="card mb-3">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=__('Automated Playlist Assignment')?></h2>
</div>
<div class="card-body">
<p><?=sprintf(__('Based on the previous performance of your station\'s songs, %s can automatically distribute songs evenly among your playlists, placing the highest performing songs in the highest-weighted playlists.'),
$environment->getAppName())?></p>
<p><?=__('Once you have configured automated assignment, click the button below to run the automated assignment process. This process will not run at all unless you have selected "Enable" below.')?></p>
<a class="btn btn-warning" role="button" href="<?=$router->fromHere('stations:automation:run')?>"><?=__('Run Automated Assignment')?></a>
</div>
</div>
<div class="card">
<div class="card-header bg-primary-dark">
<h2 class="card-title"><?=__('Configure Automated Assignment')?></h2>
</div>
<div class="card-body">
<?=$this->fetch('system/form_edit', ['form' => $form])?>
</div>
</div>

View File

@ -1,45 +0,0 @@
<?php
use App\Entity;
class Station_AutomationCest extends CestAbstract
{
/**
* @before setupComplete
* @before login
*/
public function viewAutomation(FunctionalTester $I): void
{
$I->wantTo('Test station automation.');
// Set up automation preconditions.
$testStation = $this->getTestStation();
$playlist = new Entity\StationPlaylist($testStation);
$playlist->setName('Test Playlist');
$playlist->setIncludeInAutomation(true);
$this->em->persist($playlist);
$media = $this->uploadTestSong();
$spm = new Entity\StationPlaylistMedia($playlist, $media);
$this->em->persist($spm);
$this->em->flush();
$station_id = $testStation->getId();
$this->em->clear();
// Attempt to enable and run automation.
$I->amOnPage('/station/' . $station_id . '/automation');
$I->submitForm('.form', [
'is_enabled' => '1',
]);
$I->seeCurrentUrlEquals('/station/' . $station_id . '/automation');
$I->click('Run Automated Assignment');
$I->seeInSource('Automated assignment complete!');
}
}