Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
45d5b1f
Add Grimmory connect with per-media-type library refresh
benjitobz Sep 9, 2026
28b5d2f
Suppress unfixed Microsoft.Build.Tasks.Git audit advisory so restores…
benjitobz Sep 9, 2026
4f8cc83
Add opt-in Grimmory metadata and cover push with manual push dialog
benjitobz Sep 9, 2026
3a7a340
Detect Grimmory edits by watching sidecar files instead of polling
benjitobz Sep 9, 2026
91f946b
Give targets time to settle before forwarding Grimmory edits
benjitobz Sep 9, 2026
8fd1054
Consume a push's echo once so edits right after it still forward
benjitobz Sep 9, 2026
059e361
Record only metadata pushes for echo suppression
benjitobz Sep 9, 2026
eaca6f1
Decide push-echo suppression per sidecar event, not per batch
benjitobz Sep 9, 2026
a116094
Clean up empty subfolders of a populated root during scans
benjitobz Sep 9, 2026
033b540
Unlock pushed fields before writing so re-pushes take effect
benjitobz Sep 9, 2026
70bf545
Respect Grimmory field locks on pushes
benjitobz Sep 9, 2026
858c13f
Add Grimmory card to the quickstart connections
benjitobz Sep 9, 2026
40082c7
Let sibling Grimmory connections receive forwarded edits
benjitobz Sep 9, 2026
273fdf8
Record the Grimmory push before the metadata update
benjitobz Sep 9, 2026
7145232
Trim the Grimmory connector to the codebase conventions
benjitobz Sep 9, 2026
f84d9a4
Mirror Chaptarr-started pushes to the other library edit targets
benjitobz Sep 9, 2026
8b81944
Lock the authors, categories and cover Chaptarr pushes
benjitobz Sep 9, 2026
11a7e3b
Refresh and push for books that arrive via library scans
benjitobz Sep 10, 2026
97eee96
Merge remote-tracking branch 'upstream/develop' into sync/feature/gri…
benjitobz Sep 10, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 49 additions & 1 deletion frontend/src/Book/Details/BookDetails.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import React, { Component, Fragment } from 'react';
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs';
import AuthorHistoryTable from 'Author/History/AuthorHistoryTable';
import DeleteBookModal from 'Book/Delete/DeleteBookModal';
import EditBookModalConnector from 'Book/Edit/EditBookModalConnector';
import BookFileEditorTable from 'BookFile/Editor/BookFileEditorTable';
import GrimmoryPushModal from 'Grimmory/GrimmoryPushModal';
import IconButton from 'Components/Link/IconButton';
import LoadingIndicator from 'Components/Loading/LoadingIndicator';
import PageContent from 'Components/Page/PageContent';
Expand Down Expand Up @@ -36,6 +37,7 @@ class BookDetails extends Component {
this.state = {
isOrganizeModalOpen: false,
isRetagModalOpen: false,
isGrimmoryPushModalOpen: false,
isEditBookModalOpen: false,
isDeleteBookModalOpen: false,
selectedTabIndex: 0
Expand Down Expand Up @@ -74,6 +76,19 @@ class BookDetails extends Component {
this.setState({ isRetagModalOpen: false });
};

onGrimmoryPushPress = () => {
this.setState({ isGrimmoryPushModalOpen: true });
};

onGrimmoryPushModalClose = () => {
this.setState({ isGrimmoryPushModalOpen: false });
};

onGrimmoryPushConfirmed = (fields) => {
this.setState({ isGrimmoryPushModalOpen: false });
this.props.onPushToGrimmoryPress(fields);
};

onEditBookPress = () => {
this.setState({ isEditBookModalOpen: true });
};
Expand Down Expand Up @@ -117,6 +132,9 @@ class BookDetails extends Component {
nextBook,
hasBookNavigation,
isSearching,
showPushToGrimmory,
isPushingToGrimmory,
grimmoryPreview,
onRefreshPress,
onSearchPress,
statistics = {}
Expand All @@ -129,6 +147,7 @@ class BookDetails extends Component {
const {
isOrganizeModalOpen,
isRetagModalOpen,
isGrimmoryPushModalOpen,
isEditBookModalOpen,
isDeleteBookModalOpen,
selectedTabIndex
Expand Down Expand Up @@ -172,6 +191,23 @@ class BookDetails extends Component {

<PageToolbarSeparator />

{
showPushToGrimmory ?
<Fragment>
<PageToolbarButton
label={translate('GrimmoryPush')}
title={translate('PushChaptarrMetadataToGrimmory')}
iconName={icons.EXPORT}
isDisabled={!hasBookFiles}
isSpinning={isPushingToGrimmory}
onPress={this.onGrimmoryPushPress}
/>

<PageToolbarSeparator />
</Fragment> :
null
}

<PageToolbarButton
label={translate('Edit')}
iconName={icons.EDIT}
Expand Down Expand Up @@ -376,6 +412,14 @@ class BookDetails extends Component {
onModalClose={this.onDeleteBookModalClose}
/>

<GrimmoryPushModal
isOpen={isGrimmoryPushModalOpen}
bookCount={1}
previewValues={grimmoryPreview}
onPushPress={this.onGrimmoryPushConfirmed}
onModalClose={this.onGrimmoryPushModalClose}
/>

</PageContentBody>
</PageContent>
);
Expand Down Expand Up @@ -411,6 +455,10 @@ BookDetails.propTypes = {
nextBook: PropTypes.object,
hasBookNavigation: PropTypes.bool,
isSmallScreen: PropTypes.bool.isRequired,
showPushToGrimmory: PropTypes.bool,
isPushingToGrimmory: PropTypes.bool,
grimmoryPreview: PropTypes.object,
onPushToGrimmoryPress: PropTypes.func,
onMonitorTogglePress: PropTypes.func.isRequired,
onRefreshPress: PropTypes.func,
onSearchPress: PropTypes.func.isRequired
Expand Down
56 changes: 55 additions & 1 deletion frontend/src/Book/Details/BookDetailsConnector.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { executeCommand } from 'Store/Actions/commandActions';
import { clearEditions, fetchEditions } from 'Store/Actions/editionActions';
import { clearQueueDetails, fetchQueueDetails } from 'Store/Actions/queueActions';
import { cancelFetchReleases, clearReleases } from 'Store/Actions/releaseActions';
import { fetchNotifications } from 'Store/Actions/settingsActions';
import createAllAuthorSelector from 'Store/Selectors/createAllAuthorsSelector';
import createCommandsSelector from 'Store/Selectors/createCommandsSelector';
import createDimensionsSelector from 'Store/Selectors/createDimensionsSelector';
Expand All @@ -19,6 +20,34 @@ import { findCommand, isCommandExecuting } from 'Utilities/Command';
import { registerPagePopulator, unregisterPagePopulator } from 'Utilities/pagePopulator';
import BookDetails from './BookDetails';

function buildGrimmoryPreview(book, author, edition) {
const identifiers = [];

if (edition?.isbn13) {
identifiers.push(`isbn: ${edition.isbn13}`);
}

if (edition?.asin) {
identifiers.push(`asin: ${edition.asin}`);
}

if (edition?.foreignEditionId) {
identifiers.push(`goodreads: ${edition.foreignEditionId}`);
}

return {
title: edition?.title || book.title,
authors: author.authorName,
series: book.seriesTitle,
description: edition?.overview || book.overview,
publisher: edition?.publisher,
publisheddate: book.releaseDate,
language: edition?.language,
tags: (book.genres || []).join(', '),
identifiers: identifiers.join(', ')
};
}

const selectBookFiles = createSelector(
(state) => state.bookFiles,
(bookFiles) => {
Expand Down Expand Up @@ -51,7 +80,8 @@ function createMapStateToProps() {
createCommandsSelector(),
createUISettingsSelector(),
createDimensionsSelector(),
(bookId, bookFiles, books, editions, authors, commands, uiSettings, dimensions) => {
(state) => state.settings.notifications.items,
(bookId, bookFiles, books, editions, authors, commands, uiSettings, dimensions, notifications) => {
try {
const book = books.items.find((b) => b.id === bookId);

Expand Down Expand Up @@ -116,6 +146,15 @@ function createMapStateToProps() {
isRenamingAuthorCommand.body.authorIds.indexOf(author.id) > -1
);

const grimmoryPushCommand = findCommand(commands, { name: commandNames.PUSH_GRIMMORY_METADATA });
const isPushingToGrimmory = !!(
grimmoryPushCommand &&
isCommandExecuting(grimmoryPushCommand) &&
grimmoryPushCommand.body &&
(grimmoryPushCommand.body.bookIds || []).includes(book.id)
);
const showPushToGrimmory = notifications.some((n) => n.implementation === 'Grimmory');

const isFetching = isBookFilesFetching || editions.isFetching;
const isPopulated = isBookFilesPopulated && editions.isPopulated;
const selectedEdition = editions.items
Expand All @@ -140,6 +179,9 @@ function createMapStateToProps() {
author,
isRefreshing,
isSearching,
showPushToGrimmory,
isPushingToGrimmory,
grimmoryPreview: buildGrimmoryPreview(book, author, selectedEdition),
isRenamingFiles,
isRenamingAuthor,
isFetching,
Expand All @@ -163,6 +205,7 @@ function createMapStateToProps() {

const mapDispatchToProps = {
executeCommand,
fetchNotifications,
fetchBookFiles,
clearBookFiles,
fetchEditions,
Expand Down Expand Up @@ -228,6 +271,7 @@ class BookDetailsConnector extends Component {
this.props.fetchBookFiles({ bookId });
this.props.fetchEditions({ bookId });
this.props.fetchQueueDetails({ bookIds: [bookId] });
this.props.fetchNotifications();
};

unpopulate = () => {
Expand Down Expand Up @@ -262,6 +306,14 @@ class BookDetailsConnector extends Component {
});
};

onPushToGrimmoryPress = (fields) => {
this.props.executeCommand({
name: commandNames.PUSH_GRIMMORY_METADATA,
bookIds: [this.props.id],
fields
});
};

//
// Render

Expand All @@ -272,6 +324,7 @@ class BookDetailsConnector extends Component {
onMonitorTogglePress={this.onMonitorTogglePress}
onRefreshPress={this.onRefreshPress}
onSearchPress={this.onSearchPress}
onPushToGrimmoryPress={this.onPushToGrimmoryPress}
/>
);
}
Expand All @@ -286,6 +339,7 @@ BookDetailsConnector.propTypes = {
isBookFetching: PropTypes.bool,
isBookPopulated: PropTypes.bool,
bookId: PropTypes.number.isRequired,
fetchNotifications: PropTypes.func.isRequired,
fetchBookFiles: PropTypes.func.isRequired,
clearBookFiles: PropTypes.func.isRequired,
fetchEditions: PropTypes.func.isRequired,
Expand Down
72 changes: 69 additions & 3 deletions frontend/src/Book/Editor/BookEditorFooter.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import * as commandNames from 'Commands/commandNames';
import SelectInput from 'Components/Form/SelectInput';
import SpinnerButton from 'Components/Link/SpinnerButton';
import PageContentFooter from 'Components/Page/PageContentFooter';
import GrimmoryPushModal from 'Grimmory/GrimmoryPushModal';
import { kinds } from 'Helpers/Props';
import { executeCommand } from 'Store/Actions/commandActions';
import { fetchNotifications } from 'Store/Actions/settingsActions';
import createCommandExecutingSelector from 'Store/Selectors/createCommandExecutingSelector';
import translate from 'Utilities/String/translate';
import BookEditorFooterLabel from './BookEditorFooterLabel';
import DeleteBookModal from './Delete/DeleteBookModal';
Expand All @@ -24,12 +30,17 @@ class BookEditorFooter extends Component {
rootFolderPath: NO_CHANGE,
savingTags: false,
isDeleteBookModalOpen: false,
isGrimmoryPushModalOpen: false,
isTagsModalOpen: false,
isConfirmMoveModalOpen: false,
destinationRootFolder: null
};
}

componentDidMount() {
this.props.fetchNotifications();
}

componentDidUpdate(prevProps) {
const {
isSaving,
Expand Down Expand Up @@ -72,6 +83,24 @@ class BookEditorFooter extends Component {
this.setState({ isDeleteBookModalOpen: false });
};

onPushToGrimmoryPress = () => {
this.setState({ isGrimmoryPushModalOpen: true });
};

onGrimmoryPushModalClose = () => {
this.setState({ isGrimmoryPushModalOpen: false });
};

onGrimmoryPushConfirmed = (fields) => {
this.setState({ isGrimmoryPushModalOpen: false });

this.props.executeCommand({
name: commandNames.PUSH_GRIMMORY_METADATA,
bookIds: this.props.bookIds,
fields
});
};

//
// Render

Expand All @@ -80,12 +109,15 @@ class BookEditorFooter extends Component {
bookIds,
selectedCount,
isSaving,
isDeleting
isDeleting,
isPushingToGrimmory,
showPushToGrimmory
} = this.props;

const {
monitored,
isDeleteBookModalOpen
isDeleteBookModalOpen,
isGrimmoryPushModalOpen
} = this.state;

const monitoredOptions = [
Expand Down Expand Up @@ -119,6 +151,20 @@ class BookEditorFooter extends Component {
/>

<div className={styles.buttons}>
{
showPushToGrimmory ?
<SpinnerButton
className={styles.organizeSelectedButton}
kind={kinds.WARNING}
isSpinning={isPushingToGrimmory}
isDisabled={!selectedCount || isPushingToGrimmory}
onPress={this.onPushToGrimmoryPress}
>
{translate('PushChaptarrMetadataToGrimmory')}
</SpinnerButton> :
null
}

<SpinnerButton
className={styles.deleteSelectedButton}
kind={kinds.DANGER}
Expand All @@ -138,6 +184,13 @@ class BookEditorFooter extends Component {
onModalClose={this.onDeleteBookModalClose}
/>

<GrimmoryPushModal
isOpen={isGrimmoryPushModalOpen}
bookCount={selectedCount}
onPushPress={this.onGrimmoryPushConfirmed}
onModalClose={this.onGrimmoryPushModalClose}
/>

</PageContentFooter>
);
}
Expand All @@ -150,7 +203,20 @@ BookEditorFooter.propTypes = {
saveError: PropTypes.object,
isDeleting: PropTypes.bool.isRequired,
deleteError: PropTypes.object,
isPushingToGrimmory: PropTypes.bool.isRequired,
showPushToGrimmory: PropTypes.bool.isRequired,
fetchNotifications: PropTypes.func.isRequired,
executeCommand: PropTypes.func.isRequired,
onSaveSelected: PropTypes.func.isRequired
};

export default BookEditorFooter;
const selectIsPushingToGrimmory = createCommandExecutingSelector(commandNames.PUSH_GRIMMORY_METADATA);

function mapStateToProps(state) {
return {
isPushingToGrimmory: selectIsPushingToGrimmory(state),
showPushToGrimmory: state.settings.notifications.items.some((n) => n.implementation === 'Grimmory')
};
}

export default connect(mapStateToProps, { executeCommand, fetchNotifications })(BookEditorFooter);
1 change: 1 addition & 0 deletions frontend/src/Commands/commandNames.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const BOOK_SEARCH = 'BookSearch';
export const INTERACTIVE_IMPORT = 'ManualImport';
export const MISSING_BOOK_SEARCH = 'MissingBookSearch';
export const MOVE_AUTHOR = 'MoveAuthor';
export const PUSH_GRIMMORY_METADATA = 'PushGrimmoryMetadata';
export const REFRESH_AUTHOR = 'RefreshAuthor';
export const BULK_REFRESH_AUTHOR = 'BulkRefreshAuthor';
export const REFRESH_BOOK = 'RefreshBook';
Expand Down
Loading