diff --git a/internal/handlers/media.go b/internal/handlers/media.go
index 5b4d677..b9e1992 100644
--- a/internal/handlers/media.go
+++ b/internal/handlers/media.go
@@ -1330,6 +1330,27 @@ func (mh *MediaHandler) UpdateMediaItem(c *echo.Context) error {
return c.JSON(http.StatusOK, item)
}
+// PurgeArchivedMediaItems handles POST /api/media-items/purge-archived
+// (admin only). Hard-deletes every archived item (files missing from disk for
+// 2+ scans) together with its reading history. The archive retention window
+// eventually does the same automatically; this is the manual bulk escape hatch.
+func (mh *MediaHandler) PurgeArchivedMediaItems(c *echo.Context) error {
+ user := MustGetAuthenticatedUser(c)
+
+ if user.Role != "admin" {
+ return c.JSON(http.StatusForbidden, map[string]string{"error": "admin access required"})
+ }
+
+ purged, err := mh.db.PurgeAllArchivedMediaItems(c.Request().Context())
+ if err != nil {
+ return c.JSON(http.StatusInternalServerError, map[string]string{"error": err.Error()})
+ }
+
+ return c.JSON(http.StatusOK, map[string]interface{}{
+ "purged": len(purged),
+ })
+}
+
// RescanMediaItem handles POST /api/media-items/:id/rescan (admin only)
func (mh *MediaHandler) RescanMediaItem(c *echo.Context) error {
user := MustGetAuthenticatedUser(c)
diff --git a/internal/router/frontend.go b/internal/router/frontend.go
index 265c1a8..b89306a 100644
--- a/internal/router/frontend.go
+++ b/internal/router/frontend.go
@@ -884,7 +884,8 @@ func registerFrontendRoutes(cfg *Config) {
}
var buf bytes.Buffer
- err = templates.AdminLibrary(user, libData, userData).Render(c.Request().Context(), &buf)
+ archivedCount, _ := cfg.Queries.CountArchivedMediaItems(c.Request().Context())
+ err = templates.AdminLibrary(user, libData, userData, archivedCount).Render(c.Request().Context(), &buf)
if err != nil {
return err
}
diff --git a/internal/router/media.go b/internal/router/media.go
index 439ff41..0ae238d 100644
--- a/internal/router/media.go
+++ b/internal/router/media.go
@@ -62,6 +62,7 @@ func registerMediaRoutes(cfg *Config) {
admin.PUT("/media-items/:id", cfg.MediaHandler.UpdateMediaItem)
admin.POST("/media-items/:id/rescan", cfg.MediaHandler.RescanMediaItem)
admin.DELETE("/media-items/:id", cfg.MediaHandler.DeleteMediaItem)
+ admin.POST("/media-items/purge-archived", cfg.MediaHandler.PurgeArchivedMediaItems)
// Shelf management (protected)
protected.POST("/devices/:id/shelves", cfg.MediaHandler.AddToShelf)
diff --git a/templates/admin_library.templ b/templates/admin_library.templ
index 033ad59..f1abfbd 100644
--- a/templates/admin_library.templ
+++ b/templates/admin_library.templ
@@ -1,6 +1,6 @@
package templates
-templ AdminLibrary(user User, libraries []LibraryData, users []User) {
+templ AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64) {
@@ -34,6 +34,23 @@ templ AdminLibrary(user User, libraries []LibraryData, users []User) {
+ if archivedCount > 0 {
+
+
+
+ Archived items: { archivedCount }
+
+
+ Files missing from disk for two consecutive scans. Reading history is kept
+ unless purged; items return automatically if their files come back.
+
+
+
+
+ }
@LibraryList(user, libraries, users)
diff --git a/templates/admin_library_templ.go b/templates/admin_library_templ.go
index bc03b1e..d50e7ba 100644
--- a/templates/admin_library_templ.go
+++ b/templates/admin_library_templ.go
@@ -8,7 +8,7 @@ package templates
import "github.com/a-h/templ"
import templruntime "github.com/a-h/templ/runtime"
-func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Component {
+func AdminLibrary(user User, libraries []LibraryData, users []User, archivedCount int64) templ.Component {
return templruntime.GeneratedTemplate(func(templ_7745c5c3_Input templruntime.GeneratedComponentInput) (templ_7745c5c3_Err error) {
templ_7745c5c3_W, ctx := templ_7745c5c3_Input.Writer, templ_7745c5c3_Input.Context
if templ_7745c5c3_CtxErr := ctx.Err(); templ_7745c5c3_CtxErr != nil {
@@ -53,7 +53,38 @@ func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Compon
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Create Library")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 4, "Create Library
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ if archivedCount > 0 {
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "Archived items: ")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ var templ_7745c5c3_Var2 string
+ templ_7745c5c3_Var2, templ_7745c5c3_Err = templ.JoinStringErrs(archivedCount)
+ if templ_7745c5c3_Err != nil {
+ return templ.Error{Err: templ_7745c5c3_Err, FileName: `templates/admin_library.templ`, Line: 41, Col: 40}
+ }
+ _, templ_7745c5c3_Err = templ_7745c5c3_Buffer.WriteString(templ.EscapeString(templ_7745c5c3_Var2))
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 6, "
Files missing from disk for two consecutive scans. Reading history is kept unless purged; items return automatically if their files come back.
")
+ if templ_7745c5c3_Err != nil {
+ return templ_7745c5c3_Err
+ }
+ }
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 8, "")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
@@ -61,7 +92,7 @@ func AdminLibrary(user User, libraries []LibraryData, users []User) templ.Compon
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 5, "
Create Library
Create Library
Edit Library
Edit Library
Browse Folders
Browse Folders
Delete Library
Delete Library
Are you sure you want to delete ? This will remove the library and all its folder mappings. Media files will not be deleted.
Are you sure you want to delete ? This will remove the library and all its folder mappings. Media files will not be deleted.
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 16, "Delete