diff --git a/templates/book_detail_modals.templ b/templates/book_detail_modals.templ
index 0585dfc..ad1b0b3 100644
--- a/templates/book_detail_modals.templ
+++ b/templates/book_detail_modals.templ
@@ -609,7 +609,19 @@ templ MetadataEditorModal(book handlers.MediaDetail) {
-
+
+
+
+
}
diff --git a/templates/book_detail_modals_templ.go b/templates/book_detail_modals_templ.go
index e3be864..21437c4 100644
--- a/templates/book_detail_modals_templ.go
+++ b/templates/book_detail_modals_templ.go
@@ -1336,7 +1336,15 @@ func MetadataEditorModal(book handlers.MediaDetail) templ.Component {
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
- templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 126, "\" readonly class=\"input opacity-60\">
")
+ templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 128, "Save")
if templ_7745c5c3_Err != nil {
return templ_7745c5c3_Err
}
diff --git a/web/src/book-detail.ts b/web/src/book-detail.ts
index ba2aa72..faaeeeb 100644
--- a/web/src/book-detail.ts
+++ b/web/src/book-detail.ts
@@ -97,6 +97,7 @@ interface MetadataEditorState {
coverFile: Blob | null;
coverAction: string;
saving: boolean;
+ rescanning: boolean;
userRating: number;
ratingHover: number;
ratingSaving: boolean;
@@ -110,6 +111,7 @@ interface MetadataEditorState {
generateCover(): Promise;
removeCover(): void;
saveMetadata(): Promise;
+ rescanBook(): Promise;
starFill(i: number): string;
ratingText(): string;
setRating(value: number): Promise;
@@ -149,6 +151,7 @@ Alpine.data("bookDetail", () => {
coverFile: null as Blob | null,
coverAction: "keep",
saving: false,
+ rescanning: false,
userRating: 0,
ratingHover: 0,
ratingSaving: false,
@@ -559,6 +562,31 @@ Alpine.data("bookDetail", () => {
}
},
+ async rescanBook() {
+ if (this.rescanning) return;
+ this.rescanning = true;
+ const mediaId = getMediaId();
+ try {
+ const resp = await fetch(`/api/media-items/${mediaId}/rescan`, {
+ method: "POST",
+ headers: { Authorization: getAuthHeader() },
+ });
+ if (!resp.ok) {
+ const err = await resp.json().catch(() => ({}));
+ throw new Error(err.error || "Failed to rescan book");
+ }
+ showToast("Book rescanned successfully", "success");
+ setTimeout(() => window.location.reload(), 500);
+ } catch (e) {
+ showToast(
+ e instanceof Error ? e.message : "Failed to rescan book",
+ "error",
+ );
+ } finally {
+ this.rescanning = false;
+ }
+ },
+
async searchEditorTags() {
const libraryId = document.body.getAttribute("data-library-id") || "";
if (!this.tagSearch || this.tagSearch.length < 2 || !libraryId) {