From 4ad6d5b1edab766ff08331e1fa759ba3afb64414 Mon Sep 17 00:00:00 2001 From: Franciskid Date: Sun, 26 Jul 2026 23:04:51 +0200 Subject: [PATCH] add a button to clear out finished share links The dashboard listed every share ever made, including ones revoked or expired months ago, and nothing ever pruned them. There is now a "Clean up finished links" button next to Refresh that runs a normal cleanup pass and then drops the records that are done with, reporting how many went. Only revoked, expired and failed records are removed. A link that can still be used is left alone, including a spent one-use link whose guest is still watching until it expires, since that record is Redeemed rather than finished. Folded into 1.0.3.0 rather than shipped as 1.0.3.1: the ceiling fix and this are going out as one replacement of that version. --- .../Api/ShareLinksController.cs | 15 ++++++++ .../Jellyfin.Plugin.ShareLinks.csproj | 6 ++-- .../Services/ShareLinkCleanupService.cs | 34 +++++++++++++++++++ .../Web/configPage.html | 27 +++++++++++++++ Jellyfin.Plugin.ShareLinks/meta.json | 4 +-- 5 files changed, 81 insertions(+), 5 deletions(-) diff --git a/Jellyfin.Plugin.ShareLinks/Api/ShareLinksController.cs b/Jellyfin.Plugin.ShareLinks/Api/ShareLinksController.cs index 096f24b..e63c564 100644 --- a/Jellyfin.Plugin.ShareLinks/Api/ShareLinksController.cs +++ b/Jellyfin.Plugin.ShareLinks/Api/ShareLinksController.cs @@ -269,6 +269,21 @@ public sealed class ShareLinksController : ControllerBase return Ok(ToDto(record)); } + /// Removes revoked, expired and failed share links from the store. + [HttpPost("Admin/Cleanup")] + [Authorize(AuthenticationSchemes = "CustomAuthentication")] + public async Task Cleanup(CancellationToken cancellationToken) + { + SetNoStoreHeaders(); + if (!User.IsInRole("Administrator")) + { + return Forbid(); + } + + var removed = await _cleanupService.PurgeFinishedAsync(cancellationToken).ConfigureAwait(false); + return Ok(new { removed }); + } + /// Returns the guest session state for the current authenticated user. [HttpGet("GuestState")] [Authorize(AuthenticationSchemes = "CustomAuthentication")] diff --git a/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj b/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj index c3e6ad3..672176a 100644 --- a/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj +++ b/Jellyfin.Plugin.ShareLinks/Jellyfin.Plugin.ShareLinks.csproj @@ -6,9 +6,9 @@ latest Jellyfin.Plugin.ShareLinks Jellyfin.Plugin.ShareLinks - 1.0.3.1 - 1.0.3.1 - 1.0.3.1 + 1.0.3.0 + 1.0.3.0 + 1.0.3.0 true false disable diff --git a/Jellyfin.Plugin.ShareLinks/Services/ShareLinkCleanupService.cs b/Jellyfin.Plugin.ShareLinks/Services/ShareLinkCleanupService.cs index b8a587c..04ca99e 100644 --- a/Jellyfin.Plugin.ShareLinks/Services/ShareLinkCleanupService.cs +++ b/Jellyfin.Plugin.ShareLinks/Services/ShareLinkCleanupService.cs @@ -75,6 +75,40 @@ public sealed class ShareLinkCleanupService : IShareLinkCleanupService await CleanupRecordInternalAsync(record, records, force, cancellationToken).ConfigureAwait(false); } + /// + /// Runs a normal cleanup pass and then drops every record that is finished + /// with, so the admin list does not grow forever. A link that can still be + /// used is never removed, and neither is one whose guest is still watching. + /// + public async Task PurgeFinishedAsync(CancellationToken cancellationToken) + { + // Teardown first: this expires anything past its date and removes the guest + // account and tags, so nothing is dropped from the store while it still has + // state hanging off it. + await CleanupAsync(cancellationToken).ConfigureAwait(false); + + var records = await _store.ListAsync(cancellationToken).ConfigureAwait(false); + var removed = 0; + foreach (var record in records) + { + cancellationToken.ThrowIfCancellationRequested(); + if (record.Status is not (ShareLinkStatus.Expired or ShareLinkStatus.Revoked or ShareLinkStatus.Failed)) + { + continue; + } + + await _store.DeleteAsync(record.Id, cancellationToken).ConfigureAwait(false); + removed++; + } + + if (removed > 0) + { + _logger.LogInformation("ShareLinks: purged {Count} finished share-link record(s).", removed); + } + + return removed; + } + private async Task CleanupRecordInternalAsync( ShareLinkRecord record, IReadOnlyList allRecords, diff --git a/Jellyfin.Plugin.ShareLinks/Web/configPage.html b/Jellyfin.Plugin.ShareLinks/Web/configPage.html index a69f826..e4a1e62 100644 --- a/Jellyfin.Plugin.ShareLinks/Web/configPage.html +++ b/Jellyfin.Plugin.ShareLinks/Web/configPage.html @@ -173,6 +173,9 @@ + Loading…
@@ -426,6 +429,30 @@ document.querySelector('#RefreshLinks').addEventListener('click', function () { loadLinks(); }); + + document.querySelector('#CleanupLinks').addEventListener('click', function () { + var button = page.querySelector('#CleanupLinks'); + var status = page.querySelector('#LinksStatus'); + button.disabled = true; + status.textContent = 'Cleaning up…'; + ApiClient.ajax({ + type: 'POST', + url: ApiClient.getUrl('ShareLinks/Admin/Cleanup'), + dataType: 'json' + }).then(function (result) { + var removed = result && (result.removed !== undefined ? result.removed : result.Removed); + removed = parseInt(removed, 10) || 0; + return loadLinks().then(function () { + page.querySelector('#LinksStatus').textContent = removed === 1 + ? '1 finished link removed.' + : removed + ' finished links removed.'; + }); + }).catch(function () { + status.textContent = 'Cleanup failed.'; + }).finally(function () { + button.disabled = false; + }); + }); })();
diff --git a/Jellyfin.Plugin.ShareLinks/meta.json b/Jellyfin.Plugin.ShareLinks/meta.json index 8e8a060..fbeaa2e 100644 --- a/Jellyfin.Plugin.ShareLinks/meta.json +++ b/Jellyfin.Plugin.ShareLinks/meta.json @@ -1,12 +1,12 @@ { "guid": "68540b76-ee74-436d-85ff-2abc884bbea6", "name": "ShareLinks", - "version": "1.0.3.1", + "version": "1.0.3.0", "targetAbi": "10.11.0.0", "framework": "net9.0", "owner": "Franciskid", "overview": "Secure expiring guest-share links for Jellyfin items.", "description": "Adds secure, expiring share links for Jellyfin items with JSON-backed storage, token hashing, and cleanup scaffolding.", "category": "General", - "timestamp": "2026-07-26T20:00:00.0000000Z" + "timestamp": "2026-07-26T20:15:00.0000000Z" }