Files
Cloud-CMS/lib/XTR/DynamicPlaylistSyncTask.php
Matt Batchelder 05ce0da296 Initial Upload
2025-12-02 10:32:59 -05:00

331 lines
14 KiB
PHP

<?php
/*
* Copyright (C) 2023 Xibo Signage Ltd
*
* Xibo - Digital Signage - https://xibosignage.com
*
* This file is part of Xibo.
*
* Xibo is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* any later version.
*
* Xibo is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Xibo. If not, see <http://www.gnu.org/licenses/>.
*/
namespace Xibo\XTR;
use Carbon\Carbon;
use Xibo\Entity\Media;
use Xibo\Entity\Playlist;
use Xibo\Entity\Task;
use Xibo\Factory\MediaFactory;
use Xibo\Factory\ModuleFactory;
use Xibo\Factory\PlaylistFactory;
use Xibo\Factory\WidgetFactory;
use Xibo\Helper\DateFormatHelper;
use Xibo\Storage\StorageServiceInterface;
use Xibo\Support\Exception\GeneralException;
use Xibo\Support\Exception\NotFoundException;
/**
* Class DynamicPlaylistSyncTask
* @package Xibo\XTR
*
* Keep dynamic Playlists in sync with changes to the Media table.
*/
class DynamicPlaylistSyncTask implements TaskInterface
{
use TaskTrait;
/** @var StorageServiceInterface */
private $store;
/** @var PlaylistFactory */
private $playlistFactory;
/** @var MediaFactory */
private $mediaFactory;
/** @var ModuleFactory */
private $moduleFactory;
/** @var WidgetFactory */
private $widgetFactory;
/** @inheritdoc */
public function setFactories($container)
{
$this->store = $container->get('store');
$this->playlistFactory = $container->get('playlistFactory');
$this->mediaFactory = $container->get('mediaFactory');
$this->moduleFactory = $container->get('moduleFactory');
$this->widgetFactory = $container->get('widgetFactory');
return $this;
}
/** @inheritdoc */
public function run()
{
// If we're in the error state, then always run, otherwise check the dates we modified various triggers
if ($this->getTask()->lastRunStatus !== Task::$STATUS_ERROR) {
// Run a little query to get the last modified date from the media table
$lastMediaUpdate = $this->store->select('
SELECT MAX(modifiedDt) AS modifiedDt
FROM `media`
WHERE `type` <> \'module\' AND `type` <> \'genericfile\'
', [])[0]['modifiedDt'];
$lastPlaylistUpdate = $this->store->select('
SELECT MAX(modifiedDt) AS modifiedDt
FROM `playlist`
', [])[0]['modifiedDt'];
if (empty($lastMediaUpdate) || empty($lastPlaylistUpdate)) {
$this->appendRunMessage('No library media or Playlists to assess');
return;
}
$this->log->debug('Last media updated date is ' . $lastMediaUpdate);
$this->log->debug('Last playlist updated date is ' . $lastPlaylistUpdate);
$lastMediaUpdate = Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $lastMediaUpdate);
$lastPlaylistUpdate = Carbon::createFromFormat(DateFormatHelper::getSystemFormat(), $lastPlaylistUpdate);
$lastTaskRun = Carbon::createFromTimestamp($this->getTask()->lastRunDt);
if ($lastMediaUpdate->lessThanOrEqualTo($lastTaskRun)
&& $lastPlaylistUpdate->lessThanOrEqualTo($lastTaskRun))
{
$this->appendRunMessage('No library media/playlist updates since we last ran');
return;
}
}
$count = 0;
// Get all Dynamic Playlists
foreach ($this->playlistFactory->query(null, ['isDynamic' => 1]) as $playlist) {
try {
// We want to detect any differences in what should be assigned to this Playlist.
$playlist->load(['checkDisplayOrder' => true]);
$this->log->debug('Assessing Playlist: ' . $playlist->name);
if (empty($playlist->filterMediaName) && empty($playlist->filterMediaTags) && empty($playlist->filterFolderId)) {
// if this Dynamic Playlist was populated will all Media in the system
// before we introduced measures against it, we need to go through and unassign all Widgets from it.
// if it is fresh Playlist added recently, it will not have any Widgets on it with empty filters.
if (!empty($playlist->widgets)) {
foreach ($playlist->widgets as $widget) {
$playlist->deleteWidget($widget);
}
}
$this->log->debug(sprintf(
'Dynamic Playlist ID %d , with no filters set, skipping.',
$playlist->playlistId
));
continue;
}
// Query for media which would be assigned to this Playlist and see if there are any differences
$media = [];
$mediaIds = [];
$displayOrder = [];
foreach ($this->mediaFactory->query(null, [
'name' => $playlist->filterMediaName,
'logicalOperatorName' => $playlist->filterMediaNameLogicalOperator,
'tags' => $playlist->filterMediaTags,
'exactTags' => $playlist->filterExactTags,
'logicalOperator' => $playlist->filterMediaTagsLogicalOperator,
'folderId' => !empty($playlist->filterFolderId) ? $playlist->filterFolderId : null,
'userCheckUserId' => $playlist->getOwnerId(),
'start' => 0,
'length' => $playlist->maxNumberOfItems
]) as $index => $item) {
$media[$item->mediaId] = $item;
$mediaIds[] = $item->mediaId;
// store the expected display order
$displayOrder[$item->mediaId] = $index + 1;
}
// Work out if the set of widgets is different or not.
// This is only the first loose check
$different = (count($playlist->widgets) !== count($media));
$this->log->debug('There are ' . count($media) . ' that should be assigned and '
. count($playlist->widgets) . ' currently assigned with max number of items set to '
. $playlist->maxNumberOfItems . ' First check difference is '
. var_export($different, true));
if (!$different) {
// Try a more complete check, using mediaIds
$compareMediaIds = $mediaIds;
// ordering should be the same, so the first time we get one out of order, we can stop
foreach ($playlist->widgets as $widget) {
try {
$widgetMediaId = $widget->getPrimaryMediaId();
if ($widgetMediaId !== $compareMediaIds[0]
|| $widget->duration !== $media[$widgetMediaId]->duration
) {
$different = true;
break;
}
} catch (NotFoundException $notFoundException) {
$this->log->error('Playlist ' . $playlist->getId()
. ' has a Widget without any associated media. widgetId = ' . $widget->getId());
// We ought to recalculate
$different = true;
break;
}
array_shift($compareMediaIds);
}
}
$this->log->debug('Second check difference is ' . var_export($different, true));
if ($different) {
// We will update this Playlist
$assignmentMade = false;
$count++;
// Remove the ones no-longer present, add the ones we're missing
// we don't delete and re-add the lot to avoid regenerating the widgetIds (makes stats harder to
// interpret)
foreach ($playlist->widgets as $widget) {
try {
$widgetMediaId = $widget->getPrimaryMediaId();
if (!in_array($widgetMediaId, $mediaIds)) {
$playlist->deleteWidget($widget);
} else {
// It's present in the array
// Check to see if the duration is different
if ($widget->duration !== $media[$widgetMediaId]->duration) {
// The media duration has changed, so update the widget
$widget->useDuration = 1;
$widget->duration = $media[$widgetMediaId]->duration;
$widget->calculatedDuration = $widget->duration;
$widget->save([
'saveWidgetOptions' => false,
'saveWidgetAudio' => false,
'saveWidgetMedia' => false,
'notify' => false,
'notifyPlaylists' => false,
'notifyDisplays' => false,
'audit' => true,
'alwaysUpdate' => true
]);
}
// Pop it off the list of ones to assign.
$mediaIds = array_diff($mediaIds, [$widgetMediaId]);
// We do want to save the Playlist here.
$assignmentMade = true;
}
} catch (NotFoundException) {
// Delete it
$playlist->deleteWidget($widget);
}
}
// Do we have any mediaId's left which should be assigned and aren't?
// Add the ones we have left
foreach ($media as $item) {
if (in_array($item->mediaId, $mediaIds)) {
if (count($playlist->widgets) >= $playlist->maxNumberOfItems) {
$this->log->debug(
sprintf(
'Dynamic Playlist ID %d, has reached the maximum number of items %d, finishing assignments',//phpcs:ignore
$playlist->playlistId,
$playlist->maxNumberOfItems
)
);
break;
}
$assignmentMade = true;
// make sure we pass the expected displayOrder for the new item we are about to add.
$this->createAndAssign($playlist, $item, $displayOrder[$item->mediaId]);
}
}
if ($assignmentMade) {
// We've made an assignment change, so audit this change
// don't audit any downstream save operations
$playlist->save([
'auditPlaylist' => true,
'audit' => false
]);
}
} else {
$this->log->debug('No differences detected');
}
} catch (GeneralException $exception) {
$this->log->debug($exception->getTraceAsString());
$this->log->error('Problem with PlaylistId: ' . $playlist->getId()
. ', e = ' . $exception->getMessage());
$this->appendRunMessage('Error with Playlist: ' . $playlist->name);
}
}
$this->appendRunMessage('Updated ' . $count . ' Playlists');
}
/**
* @param Playlist $playlist
* @param Media $media
* @param int $displayOrder
* @throws NotFoundException
*/
private function createAndAssign(Playlist $playlist, Media $media, int $displayOrder): void
{
$this->log->debug('Media Item needs to be assigned ' . $media->name . ' in sequence ' . $displayOrder);
// Create a module
try {
$module = $this->moduleFactory->getByType($media->mediaType);
} catch (NotFoundException) {
$this->log->error('createAndAssign: dynamic playlist matched missing module: ' . $media->mediaType);
return;
}
if ($module->assignable == 0) {
$this->log->error('createAndAssign: dynamic playlist matched unassignable media: ' . $media->mediaId);
return;
}
// Determine the duration
$mediaDuration = $media->duration;
if ($mediaDuration <= 0) {
$mediaDuration = $module->defaultDuration;
}
// Create a widget
$widget = $this->widgetFactory->create(
$playlist->getOwnerId(),
$playlist->playlistId,
$media->mediaType,
$mediaDuration,
$module->schemaVersion
);
$widget->useDuration = 1;
$widget->displayOrder = $displayOrder;
$widget->calculateDuration($module);
$widget->assignMedia($media->mediaId);
// Assign the widget to the playlist
// making sure we pass the displayOrder here, otherwise it would be added to the end of the array.
$playlist->assignWidget($widget, $displayOrder);
}
}