diff --git a/db/migrations/20240909114945_add_folder_filter_to_playlist_table_migration.php b/db/migrations/20240909114945_add_folder_filter_to_playlist_table_migration.php new file mode 100644 index 0000000000..20725164dc --- /dev/null +++ b/db/migrations/20240909114945_add_folder_filter_to_playlist_table_migration.php @@ -0,0 +1,38 @@ +. + */ + +use Phinx\Migration\AbstractMigration; + +/** + * Add a new connector (Open Weather Map) to connectors table + * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace + */ +class AddFolderFilterToPlaylistTableMigration extends AbstractMigration +{ + public function change() + { + $this->table('playlist') + ->addColumn('filterFolderId', 'integer', ['after' => 'filterMediaTagsLogicalOperator', 'default' => null, + 'null' => true]) + ->save(); + } +} diff --git a/lib/Controller/Playlist.php b/lib/Controller/Playlist.php index f05ceb164d..92bbacc53d 100644 --- a/lib/Controller/Playlist.php +++ b/lib/Controller/Playlist.php @@ -703,16 +703,17 @@ public function add(Request $request, Response $response) $playlist->updateTagLinks($tags); } - // Do we have a tag or name filter? + // Do we have a tag, name or folder filter? $nameFilter = $sanitizedParams->getString('filterMediaName'); $nameFilterLogicalOperator = $sanitizedParams->getString('logicalOperatorName'); $tagFilter = $this->getUser()->featureEnabled('tag.tagging') ? $sanitizedParams->getString('filterMediaTag') : null; $logicalOperator = $this->getUser()->featureEnabled('tag.tagging') ? $sanitizedParams->getString('logicalOperator') : 'OR'; $exactTags = $this->getUser()->featureEnabled('tag.tagging') ? $sanitizedParams->getCheckbox('exactTags') : 0; + $folderIdFilter = $this->getUser()->featureEnabled('folder.view') ? $sanitizedParams->getInt('filterFolderId') : null; // Capture these as dynamic filter criteria if ($playlist->isDynamic === 1) { - if (empty($nameFilter) && empty($tagFilter)) { + if (empty($nameFilter) && empty($tagFilter) && empty($folderIdFilter)) { throw new InvalidArgumentException(__('No filters have been set for this dynamic Playlist, please click the Filters tab to define')); } $playlist->filterMediaName = $nameFilter; @@ -722,18 +723,24 @@ public function add(Request $request, Response $response) $playlist->filterExactTags = $exactTags; $playlist->filterMediaTagsLogicalOperator = $logicalOperator; } + + if ($this->getUser()->featureEnabled('folder.view')) { + $playlist->filterFolderId = $folderIdFilter; + } + $playlist->maxNumberOfItems = $sanitizedParams->getInt('maxNumberOfItems', ['default' => $this->getConfig()->getSetting('DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER')]); } $playlist->save(); // Should we assign any existing media - if (!empty($nameFilter) || !empty($tagFilter)) { + if (!empty($nameFilter) || !empty($tagFilter) || !empty($folderIdFilter)) { $media = $this->mediaFactory->query( null, [ 'name' => $nameFilter, 'tags' => $tagFilter, + 'folderId' => $folderIdFilter, 'assignable' => 1, 'exactTags' => $exactTags, 'logicalOperator' => $logicalOperator, @@ -960,17 +967,26 @@ public function edit(Request $request, Response $response, $id) // Do we have a tag or name filter? // Capture these as dynamic filter criteria if ($playlist->isDynamic === 1) { - if (empty($sanitizedParams->getString('filterMediaName')) && empty($sanitizedParams->getString('filterMediaTag'))) { + $filterMediaName = $sanitizedParams->getString('filterMediaName'); + $filterMediaTag = $sanitizedParams->getString('filterMediaTag'); + $filterFolderId = $sanitizedParams->getString('filterFolderId'); + + if (empty($filterMediaName) && empty($filterMediaTag) && empty($filterFolderId)) { throw new InvalidArgumentException(__('No filters have been set for this dynamic Playlist, please click the Filters tab to define')); } - $playlist->filterMediaName = $sanitizedParams->getString('filterMediaName'); + $playlist->filterMediaName = $filterMediaName; $playlist->filterMediaNameLogicalOperator = $sanitizedParams->getString('logicalOperatorName'); if ($this->getUser()->featureEnabled('tag.tagging')) { - $playlist->filterMediaTags = $sanitizedParams->getString('filterMediaTag'); + $playlist->filterMediaTags = $filterMediaTag; $playlist->filterExactTags = $sanitizedParams->getCheckbox('exactTags'); $playlist->filterMediaTagsLogicalOperator = $sanitizedParams->getString('logicalOperator'); } + + if ($this->getUser()->featureEnabled('folder.view')) { + $playlist->filterFolderId = $filterFolderId; + } + $playlist->maxNumberOfItems = $sanitizedParams->getInt('maxNumberOfItems'); } diff --git a/lib/Entity/Playlist.php b/lib/Entity/Playlist.php index f11175b5a3..b0b9e25919 100644 --- a/lib/Entity/Playlist.php +++ b/lib/Entity/Playlist.php @@ -110,6 +110,12 @@ class Playlist implements \JsonSerializable */ public $filterMediaTagsLogicalOperator; + /** + * @SWG\Property(description="The ID of the folder to filter media items by") + * @var int + */ + public $filterFolderId; + /** * @SWG\Property(description="Maximum number of Media items matching dynamic Playlist filters") * @var int @@ -786,8 +792,8 @@ private function add() $time = Carbon::now()->format(DateFormatHelper::getSystemFormat()); $sql = ' - INSERT INTO `playlist` (`name`, `ownerId`, `regionId`, `isDynamic`, `filterMediaName`, `filterMediaNameLogicalOperator`, `filterMediaTags`, `filterExactTags`, `filterMediaTagsLogicalOperator`, `maxNumberOfItems`, `createdDt`, `modifiedDt`, `requiresDurationUpdate`, `enableStat`, `folderId`, `permissionsFolderId`) - VALUES (:name, :ownerId, :regionId, :isDynamic, :filterMediaName, :filterMediaNameLogicalOperator, :filterMediaTags, :filterExactTags, :filterMediaTagsLogicalOperator, :maxNumberOfItems, :createdDt, :modifiedDt, :requiresDurationUpdate, :enableStat, :folderId, :permissionsFolderId) + INSERT INTO `playlist` (`name`, `ownerId`, `regionId`, `isDynamic`, `filterMediaName`, `filterMediaNameLogicalOperator`, `filterMediaTags`, `filterExactTags`, `filterMediaTagsLogicalOperator`, `filterFolderId`, `maxNumberOfItems`, `createdDt`, `modifiedDt`, `requiresDurationUpdate`, `enableStat`, `folderId`, `permissionsFolderId`) + VALUES (:name, :ownerId, :regionId, :isDynamic, :filterMediaName, :filterMediaNameLogicalOperator, :filterMediaTags, :filterExactTags, :filterMediaTagsLogicalOperator, :filterFolderId, :maxNumberOfItems, :createdDt, :modifiedDt, :requiresDurationUpdate, :enableStat, :folderId, :permissionsFolderId) '; $this->playlistId = $this->getStore()->insert($sql, array( 'name' => $this->name, @@ -799,6 +805,7 @@ private function add() 'filterMediaTags' => $this->filterMediaTags, 'filterExactTags' => $this->filterExactTags ?? 0, 'filterMediaTagsLogicalOperator' => $this->filterMediaTagsLogicalOperator ?? 'OR', + 'filterFolderId' => $this->filterFolderId, 'maxNumberOfItems' => $this->isDynamic == 0 ? null : $this->maxNumberOfItems, 'createdDt' => $time, 'modifiedDt' => $time, @@ -835,6 +842,7 @@ private function update() `filterMediaTags` = :filterMediaTags, `filterExactTags` = :filterExactTags, `filterMediaTagsLogicalOperator` = :filterMediaTagsLogicalOperator, + `filterFolderId` = :filterFolderId, `maxNumberOfItems` = :maxNumberOfItems, `requiresDurationUpdate` = :requiresDurationUpdate, `enableStat` = :enableStat, @@ -855,6 +863,7 @@ private function update() 'filterMediaTags' => $this->filterMediaTags, 'filterExactTags' => $this->filterExactTags ?? 0, 'filterMediaTagsLogicalOperator' => $this->filterMediaTagsLogicalOperator ?? 'OR', + 'filterFolderId' => $this->filterFolderId, 'maxNumberOfItems' => $this->maxNumberOfItems, 'modifiedDt' => Carbon::now()->format(DateFormatHelper::getSystemFormat()), 'requiresDurationUpdate' => $this->requiresDurationUpdate, diff --git a/lib/Factory/PlaylistFactory.php b/lib/Factory/PlaylistFactory.php index e98d0430d7..03a41a3f34 100644 --- a/lib/Factory/PlaylistFactory.php +++ b/lib/Factory/PlaylistFactory.php @@ -196,6 +196,7 @@ public function query($sortOrder = null, $filterBy = []) `playlist`.filterMediaTags, `playlist`.filterExactTags, `playlist`.filterMediaTagsLogicalOperator, + `playlist`.filterFolderId, `playlist`.maxNumberOfItems, `playlist`.requiresDurationUpdate, `playlist`.enableStat, @@ -458,6 +459,11 @@ public function query($sortOrder = null, $filterBy = []) $params['mediaLike'] = '%' . $parsedFilter->getString('mediaLike') . '%'; } + if ($parsedFilter->getInt('filterFolderId') !== null) { + $body .= " AND `playlist`.filterFolderId = :filterFolderId "; + $params['filterFolderId'] = $parsedFilter->getInt('filterFolderId'); + } + if ($parsedFilter->getInt('folderId') !== null) { $body .= " AND `playlist`.folderId = :folderId "; $params['folderId'] = $parsedFilter->getInt('folderId'); diff --git a/lib/XTR/DynamicPlaylistSyncTask.php b/lib/XTR/DynamicPlaylistSyncTask.php index b3cba443eb..ecc1230d9e 100644 --- a/lib/XTR/DynamicPlaylistSyncTask.php +++ b/lib/XTR/DynamicPlaylistSyncTask.php @@ -118,7 +118,7 @@ public function run() $this->log->debug('Assessing Playlist: ' . $playlist->name); - if (empty($playlist->filterMediaName) && empty($playlist->filterMediaTags)) { + 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. @@ -144,6 +144,7 @@ public function run() 'tags' => $playlist->filterMediaTags, 'exactTags' => $playlist->filterExactTags, 'logicalOperator' => $playlist->filterMediaTagsLogicalOperator, + 'folderId' => !empty($playlist->filterFolderId) ? $playlist->filterFolderId : null, 'userCheckUserId' => $playlist->getOwnerId(), 'start' => 0, 'length' => $playlist->maxNumberOfItems diff --git a/views/playlist-form-add.twig b/views/playlist-form-add.twig index c928cd03f6..f81b6787ff 100644 --- a/views/playlist-form-add.twig +++ b/views/playlist-form-add.twig @@ -167,6 +167,24 @@ {{ forms.inputWithTags("filterMediaTag", title, "", helpText, "", null, null, "exactTags", exactTagTitle, logicalOperatorTitle) }} {% endif %} + {% if currentUser.featureEnabled("folder.view") %} + {% set title %}{% trans "Folder Filter" %}{% endset %} + {% set helpText %}{% trans "Select a folder to filter the media items." %}{% endset %} + {% set attributes = [ + { name: "data-width", value: "100%" }, + { name: "data-search-url", value: url_for("folders.search") }, + { name: "data-filter-options", value: '{"gridView":1}' }, + { name: "data-search-term", value: "folderName" }, + { name: "data-id-property", value: "folderId" }, + { name: "data-text-property", value: "text" }, + { name: "data-initial-key", value: "folderId" }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" } + ] %} + {{ forms.dropdown("filterFolderId", "single", title, "", null, "", "", helpText, "pagedSelect", "", "", "", attributes) }} + {% endif %} + {% set title %}{% trans "Max number of Items" %}{% endset %} {% set helpText %}{% trans "The upper limit on number of Media items that can be dynamically assigned to this Playlist" %}{% endset %} {{ forms.number("maxNumberOfItems", title, settings.DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER, helpText, 'dynamic-message', '', '', settings.DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT) }} diff --git a/views/playlist-form-edit.twig b/views/playlist-form-edit.twig index b24de7f013..82e812a706 100644 --- a/views/playlist-form-edit.twig +++ b/views/playlist-form-edit.twig @@ -164,6 +164,25 @@ {{ forms.inputWithTags("filterMediaTag", title, playlist.filterMediaTags, helpText, "", null, null, "exactTags", exactTagTitle, logicalOperatorTitle, playlist.filterExactTags, playlist.filterMediaTagsLogicalOperator) }} {% endif %} + {% if currentUser.featureEnabled("folder.view") %} + {% set title %}{% trans "Folder Filter" %}{% endset %} + {% set helpText %}{% trans "Select a folder to filter the media items." %}{% endset %} + {% set attributes = [ + { name: "data-width", value: "100%" }, + { name: "data-search-url", value: url_for("folders.search") }, + { name: "data-filter-options", value: '{"gridView":1}' }, + { name: "data-search-term", value: "folderName" }, + { name: "data-id-property", value: "folderId" }, + { name: "data-text-property", value: "text" }, + { name: "data-initial-key", value: "folderId" }, + { name: "data-initial-value", value: playlist.filterFolderId }, + { name: "data-allow-clear", value: "true" }, + { name: "data-placeholder--id", value: null }, + { name: "data-placeholder--value", value: "" } + ] %} + {{ forms.dropdown("filterFolderId", "single", title, playlist.filterFolderId, null, "", "", helpText, "pagedSelect", "", "", "", attributes) }} + {% endif %} + {% set title %}{% trans "Max number of Items" %}{% endset %} {% set helpText %}{% trans "The upper limit on number of Media items that can be dynamically assigned to this Playlist" %}{% endset %} {{ forms.number("maxNumberOfItems", title, playlist.maxNumberOfItems, helpText, 'dynamic-message', '', '', settings.DEFAULT_DYNAMIC_PLAYLIST_MAXNUMBER_LIMIT) }} diff --git a/views/playlist-page.twig b/views/playlist-page.twig index 5ab5b427f1..6a096e3127 100644 --- a/views/playlist-page.twig +++ b/views/playlist-page.twig @@ -303,6 +303,7 @@ var exactTags; var logicalOperator; var logicalOperatorName; + var filterFolderId; function playlistEditorFormOpen(formData) { @@ -320,7 +321,7 @@ playlistFormPopulateMediaTable(dialog); }, 500)); - $(dialog).find("input[name=filterMediaTag], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName]").on("change", function () { + $(dialog).find("input[name=filterMediaTag], input[name=exactTags], select[name=logicalOperator], select[name=logicalOperatorName], select[name=filterFolderId]").on("change", function () { playlistFormPopulateMediaTable(dialog); }); @@ -394,8 +395,9 @@ exactTags = $(dialog).find("input[name=exactTags]").is(':checked') logicalOperator = $(dialog).find("select[name=logicalOperator]").val(); logicalOperatorName = $(dialog).find("select[name=logicalOperatorName]").val(); + filterFolderId = $(dialog).find("select[name=filterFolderId]").val() ?? ""; - if (nameFilter === "" && tagFilter === "") { + if (nameFilter === "" && tagFilter === "" && filterFolderId === "") { if (mediaTable != null) { mediaTable.destroy(); mediaTable = null; @@ -425,6 +427,7 @@ { media: nameFilter, tags: tagFilter, + folderId: filterFolderId, assignable: 1, exactTags: exactTags, logicalOperator: logicalOperator,