From 51be731ee30d28c8276f5612ce8401a9d410e7c8 Mon Sep 17 00:00:00 2001 From: "Buster \"Silver Eagle\" Neece" Date: Fri, 10 Jun 2022 08:52:58 -0500 Subject: [PATCH] Remove automated playlist assignment. --- CHANGELOG.md | 4 +- config/events.php | 1 - config/menus/station.php | 23 +- config/routes/api_station.php | 25 -- config/routes/stations.php | 11 - .../vue/components/Stations/Automation.vue | 159 --------- .../Stations/Playlists/EditModal.vue | 2 - .../Stations/Playlists/Form/BasicInfo.vue | 12 +- .../Stations/Reports/Performance.vue | 75 ----- frontend/vue/pages/Stations/Automation.js | 7 - .../vue/pages/Stations/Reports/Performance.js | 9 - frontend/webpack.config.js | 2 - .../Stations/Automation/GetSettingsAction.php | 24 -- .../Stations/Automation/PutSettingsAction.php | 35 -- .../Api/Stations/Automation/RunAction.php | 52 --- .../Stations/Reports/PerformanceAction.php | 105 ------ src/Controller/Stations/AutomationAction.php | 31 -- .../Stations/Reports/PerformanceAction.php | 30 -- .../Migration/Version20220610132810.php | 30 ++ src/Entity/Station.php | 40 --- src/Entity/StationPlaylist.php | 16 - src/Sync/Task/RunAutomatedAssignmentTask.php | 313 ------------------ tests/Functional/Api_Stations_ReportsCest.php | 29 -- tests/Functional/Station_ReportsCest.php | 5 - 24 files changed, 37 insertions(+), 1003 deletions(-) delete mode 100644 frontend/vue/components/Stations/Automation.vue delete mode 100644 frontend/vue/components/Stations/Reports/Performance.vue delete mode 100644 frontend/vue/pages/Stations/Automation.js delete mode 100644 frontend/vue/pages/Stations/Reports/Performance.js delete mode 100644 src/Controller/Api/Stations/Automation/GetSettingsAction.php delete mode 100644 src/Controller/Api/Stations/Automation/PutSettingsAction.php delete mode 100644 src/Controller/Api/Stations/Automation/RunAction.php delete mode 100644 src/Controller/Api/Stations/Reports/PerformanceAction.php delete mode 100644 src/Controller/Stations/AutomationAction.php delete mode 100644 src/Controller/Stations/Reports/PerformanceAction.php create mode 100644 src/Entity/Migration/Version20220610132810.php delete mode 100644 src/Sync/Task/RunAutomatedAssignmentTask.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b076029..84777325a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,9 @@ release channel, you can take advantage of these new features and fixes. ## Code Quality/Technical Changes -There have been no code quality/technical changes since the last stable release. +- Automated station playlist assignment (and the corresponding Song Performance Report) is being retired. Internally, + this functionality was not well-explained, and likely does not work the way station operators expect it to. With the + upcoming development of new, better reporting tools, this functionality will no longer be required. ## Bug Fixes diff --git a/config/events.php b/config/events.php index 70201ed5a..ae4bceb45 100644 --- a/config/events.php +++ b/config/events.php @@ -141,7 +141,6 @@ return function (CallableEventDispatcherInterface $dispatcher) { App\Sync\Task\RenewAcmeCertTask::class, App\Sync\Task\RotateLogsTask::class, App\Sync\Task\RunAnalyticsTask::class, - App\Sync\Task\RunAutomatedAssignmentTask::class, App\Sync\Task\RunBackupTask::class, App\Sync\Task\UpdateGeoLiteTask::class, App\Sync\Task\UpdateStorageLocationSizesTask::class, diff --git a/config/menus/station.php b/config/menus/station.php index 2e5bcc93a..50fb29120 100644 --- a/config/menus/station.php +++ b/config/menus/station.php @@ -119,21 +119,9 @@ return function (App\Event\BuildStationMenu $e) { 'playlists' => [ 'label' => __('Playlists'), 'icon' => 'queue_music', - 'items' => [ - 'playlists' => [ - 'label' => __('Playlists'), - 'url' => (string)$router->fromHere('stations:playlists:index'), - 'visible' => $backend->supportsMedia(), - 'permission' => StationPermissions::Media, - ], - 'automation' => [ - 'label' => __('Automated Assignment'), - 'class' => 'text-muted', - 'url' => (string)$router->fromHere('stations:automation:index'), - 'visible' => $backend->supportsMedia(), - 'permission' => StationPermissions::Automation, - ], - ], + 'url' => (string)$router->fromHere('stations:playlists:index'), + 'visible' => $backend->supportsMedia(), + 'permission' => StationPermissions::Media, ], 'podcasts' => [ @@ -200,11 +188,6 @@ return function (App\Event\BuildStationMenu $e) { 'label' => __('Song Playback Timeline'), 'url' => (string)$router->fromHere('stations:reports:timeline'), ], - 'reports_performance' => [ - 'label' => __('Song Listener Impact'), - 'url' => (string)$router->fromHere('stations:reports:performance'), - 'visible' => $backend->supportsMedia(), - ], 'reports_soundexchange' => [ 'label' => __('SoundExchange Royalties'), 'url' => (string)$router->fromHere('stations:reports:soundexchange'), diff --git a/config/routes/api_station.php b/config/routes/api_station.php index 9b1322dd0..aaa25aa30 100644 --- a/config/routes/api_station.php +++ b/config/routes/api_station.php @@ -13,26 +13,6 @@ return static function (RouteCollectorProxy $group) { ->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(StationPermissions::Automation, true)); - $group->get('/nowplaying', Controller\Api\NowPlayingAction::class . ':indexAction'); $group->map( @@ -517,11 +497,6 @@ return static function (RouteCollectorProxy $group) { } )->add(new Middleware\Permissions(StationPermissions::Broadcasting, true)); - $group->get( - '/performance', - Controller\Api\Stations\Reports\PerformanceAction::class - )->setName('api:stations:reports:performance'); - $group->get( '/overview/charts', Controller\Api\Stations\Reports\Overview\ChartsAction::class diff --git a/config/routes/stations.php b/config/routes/stations.php index 2335b3096..89bc45a36 100644 --- a/config/routes/stations.php +++ b/config/routes/stations.php @@ -20,12 +20,6 @@ return static function (RouteCollectorProxy $app) { } )->setName('stations:index:index'); - $group->get( - '/automation', - Controller\Stations\AutomationAction::class - )->setName('stations:automation:index') - ->add(new Middleware\Permissions(StationPermissions::Automation, true)); - $group->get('/bulk-media', Controller\Stations\BulkMediaAction::class) ->setName('stations:bulk-media') ->add(new Middleware\Permissions(StationPermissions::Media, true)); @@ -104,11 +98,6 @@ return static function (RouteCollectorProxy $app) { $group->get('/timeline', Controller\Stations\Reports\TimelineAction::class) ->setName('stations:reports:timeline'); - $group->get( - '/performance', - Controller\Stations\Reports\PerformanceAction::class - )->setName('stations:reports:performance'); - $group->get('/listeners', Controller\Stations\Reports\ListenersAction::class) ->setName('stations:reports:listeners'); diff --git a/frontend/vue/components/Stations/Automation.vue b/frontend/vue/components/Stations/Automation.vue deleted file mode 100644 index cb7f29fa5..000000000 --- a/frontend/vue/components/Stations/Automation.vue +++ /dev/null @@ -1,159 +0,0 @@ - - - diff --git a/frontend/vue/components/Stations/Playlists/EditModal.vue b/frontend/vue/components/Stations/Playlists/EditModal.vue index a40df4789..b2f3ceed5 100644 --- a/frontend/vue/components/Stations/Playlists/EditModal.vue +++ b/frontend/vue/components/Stations/Playlists/EditModal.vue @@ -52,7 +52,6 @@ export default { 'play_per_minutes': {}, 'play_per_hour_minute': {}, 'include_in_requests': {}, - 'include_in_automation': {}, 'avoid_duplicates': {}, 'backend_options': {}, 'schedule_items': { @@ -85,7 +84,6 @@ export default { 'play_per_minutes': 0, 'play_per_hour_minute': 0, 'include_in_requests': true, - 'include_in_automation': false, 'avoid_duplicates': true, 'backend_options': [], 'schedule_items': [] diff --git a/frontend/vue/components/Stations/Playlists/Form/BasicInfo.vue b/frontend/vue/components/Stations/Playlists/Form/BasicInfo.vue index 06e0be891..9d327ac0f 100644 --- a/frontend/vue/components/Stations/Playlists/Form/BasicInfo.vue +++ b/frontend/vue/components/Stations/Playlists/Form/BasicInfo.vue @@ -158,7 +158,7 @@ - + @@ -170,16 +170,6 @@ :state="props.state"> - - - - - diff --git a/frontend/vue/components/Stations/Reports/Performance.vue b/frontend/vue/components/Stations/Reports/Performance.vue deleted file mode 100644 index 62f2bb6ec..000000000 --- a/frontend/vue/components/Stations/Reports/Performance.vue +++ /dev/null @@ -1,75 +0,0 @@ - - - diff --git a/frontend/vue/pages/Stations/Automation.js b/frontend/vue/pages/Stations/Automation.js deleted file mode 100644 index cc9b81956..000000000 --- a/frontend/vue/pages/Stations/Automation.js +++ /dev/null @@ -1,7 +0,0 @@ -import initBase from '~/base.js'; - -import '~/vendor/bootstrapVue.js'; - -import Automation from '~/components/Stations/Automation.vue'; - -export default initBase(Automation); diff --git a/frontend/vue/pages/Stations/Reports/Performance.js b/frontend/vue/pages/Stations/Reports/Performance.js deleted file mode 100644 index f9ecd5d2b..000000000 --- a/frontend/vue/pages/Stations/Reports/Performance.js +++ /dev/null @@ -1,9 +0,0 @@ -import initBase - from '~/base.js'; - -import '~/vendor/bootstrapVue.js'; - -import Performance - from '~/components/Stations/Reports/Performance.vue'; - -export default initBase(Performance); diff --git a/frontend/webpack.config.js b/frontend/webpack.config.js index fabf89736..e87bcbb76 100644 --- a/frontend/webpack.config.js +++ b/frontend/webpack.config.js @@ -33,7 +33,6 @@ module.exports = { SetupRegister: '~/pages/Setup/Register.js', SetupSettings: '~/pages/Setup/Settings.js', SetupStation: '~/pages/Setup/Station.js', - StationsAutomation: '~/pages/Stations/Automation.js', StationsBulkMedia: '~/pages/Stations/BulkMedia.js', StationsFallback: '~/pages/Stations/Fallback.js', StationsHlsStreams: '~/pages/Stations/HlsStreams.js', @@ -51,7 +50,6 @@ module.exports = { StationsReportsListeners: '~/pages/Stations/Reports/Listeners.js', StationsReportsRequests: '~/pages/Stations/Reports/Requests.js', StationsReportsOverview: '~/pages/Stations/Reports/Overview.js', - StationsReportsPerformance: '~/pages/Stations/Reports/Performance.js', StationsReportsSoundExchange: '~/pages/Stations/Reports/SoundExchange.js', StationsReportsTimeline: '~/pages/Stations/Reports/Timeline.js', StationsSftpUsers: '~/pages/Stations/SftpUsers.js', diff --git a/src/Controller/Api/Stations/Automation/GetSettingsAction.php b/src/Controller/Api/Stations/Automation/GetSettingsAction.php deleted file mode 100644 index 339cb32dc..000000000 --- a/src/Controller/Api/Stations/Automation/GetSettingsAction.php +++ /dev/null @@ -1,24 +0,0 @@ -getStation(); - - return $response->withJson( - (array)$station->getAutomationSettings() - ); - } -} diff --git a/src/Controller/Api/Stations/Automation/PutSettingsAction.php b/src/Controller/Api/Stations/Automation/PutSettingsAction.php deleted file mode 100644 index 954c427d8..000000000 --- a/src/Controller/Api/Stations/Automation/PutSettingsAction.php +++ /dev/null @@ -1,35 +0,0 @@ -getStation(); - - $station = $this->em->refetch($station); - $station->setAutomationSettings((array)$request->getParsedBody()); - - $this->em->persist($station); - $this->em->flush(); - - return $response->withJson(Entity\Api\Status::updated()); - } -} diff --git a/src/Controller/Api/Stations/Automation/RunAction.php b/src/Controller/Api/Stations/Automation/RunAction.php deleted file mode 100644 index deafa5078..000000000 --- a/src/Controller/Api/Stations/Automation/RunAction.php +++ /dev/null @@ -1,52 +0,0 @@ -getStation(); - - try { - $this->syncTask->runStation($station, true); - return $response->withJson(Entity\Api\Status::success()); - } catch (Throwable $e) { - return $response->withStatus(400)->withJson(Entity\Api\Error::fromException($e)); - } - } -} diff --git a/src/Controller/Api/Stations/Reports/PerformanceAction.php b/src/Controller/Api/Stations/Reports/PerformanceAction.php deleted file mode 100644 index 1e08790eb..000000000 --- a/src/Controller/Api/Stations/Reports/PerformanceAction.php +++ /dev/null @@ -1,105 +0,0 @@ -getStation(); - - $automationConfig = (array)$station->getAutomationSettings(); - $thresholdDays = (int)($automationConfig['threshold_days'] - ?? RunAutomatedAssignmentTask::DEFAULT_THRESHOLD_DAYS); - - $reportData = $this->automationTask->generateReport($station, $thresholdDays); - - // Do not show songs that are not in playlists. - $reportData = array_filter( - $reportData, - static function ($media) { - return !(empty($media['playlists'])); - } - ); - - $queryParams = $request->getQueryParams(); - $format = $queryParams['format'] ?? 'json'; - - if ($format === 'csv') { - return $this->exportReportAsCsv( - $response, - $reportData, - $station->getShortName() . '_media_' . date('Ymd') . '.csv' - ); - } - - return Paginator::fromArray($reportData, $request)->write($response); - } - - /** - * @param Response $response - * @param mixed[] $reportData - * @param string $filename - */ - private function exportReportAsCsv( - Response $response, - array $reportData, - string $filename - ): ResponseInterface { - if (!($tempFile = tmpfile())) { - throw new \RuntimeException('Could not create temp file.'); - } - $csv = Writer::createFromStream($tempFile); - - $csv->insertOne( - [ - 'Song Title', - 'Song Artist', - 'Filename', - 'Length', - 'Current Playlist', - 'Delta Joins', - 'Delta Losses', - 'Delta Total', - 'Play Count', - 'Play Percentage', - 'Weighted Ratio', - ] - ); - - foreach ($reportData as $row) { - $csv->insertOne([ - $row['title'], - $row['artist'], - $row['path'], - $row['length'], - implode('/', $row['playlists']), - $row['delta_positive'], - $row['delta_negative'], - $row['delta_total'], - $row['num_plays'], - $row['percent_plays'] . '%', - $row['ratio'], - ]); - } - - return $response->withFileDownload($tempFile, $filename, 'text/csv'); - } -} diff --git a/src/Controller/Stations/AutomationAction.php b/src/Controller/Stations/AutomationAction.php deleted file mode 100644 index 5d169cb04..000000000 --- a/src/Controller/Stations/AutomationAction.php +++ /dev/null @@ -1,31 +0,0 @@ -getRouter(); - - return $request->getView()->renderVuePage( - response: $response, - component: 'Vue_StationsAutomation', - id: 'station-automation', - title: __('Automated Assignment'), - props: [ - 'settingsUrl' => (string)$router->fromHere('api:stations:automation:settings'), - 'runUrl' => (string)$router->fromHere('api:stations:automation:run'), - ], - ); - } -} diff --git a/src/Controller/Stations/Reports/PerformanceAction.php b/src/Controller/Stations/Reports/PerformanceAction.php deleted file mode 100644 index f77d4efd6..000000000 --- a/src/Controller/Stations/Reports/PerformanceAction.php +++ /dev/null @@ -1,30 +0,0 @@ -getRouter(); - - return $request->getView()->renderVuePage( - response: $response, - component: 'Vue_StationsReportsPerformance', - id: 'station-report-performance', - title: __('Song Listener Impact'), - props: [ - 'apiUrl' => (string)$router->fromHere('api:stations:reports:performance'), - ] - ); - } -} diff --git a/src/Entity/Migration/Version20220610132810.php b/src/Entity/Migration/Version20220610132810.php new file mode 100644 index 000000000..6d9801f18 --- /dev/null +++ b/src/Entity/Migration/Version20220610132810.php @@ -0,0 +1,30 @@ +addSql('ALTER TABLE station DROP automation_settings, DROP automation_timestamp'); + $this->addSql('ALTER TABLE station_playlists DROP include_in_automation'); + } + + public function down(Schema $schema): void + { + $this->addSql( + 'ALTER TABLE station ADD automation_settings LONGTEXT DEFAULT NULL COMMENT \'(DC2Type:json)\', ADD automation_timestamp INT DEFAULT NULL' + ); + $this->addSql('ALTER TABLE station_playlists ADD include_in_automation TINYINT(1) NOT NULL'); + } +} diff --git a/src/Entity/Station.php b/src/Entity/Station.php index 66efc76aa..3d0f62a5e 100644 --- a/src/Entity/Station.php +++ b/src/Entity/Station.php @@ -46,9 +46,6 @@ class Station implements Stringable, IdentifiableEntityInterface use Traits\HasAutoIncrementId; use Traits\TruncateStrings; - // Taxonomical groups for permission-based serialization. - public const GROUP_AUTOMATION = 'automation'; - #[ OA\Property(description: "The full display name of the station.", example: "AzuraTest Radio"), ORM\Column(length: 100, nullable: false), @@ -166,20 +163,6 @@ class Station implements Stringable, IdentifiableEntityInterface ] protected ?int $nowplaying_timestamp = null; - #[ - OA\Property(type: "array", items: new OA\Items()), - ORM\Column(type: 'json', nullable: true), - Serializer\Groups([self::GROUP_AUTOMATION, EntityGroupsInterface::GROUP_ALL]) - ] - protected ?array $automation_settings = null; - - #[ - ORM\Column(nullable: true), - Attributes\AuditIgnore, - Serializer\Groups([self::GROUP_AUTOMATION, EntityGroupsInterface::GROUP_ALL]) - ] - protected ?int $automation_timestamp = 0; - #[ OA\Property( description: "Whether listeners can request songs to play on this station.", @@ -767,29 +750,6 @@ class Station implements Stringable, IdentifiableEntityInterface $this->nowplaying_timestamp = $nowplaying_timestamp; } - /** - * @return mixed[]|null - */ - public function getAutomationSettings(): ?array - { - return $this->automation_settings; - } - - public function setAutomationSettings(array $automation_settings = null): void - { - $this->automation_settings = $automation_settings; - } - - public function getAutomationTimestamp(): ?int - { - return $this->automation_timestamp; - } - - public function setAutomationTimestamp(int $automation_timestamp = null): void - { - $this->automation_timestamp = $automation_timestamp; - } - public function getEnableRequests(): bool { return $this->enable_requests; diff --git a/src/Entity/StationPlaylist.php b/src/Entity/StationPlaylist.php index a26874156..345441c7b 100644 --- a/src/Entity/StationPlaylist.php +++ b/src/Entity/StationPlaylist.php @@ -150,12 +150,6 @@ class StationPlaylist implements ] protected bool $include_in_on_demand = false; - #[ - OA\Property(example: false), - ORM\Column - ] - protected bool $include_in_automation = false; - #[ OA\Property(example: "interrupt,loop_once,single_track,merge"), ORM\Column(length: 255, nullable: true) @@ -403,16 +397,6 @@ class StationPlaylist implements return ($this->is_enabled && $this->include_in_requests); } - public function getIncludeInAutomation(): bool - { - return $this->include_in_automation; - } - - public function setIncludeInAutomation(bool $include_in_automation): void - { - $this->include_in_automation = $include_in_automation; - } - public function getAvoidDuplicates(): bool { return $this->avoid_duplicates; diff --git a/src/Sync/Task/RunAutomatedAssignmentTask.php b/src/Sync/Task/RunAutomatedAssignmentTask.php deleted file mode 100644 index 3e0cc6320..000000000 --- a/src/Sync/Task/RunAutomatedAssignmentTask.php +++ /dev/null @@ -1,313 +0,0 @@ -iterateStations() as $station) { - try { - if ($this->runStation($station)) { - $this->logger->info('Automated assignment [' . $station->getName() . ']: Successfully run.'); - } else { - $this->logger->info('Automated assignment [' . $station->getName() . ']: Skipped.'); - } - } catch (Exception $e) { - $this->logger->error('Automated assignment [' . $station->getName() . ']: Error: ' . $e->getMessage()); - } - } - } - - public function runStation(Entity\Station $station, bool $force = false): bool - { - $settings = (array)$station->getAutomationSettings(); - - if (empty($settings) || !$settings['is_enabled']) { - return false; - } - - // Check whether assignment needs to be run. - $threshold_days = (int)$settings['threshold_days']; - $threshold = CarbonImmutable::now('UTC') - ->subDays($threshold_days) - ->getTimestamp(); - - if (!$force && $station->getAutomationTimestamp() >= $threshold) { - return false; - } // No error, but no need to run assignment. - - // Pull songs in current playlists, then clear those playlists. - $getSongsInPlaylistQuery = $this->em->createQuery( - <<<'DQL' - SELECT sm.id - FROM App\Entity\StationPlaylistMedia spm - JOIN spm.media sm - WHERE spm.playlist = :playlist - DQL - ); - - $mediaToUpdate = []; - $playlists = []; - - foreach ($station->getPlaylists() as $playlist) { - /** @var Entity\StationPlaylist $playlist */ - if ( - $playlist->getIsEnabled() - && Entity\Enums\PlaylistTypes::Standard === $playlist->getTypeEnum() - && $playlist->getIncludeInAutomation() - ) { - $playlists[] = $playlist->getId(); - - // Clear all related media. - $mediaInPlaylist = $getSongsInPlaylistQuery->setParameter('playlist', $playlist) - ->getArrayResult(); - - foreach ($mediaInPlaylist as $media) { - $mediaToUpdate[$media['id']] = [ - 'old_playlist_id' => $playlist->getId(), - 'new_playlist_id' => $playlist->getId(), - ]; - } - } - } - - if (0 === count($playlists)) { - throw new Exception('No playlists have automation enabled.'); - } - - // Generate the actual report for listenership. - $mediaReport = $this->generateReport($station, $threshold_days); - - // Remove songs that weren't already in auto-assigned playlists. - $mediaReport = array_filter( - $mediaReport, - static function ($media) use ($mediaToUpdate) { - return (isset($mediaToUpdate[$media['id']])); - } - ); - - // Place all songs with 0 plays back in their original playlists. - foreach ($mediaReport as $song_id => $media) { - if ($media['num_plays'] === 0) { - unset($mediaToUpdate[$media['id']], $mediaReport[$song_id]); - } - } - - // Sort songs by ratio descending. - uasort( - $mediaReport, - static function ($a_media, $b_media) { - return (int)$b_media['ratio'] <=> (int)$a_media['ratio']; - } - ); - - // Distribute media across the enabled playlists and assign media to playlist. - $numSongs = count($mediaReport); - $numPlaylists = count($playlists); - - $songsPerPlaylist = (int)floor($numSongs / $numPlaylists); - - $i = 0; - foreach ($playlists as $playlistId) { - if ($i === 0) { - $playlistNumSongs = $songsPerPlaylist + ($numSongs % $numPlaylists); - } else { - $playlistNumSongs = $songsPerPlaylist; - } - - foreach (array_slice($mediaReport, $i, $playlistNumSongs) as $media) { - $mediaToUpdate[$media['id']]['new_playlist_id'] = $playlistId; - } - - $i += $playlistNumSongs; - } - - // Update media playlist placement. - $updateMediaPlaylistQuery = $this->em->createQuery( - <<<'DQL' - UPDATE App\Entity\StationPlaylistMedia spm - SET spm.playlist_id = :new_playlist_id - WHERE spm.playlist_id = :old_playlist_id - AND spm.media_id = :media_id - DQL - ); - - foreach ($mediaToUpdate as $mediaId => $playlists) { - $updateMediaPlaylistQuery->setParameter('media_id', $mediaId) - ->setParameter('old_playlist_id', $playlists['old_playlist_id']) - ->setParameter('new_playlist_id', $playlists['new_playlist_id']) - ->execute(); - } - - $this->em->clear(); - - $station = $this->em->refetch($station); - $station->setAutomationTimestamp(time()); - - $this->em->persist($station); - $this->em->flush(); - - // Write new PLS playlist configuration. - $backend_adapter = $this->adapters->getBackendAdapter($station); - $backend_adapter->write($station); - - return true; - } - - /** - * @return mixed[] - */ - public function generateReport( - Entity\Station $station, - int $threshold_days = self::DEFAULT_THRESHOLD_DAYS - ): array { - $threshold = CarbonImmutable::now() - ->subDays($threshold_days) - ->getTimestamp(); - - // Pull all SongHistory data points. - $dataPointsRaw = $this->em->createQuery( - <<<'DQL' - SELECT sh.song_id, sh.timestamp_start, sh.delta_positive, sh.delta_negative, sh.listeners_start - FROM App\Entity\SongHistory sh - WHERE sh.station = :station - AND sh.timestamp_end != 0 - AND sh.timestamp_start >= :threshold - DQL - )->setParameter('station', $station) - ->setParameter('threshold', $threshold) - ->getArrayResult(); - - $total_plays = 0; - $data_points = []; - - foreach ($dataPointsRaw as $row) { - $total_plays++; - - if (!isset($data_points[$row['song_id']])) { - $data_points[$row['song_id']] = []; - } - - $data_points[$row['song_id']][] = $row; - } - - $mediaQuery = $this->em->createQuery( - <<<'DQL' - SELECT sm - FROM App\Entity\StationMedia sm - WHERE sm.storage_location = :storageLocation - ORDER BY sm.artist ASC, sm.title ASC - DQL - )->setParameter('storageLocation', $station->getMediaStorageLocation()); - - $iterator = ReadOnlyBatchIteratorAggregate::fromQuery($mediaQuery, 100); - $report = []; - - /** @var Entity\StationMedia $row */ - foreach ($iterator as $row) { - $songId = $row->getSongId(); - - $media = [ - 'id' => $row->getId(), - 'song_id' => $songId, - - 'title' => $row->getTitle(), - 'artist' => $row->getArtist(), - 'length_raw' => $row->getLength(), - 'length' => $row->getLengthText(), - 'path' => $row->getPath(), - - 'playlists' => [], - 'data_points' => [], - - 'num_plays' => 0, - 'percent_plays' => 0, - - 'delta_negative' => 0, - 'delta_positive' => 0, - 'delta_total' => 0, - - 'ratio' => 0, - ]; - - if ($row->getPlaylists()->count() > 0) { - /** @var Entity\StationPlaylistMedia $playlist_item */ - foreach ($row->getPlaylists() as $playlist_item) { - $media['playlists'][] = $playlist_item->getPlaylist()->getName(); - } - } - - if (isset($data_points[$songId])) { - $ratio_points = []; - - foreach ($data_points[$songId] as $data_row) { - $media['num_plays']++; - - $media['delta_positive'] += $data_row['delta_positive']; - $media['delta_negative'] -= $data_row['delta_negative']; - - /* - * The song ratio is determined by the total impact in listenership the song caused - * (both up and down) over its play time, divided by the number of listeners the song started - * with. Impacts are weighted higher for more significant percentage impacts up or down. - * - * i.e. - * 1 listener at start, gained 3 listeners => 3/1*100 = 300 - * 100 listeners at start, lost 15 listeners => -15/100*100 = -15 - */ - - $delta_total = $data_row['delta_positive'] - $data_row['delta_negative']; - $ratio_points[] = ($data_row['listeners_start'] == 0) - ? 0 - : ($delta_total / $data_row['listeners_start']) * 100; - } - - $media['delta_total'] = $media['delta_positive'] + $media['delta_negative']; - $media['percent_plays'] = round(($media['num_plays'] / $total_plays) * 100, 2); - - $media['ratio'] = round(array_sum($ratio_points) / count($ratio_points), 3); - } - - $report[$songId] = $media; - } - - return $report; - } -} diff --git a/tests/Functional/Api_Stations_ReportsCest.php b/tests/Functional/Api_Stations_ReportsCest.php index 9a2e0a998..bcb20921a 100644 --- a/tests/Functional/Api_Stations_ReportsCest.php +++ b/tests/Functional/Api_Stations_ReportsCest.php @@ -116,35 +116,6 @@ class Api_Stations_ReportsCest extends CestAbstract $this->testReportCsv($I, $requestUrl, $csvHeaders); } - /** - * @before setupComplete - * @before login - */ - public function downloadPerformanceReportCsv(\FunctionalTester $I): void - { - $I->wantTo('Download station song impact CSV via API.'); - - $station = $this->getTestStation(); - $uriBase = '/api/station/' . $station->getId(); - $requestUrl = $uriBase . '/reports/performance?format=csv'; - - $csvHeaders = [ - 'Song Title', - 'Song Artist', - 'Filename', - 'Length', - 'Current Playlist', - 'Delta Joins', - 'Delta Losses', - 'Delta Total', - 'Play Count', - 'Play Percentage', - 'Weighted Ratio', - ]; - - $this->testReportCsv($I, $requestUrl, $csvHeaders); - } - protected function testReportCsv( \FunctionalTester $I, string $url, diff --git a/tests/Functional/Station_ReportsCest.php b/tests/Functional/Station_ReportsCest.php index d8ac26d35..ffc06c08b 100644 --- a/tests/Functional/Station_ReportsCest.php +++ b/tests/Functional/Station_ReportsCest.php @@ -25,11 +25,6 @@ class Station_ReportsCest extends CestAbstract $I->seeResponseCodeIs(200); $I->see('Song Playback Timeline'); - $I->amOnPage('/station/' . $station_id . '/reports/performance'); - - $I->seeResponseCodeIs(200); - $I->see('Song Listener Impact'); - $I->amOnPage('/station/' . $station_id . '/reports/requests'); $I->seeResponseCodeIs(200);