Skip to content

Download book cover when preview panel is opened#15250

Closed
melkerolle wants to merge 13 commits into
JabRef:mainfrom
DD2480-Group-14:main
Closed

Download book cover when preview panel is opened#15250
melkerolle wants to merge 13 commits into
JabRef:mainfrom
DD2480-Group-14:main

Conversation

@melkerolle

@melkerolle melkerolle commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Related issues and pull requests

Closes #14848

PR Description

This PR makes it so the book cover is automatically downloaded when the preview panel is opened. To make this possible, BookCoverFetcher was modified to create .not-available files when it could not find a remote cover. Repeated attempts to download the cover should not be made if a previous attempt failed within 24 hours. A new test file, BookCoverFetcherTest.java was created to test this functionality. Additionaly, PreviewViewer was modified to download the book cover when opened and refresh when that is done.

We open this PR as a draft. We plan to add more tests in the coming days and complete all items in the checklist.

Steps to test

Test download and refresh

Disable download book covers in preferences. Create a new entry with the ISBN filled in. Enable download book covers in preferences. Open the preview panel for the created entry. The book cover should download and refresh the panel content.

Test download cooldown

Go to the book covers directory and create the file isbn-9780141036144.not-available. Make a new entry with ISBN 9780141036144. The book cover should not be downloaded (see logs). Change your time to be more than 24 hours in the future. Open the entry again. The book cover should now be downloaded and the panel refreshed. The .not-available file should be deleted.

Screenshot

image

Checklist

  • I own the copyright of the code submitted and I license it under the MIT license
  • I manually tested my changes in running JabRef (always required)
  • I added JUnit tests for changes (if applicable)
  • [/] I added screenshots in the PR description (if change is visible to the user)
  • I added a screenshot in the PR description showing a library with a single entry with me as author and as title the issue number
  • I described the change in CHANGELOG.md in a way that can be understood by the average user (if change is visible to the user)
  • I checked the user documentation for up to dateness and submitted a pull request to our user documentation repository

melkerolle and others added 4 commits March 1, 2026 17:48
* feat: add a test for requirement RE02

I also had to change the access modifier to `protected` for one download
function. Additionally, I refactored the download method a bit, in order
to mock at test it more easily.

* fix: cleanup imports
@github-actions

github-actions Bot commented Mar 2, 2026

Copy link
Copy Markdown
Contributor

Hey @melkerolle! 👋

Thank you for contributing to JabRef!

We have automated checks in place, based on which you will soon get feedback if any of them are failing. We also use Qodo for review assistance. It will update your pull request description with a review help and offer suggestions to improve the pull request.

After all automated checks pass, a maintainer will also review your contribution. Once that happens, you can go through their comments in the "Files changed" tab and act on them, or reply to the conversation if you have further inputs. You can read about the whole pull request process in our contribution guide.

Please ensure that your pull request is in line with our AI Usage Policy and make necessary disclosures.

@github-actions github-actions Bot added first contrib good first issue An issue intended for project-newcomers. Varies in difficulty. status: changes-required Pull requests that are not yet complete labels Mar 2, 2026
@testlens-app

This comment has been minimized.

@faneeshh

faneeshh commented Mar 2, 2026

Copy link
Copy Markdown
Collaborator

The cooldown logic looks fine but the destination.get( ) calls might fail since destination is already a Path. Also worth adding a null check after resolveNameWithType returns empty.

@testlens-app

This comment has been minimized.

@melkerolle

melkerolle commented Mar 2, 2026

Copy link
Copy Markdown
Contributor Author

... destination.get( ) calls might fail since destination is already a Path.

Do you mean that the variable name indicates that it should be a Path and not Optional<Path>?

Also worth adding a null check after resolveNameWithType returns empty.

Are we not already doing this?

Co-authored-by: Melker Trané <melkert@kth.se>
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

…gai (#62)

Co-authored-by: Melker Trané <melkert@kth.se>
@testlens-app

This comment has been minimized.

@testlens-app

This comment has been minimized.

@testlens-app

testlens-app Bot commented Mar 3, 2026

Copy link
Copy Markdown

✅ All tests passed ✅

🏷️ Commit: e9cff95
▶️ Tests: 10112 executed
⚪️ Checks: 122/122 completed


Learn more about TestLens at testlens.app.

@jabref-machine

Copy link
Copy Markdown
Collaborator

You committed your code on the main brach of your fork. This is a bad practice. The right way is to branch out from main, work on your patch/feature in that new branch, and then get that branch merged via the pull request (see GitHub flow).

For this pull request, this is OK. For subsequent pull requests, please start with a different branch with a proper branch name. See CONTRIBUTING.md for more details.

@melkerolle melkerolle marked this pull request as ready for review March 3, 2026 18:47
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Automatic book cover download with cooldown mechanism

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Automatically download book covers when preview panel opens
• Implement 24-hour cooldown for failed download attempts
• Create .not-available marker files for unavailable covers
• Add comprehensive test coverage for cover fetching logic
Diagram
flowchart LR
  A["Preview Panel Opens"] --> B["Check if Cover Downloaded"]
  B --> C{Cover Exists?}
  C -->|Yes| D["Display Cover"]
  C -->|No| E["Check .not-available File"]
  E --> F{Within 24 Hours?}
  F -->|Yes| G["Skip Download"]
  F -->|No| H["Download Cover"]
  H --> I{Success?}
  I -->|Yes| J["Delete .not-available File"]
  I -->|No| K["Create/Update .not-available File"]
  J --> L["Refresh Preview"]
  K --> L
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java ✨ Enhancement +88/-7

Add cooldown and marker file logic for cover downloads

• Added 24-hour cooldown mechanism using .not-available marker files
• Implemented timeSincePreviousAttempt() to check file modification timestamps
• Added flagAsNotAvailable() to create/update marker files on failed downloads
• Added deleteNotAvailableFileIfExists() to clean up markers on successful downloads
• Extracted getURLDownload() as protected method for testability
• Enhanced exception handling to distinguish between client/server errors and other failures
• Changed getImageUrl() from static to instance method for better testability

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java


2. jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java ✨ Enhancement +16/-1

Trigger automatic cover download on preview open

• Modified update() method to trigger cover download after preview generation
• Added downloadCoverAndRefresh() method to handle background cover fetching
• Integrated preference check for shouldDownloadCovers() before attempting download
• Refresh preview text after successful cover download if entry is still current

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java


3. jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java 🧪 Tests +207/-0

New test suite for BookCoverFetcher functionality

• Created comprehensive test suite with 8 test cases covering core functionality
• Tests include cooldown mechanism, .not-available file creation, and retrieval logic
• Tests verify modification time updates and file deletion after successful downloads
• Uses mocking and temporary directories for isolated test execution
• Tests cover edge cases like empty directories and stale marker files

jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java


View more (1)
4. CHANGELOG.md 📝 Documentation +1/-0

Document automatic cover download feature

• Added entry documenting automatic book cover download feature
• References issue #14848 for tracking purposes

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Mar 3, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (3) 📘 Rule violations (2) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Tests miss getMimeType stub 🐞 Bug ✓ Correctness
Description
BookCoverFetcherTest mocks URLDownload but never stubs getMimeType(), while production code
immediately calls .getMimeType().flatMap(...). Depending on Mockito defaults, this can return an
unexpected default (commonly null) and crash tests at runtime.
Code

jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[R64-67]

+        bookCoverFetcher = spy(new BookCoverFetcher(externalApplicationsPreferences));
+        mockDownload = mock(URLDownload.class);
+        doReturn(mockDownload).when(bookCoverFetcher).getURLDownload(Mockito.anyString());
+    }
Evidence
The production path unconditionally dereferences the Optional returned by
URLDownload.getMimeType() via flatMap. In the test, URLDownload is a plain mock and
getMimeType() is never stubbed, so the call chain is not guaranteed to be a real Optional and
can break at runtime.

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[111-117]
jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[64-67]
jablib/src/main/java/org/jabref/logic/net/URLDownload.java[125-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`BookCoverFetcherTest` mocks `URLDownload` but does not stub `getMimeType()`. Production code calls `download.getMimeType().flatMap(...)`, which requires `getMimeType()` to return a real, non-null `Optional`.

### Issue Context
This can cause the new tests to fail at runtime (e.g., NPE / unexpected mock behavior) and makes the test suite brittle.

### Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[55-67]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[111-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

2. Optional get() after isEmpty() 📘 Rule violation ✓ Correctness
Description
New code branches on Optional.isEmpty() and then calls Optional.get(), which discards
Optional-driven control flow and reduces readability. This violates the guideline to use Optional
APIs like map, filter, and ifPresentOrElse instead of presence checks + get().
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R64-66]

+            Optional<Duration> timeSincePreviousAttempt = timeSincePreviousAttempt(name, directory);
+            if (!timeSincePreviousAttempt.isEmpty() && timeSincePreviousAttempt.get().toHours() < IMAGE_DOWNLOAD_COOLDOWN_HOURS) {
+                LOGGER.info("Skipped download attempt for {}, attempted less than 24 hours ago", name);
Evidence
PR Compliance ID 19 requires Optional-driven control flow rather than isPresent/isEmpty
branching and get(). The added code uses !timeSincePreviousAttempt.isEmpty() and then
immediately dereferences via get().

AGENTS.md
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`BookCoverFetcher` uses `Optional.isEmpty()`/`isPresent()` checks followed by `Optional.get()`, which goes against the project’s Optional-driven control-flow guideline.

## Issue Context
This pattern was introduced in the new cooldown logic and can be rewritten more idiomatically using `Optional` combinators (e.g., `map`, `filter`, `ifPresentOrElse`, `orElse`).

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-66]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[75-81]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[119-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Exception not logged in catch 📘 Rule violation ✧ Quality
Description
The new catch (FetcherClientException | FetcherServerException e) block does not log the exception
object, which reduces diagnosability when these failures occur. This violates the logging
requirement to log exceptions correctly when handling error cases.
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R128-131]

+        } catch (FetcherClientException | FetcherServerException e) {
+            LOGGER.info("Remote book cover does not exist or server returned an error for URL: {}", url);
+            LOGGER.info("Flagging book cover as not available");
+            flagAsNotAvailable(name, directory);
Evidence
PR Compliance ID 16 requires that exceptions are logged by passing the exception as the last logger
argument (and not silently discarded). The new catch block logs informational messages but omits
e, so stack/causal details are lost.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[128-131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly introduced catch block handles `FetcherClientException`/`FetcherServerException` but does not log the exception object, losing useful diagnostic information.

## Issue Context
Compliance requires logging exceptions by passing the exception as the last argument to the logger.

## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[128-131]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Stale preview text overwrite 🐞 Bug ⛯ Reliability
Description
After cover download completes, PreviewViewer re-applies the *captured* previewText instead of
regenerating. If the same entry changes (fields/layout) while the download task is running, the
completion callback can overwrite a newer preview with stale content.
Code

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[R213-231]

        BackgroundTask.wrap(() -> layout.generatePreview(currentEntry, databaseContext))
-                      .onSuccess(this::setPreviewText)
+                      .onSuccess(previewText -> {
+                          setPreviewText(previewText);
+                          if (preferences.getPreviewPreferences().shouldDownloadCovers()) {
+                              downloadCoverAndRefresh(currentEntry, previewText);
+                          }
+                      })
                      .onFailure(e -> setPreviewText(formatError(currentEntry, e)))
                      .executeWith(taskExecutor);
    }

+    private void downloadCoverAndRefresh(BibEntry entry, String previewText) {
+        BackgroundTask.wrap(() -> bookCoverFetcher.downloadCoversForEntry(entry))
+                      .onSuccess((ignored) -> {
+                          if (entry.equals(this.entry)) {
+                              setPreviewText(previewText);
+                          }
+                      })
+                      .executeWith(taskExecutor);
Evidence
update() generates a previewText snapshot and starts an async download. On download completion,
it calls setPreviewText(previewText) using that old snapshot; the only guard is
entry.equals(this.entry), which remains true for the same selected entry even if its content
changed since the snapshot.

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[213-231]
jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[248-274]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`PreviewViewer.downloadCoverAndRefresh` re-renders using a captured `previewText` from an earlier async generation. If the same entry changes while the cover download runs, the completion callback can overwrite newer preview content.

### Issue Context
The current equality check (`entry.equals(this.entry)`) only prevents updates when the user switches to a different entry; it does not protect against in-place modifications to the same selected entry.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[200-232]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (1)
5. Cooldown breaks with clock skew 🐞 Bug ⛯ Reliability
Description
If the .not-available file has a last-modified timestamp in the future (e.g., user/system clock
changed), Duration.between(lastModified, now) becomes negative and the < 24h check will always
skip downloads until real time catches up.
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R64-86]

+            Optional<Duration> timeSincePreviousAttempt = timeSincePreviousAttempt(name, directory);
+            if (!timeSincePreviousAttempt.isEmpty() && timeSincePreviousAttempt.get().toHours() < IMAGE_DOWNLOAD_COOLDOWN_HOURS) {
+                LOGGER.info("Skipped download attempt for {}, attempted less than 24 hours ago", name);
+                return;
+            }
            final String url = getImageUrl(isbn);
            downloadCoverImage(url, name, directory);
        }
    }

+    private Optional<Duration> timeSincePreviousAttempt(String name, Path directory) {
+        Optional<Path> notAvailablePathOptional = resolveNameWithType(directory, name, NOT_AVAILABLE_FILE_TYPE);
+        if (notAvailablePathOptional.isEmpty()) {
+            LOGGER.warn("Could not find not available file path for: {}", name);
+            return Optional.empty();
+        }
+        Path notAvailablePath = notAvailablePathOptional.get();
+        if (Files.exists(notAvailablePath)) {
+            try {
+                FileTime lastModifiedTimeStamp = Files.getLastModifiedTime(notAvailablePath);
+                Duration timeSinceLastModification = Duration.between(lastModifiedTimeStamp.toInstant(), Instant.now());
+                return Optional.of(timeSinceLastModification);
+            } catch (IOException e) {
Evidence
The cooldown uses a raw Duration.between(fileMtime, Instant.now()) and compares toHours() < 24.
For negative durations, toHours() is negative, so the condition is true and the download is
skipped.

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-68]
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[83-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
Cooldown computation can produce a negative `Duration` when `.not-available` mtime is in the future, causing the `&lt; 24h` condition to skip downloads indefinitely (until the system clock catches up).

### Issue Context
This can happen after manual clock adjustments or time sync corrections.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[74-90]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-68]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Comment on lines +137 to +139
protected URLDownload getURLDownload(String url) throws MalformedURLException {
return new URLDownload(url);
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The only purpose of this method is for the URLDownload to be mockable in the tests

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a code smell to me. Maybe dependency injection by constructor or factory. Without seeing context right now: HttpClient as service?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we want to test with real downloads anyway, mocking the URLDownload is not needed, so I will just remove this method.

@github-actions

github-actions Bot commented Mar 7, 2026

Copy link
Copy Markdown
Contributor

Your pull request conflicts with the target branch.

Please merge with your code. For a step-by-step guide to resolve merge conflicts, see https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/addressing-merge-conflicts/resolving-a-merge-conflict-using-the-command-line.

import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

class BookCoverFetcherTest {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mark this as FetcherTest and test the real download

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I cannot simply use the FetcherTest annotation since it is in jabref/jablib and this is in jabref/jabgui and I do not really know how to solve this.

Another thing I noted was that BookCoverFetcher seems to be the only "fetcher" that is outside of jabref/jablib. Would it make more sense if BookCoverFetcher was moved?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would appreciate if someone more knowledgeable about the JabRef architecture would give their opinion about this issue, so it can be solved in an elegant way.

@calixtus

Copy link
Copy Markdown
Member

Closed due to contributor not responding

@calixtus calixtus closed this Mar 12, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull requests was closed without merging. You have been unassigned from the respective issue #14848. In case you closed the PR for yourself, you can re-open it. Please also check After submission of a pull request in CONTRIBUTING.md.

@melkerolle

Copy link
Copy Markdown
Contributor Author

Can this be opened again? I did not know that not responding would lead to it being closed. I intend to implement the suggestions in the coming days

@calixtus calixtus reopened this Mar 12, 2026
@qodo-free-for-open-source-projects

Copy link
Copy Markdown
Contributor

Review Summary by Qodo

Automatic book cover download with cooldown mechanism

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Automatically download book covers when preview panel opens
• Implement 24-hour cooldown for failed download attempts
• Create .not-available marker files for unavailable covers
• Add comprehensive test coverage for cover fetching logic
Diagram
flowchart LR
  A["Preview Panel Opens"] --> B["Check if Cover Downloaded"]
  B --> C{Cover Exists?}
  C -->|Yes| D["Display Cover"]
  C -->|No| E["Check .not-available File"]
  E --> F{Within 24 Hours?}
  F -->|Yes| G["Skip Download"]
  F -->|No| H["Download Cover"]
  H --> I{Success?}
  I -->|Yes| J["Delete .not-available File"]
  I -->|No| K["Create/Update .not-available File"]
  J --> L["Refresh Preview"]
  K --> L
Loading

Grey Divider

File Changes

1. jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java ✨ Enhancement +88/-7

Add cooldown and marker file logic for cover downloads

• Added 24-hour cooldown mechanism using .not-available marker files
• Implemented timeSincePreviousAttempt() method to check file modification timestamps
• Added flagAsNotAvailable() and deleteNotAvailableFileIfExists() methods
• Enhanced error handling to distinguish between client/server errors and other exceptions
• Made getImageUrl() and getURLDownload() methods protected/non-static for testing
• Added imports for file time operations and new exception types

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java


2. jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java ✨ Enhancement +16/-1

Trigger background cover download on preview open

• Modified update() method to trigger cover download after preview generation
• Added downloadCoverAndRefresh() method for background cover fetching
• Integrated preference check for shouldDownloadCovers() setting
• Refreshes preview text after successful cover download

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java


3. jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java 🧪 Tests +207/-0

Add comprehensive test coverage for BookCoverFetcher

• Created comprehensive test suite with 8 test cases
• Tests cover cooldown mechanism, .not-available file creation/deletion
• Tests modification time updates for retry logic
• Tests retrieval of downloaded covers and empty directory scenarios
• Uses mocking and temporary directories for isolated testing

jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java


View more (1)
4. CHANGELOG.md 📝 Documentation +1/-0

Document automatic cover download feature

• Added entry documenting automatic book cover download feature
• References issue #14848

CHANGELOG.md


Grey Divider

Qodo Logo

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Mar 12, 2026

Copy link
Copy Markdown
Contributor

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (4) 📎 Requirement gaps (0)

Grey Divider


Action required

1. Caches transient HTTP errors 🐞 Bug ⛯ Reliability ⭐ New
Description
BookCoverFetcher flags a cover as not available (creating a .not-available file that triggers a 24h
cooldown) for any HTTP 5xx and all 4xx responses, which can suppress retries for transient
conditions like 503 or 429 for an entire day.
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R126-132]

+            download.toFile(destination);
+            deleteNotAvailableFileIfExists(name, directory);
+        } catch (FetcherClientException | FetcherServerException e) {
+            LOGGER.info("Remote book cover does not exist or server returned an error for URL: {}", url);
+            LOGGER.info("Flagging book cover as not available");
+            flagAsNotAvailable(name, directory);
        } catch (FetcherException e) {
-            LOGGER.error("Error while downloading cover image file", e);
Evidence
URLDownload.openConnection throws FetcherClientException for any HTTP status >=400 and <500
(includes 429) and FetcherServerException for >=500 (includes transient 5xx). BookCoverFetcher
treats both the same by creating/updating a .not-available marker, and subsequent downloads are
skipped for <24h when that marker exists.

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[61-71]
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[125-134]
jablib/src/main/java/org/jabref/logic/net/URLDownload.java[379-387]
jablib/src/main/java/org/jabref/logic/importer/FetcherException.java[110-112]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

### Issue description
`BookCoverFetcher` currently creates/updates the `.not-available` marker for *all* `FetcherClientException` (any 4xx) and `FetcherServerException` (any 5xx). This turns transient outages and rate limiting (e.g., HTTP 503/429) into a 24-hour suppression of download attempts.

### Issue Context
`URLDownload.openConnection()` maps HTTP statuses to these exception types, and `FetcherException` provides access to the `SimpleHttpResponse` (status code) via `getHttpResponse()`.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[119-134]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[61-71]
- jablib/src/main/java/org/jabref/logic/net/URLDownload.java[379-387]
- jablib/src/main/java/org/jabref/logic/importer/FetcherException.java[110-124]

### Implementation direction
- Split the catch blocks:
 - For `FetcherClientException`, inspect `e.getHttpResponse().map(SimpleHttpResponse::statusCode)`:
   - If 404/410 (or other permanent “no cover” statuses you decide), create/update `.not-available`.
   - If 429 or other potentially transient 4xx, do **not** create `.not-available` (or create a separate marker with a short cooldown).
 - For `FetcherServerException` (5xx), do **not** create `.not-available`; log and allow retry later (or implement a short backoff).
- Keep deleting `.not-available` on successful download.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Tests miss getMimeType stub 🐞 Bug ✓ Correctness
Description
BookCoverFetcherTest mocks URLDownload but never stubs getMimeType(), while production code
immediately calls .getMimeType().flatMap(...). Depending on Mockito defaults, this can return an
unexpected default (commonly null) and crash tests at runtime.
Code

jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[R64-67]

+        bookCoverFetcher = spy(new BookCoverFetcher(externalApplicationsPreferences));
+        mockDownload = mock(URLDownload.class);
+        doReturn(mockDownload).when(bookCoverFetcher).getURLDownload(Mockito.anyString());
+    }
Evidence
The production path unconditionally dereferences the Optional returned by
URLDownload.getMimeType() via flatMap. In the test, URLDownload is a plain mock and
getMimeType() is never stubbed, so the call chain is not guaranteed to be a real Optional and
can break at runtime.

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[111-117]
jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[64-67]
jablib/src/main/java/org/jabref/logic/net/URLDownload.java[125-176]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`BookCoverFetcherTest` mocks `URLDownload` but does not stub `getMimeType()`. Production code calls `download.getMimeType().flatMap(...)`, which requires `getMimeType()` to return a real, non-null `Optional`.
### Issue Context
This can cause the new tests to fail at runtime (e.g., NPE / unexpected mock behavior) and makes the test suite brittle.
### Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[55-67]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[111-117]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Typo in test method 📘 Rule violation ✓ Correctness ⭐ New
Description
The new test method name modificationTimeDoesNotChangesWhenLessThan24Hours contains a
grammatical/spelling error (DoesNotChanges). This reduces readability and violates the requirement
for correctly spelled, meaningful identifiers.
Code

jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[177]

+    void modificationTimeDoesNotChangesWhenLessThan24Hours() throws IOException, FetcherException {
Evidence
The checklist requires newly introduced identifiers to be correctly spelled and meaningful. The
added method name contains an incorrect verb form.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[177-177]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly added JUnit test method name contains a spelling/grammar typo (`DoesNotChanges`).

## Issue Context
Project conventions require typo-free, intention-revealing identifiers.

## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[177-177]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


4. Trivial test block comments 📘 Rule violation ✓ Correctness ⭐ New
Description
The new tests add multi-line comments that primarily restate what the test code does rather than
explaining intent or rationale. This adds noise and violates the guideline to avoid trivial
comments.
Code

jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[R74-78]

+    /// Test the cooldown for trying to download a book cover
+    ///
+    /// Asserts that the system does not try to download
+    /// a book cover again if it just attempted to do so
+    @Test
Evidence
The compliance rule requires comments to explain "why" rather than narrating the code. The added
comment block describes obvious behavior (that the test asserts a cooldown) without adding rationale
beyond what the method name and assertions already communicate.

AGENTS.md
jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[74-78]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several newly added test comments are trivial (they restate what the code does) instead of explaining intent/rationale.

## Issue Context
JabRef guidelines require comments to explain &quot;why&quot; rather than narrating implementation details.

## Fix Focus Areas
- jabgui/src/test/java/org/jabref/gui/importer/BookCoverFetcherTest.java[74-78]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


5. Optional get() after isEmpty() 📘 Rule violation ✓ Correctness
Description
New code branches on Optional.isEmpty() and then calls Optional.get(), which discards
Optional-driven control flow and reduces readability. This violates the guideline to use Optional
APIs like map, filter, and ifPresentOrElse instead of presence checks + get().
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R64-66]

+            Optional<Duration> timeSincePreviousAttempt = timeSincePreviousAttempt(name, directory);
+            if (!timeSincePreviousAttempt.isEmpty() && timeSincePreviousAttempt.get().toHours() < IMAGE_DOWNLOAD_COOLDOWN_HOURS) {
+                LOGGER.info("Skipped download attempt for {}, attempted less than 24 hours ago", name);
Evidence
PR Compliance ID 19 requires Optional-driven control flow rather than isPresent/isEmpty
branching and get(). The added code uses !timeSincePreviousAttempt.isEmpty() and then
immediately dereferences via get().

AGENTS.md
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`BookCoverFetcher` uses `Optional.isEmpty()`/`isPresent()` checks followed by `Optional.get()`, which goes against the project’s Optional-driven control-flow guideline.
## Issue Context
This pattern was introduced in the new cooldown logic and can be rewritten more idiomatically using `Optional` combinators (e.g., `map`, `filter`, `ifPresentOrElse`, `orElse`).
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-66]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[75-81]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[119-125]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


View more (3)
6. Exception not logged in catch 📘 Rule violation ✓ Correctness
Description
The new catch (FetcherClientException | FetcherServerException e) block does not log the exception
object, which reduces diagnosability when these failures occur. This violates the logging
requirement to log exceptions correctly when handling error cases.
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R128-131]

+        } catch (FetcherClientException | FetcherServerException e) {
+            LOGGER.info("Remote book cover does not exist or server returned an error for URL: {}", url);
+            LOGGER.info("Flagging book cover as not available");
+            flagAsNotAvailable(name, directory);
Evidence
PR Compliance ID 16 requires that exceptions are logged by passing the exception as the last logger
argument (and not silently discarded). The new catch block logs informational messages but omits
e, so stack/causal details are lost.

AGENTS.md
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[128-131]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly introduced catch block handles `FetcherClientException`/`FetcherServerException` but does not log the exception object, losing useful diagnostic information.
## Issue Context
Compliance requires logging exceptions by passing the exception as the last argument to the logger.
## Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[128-131]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


7. Stale preview text overwrite 🐞 Bug ⛯ Reliability
Description
After cover download completes, PreviewViewer re-applies the *captured* previewText instead of
regenerating. If the same entry changes (fields/layout) while the download task is running, the
completion callback can overwrite a newer preview with stale content.
Code

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[R213-231]

       BackgroundTask.wrap(() -> layout.generatePreview(currentEntry, databaseContext))
-                      .onSuccess(this::setPreviewText)
+                      .onSuccess(previewText -> {
+                          setPreviewText(previewText);
+                          if (preferences.getPreviewPreferences().shouldDownloadCovers()) {
+                              downloadCoverAndRefresh(currentEntry, previewText);
+                          }
+                      })
                     .onFailure(e -> setPreviewText(formatError(currentEntry, e)))
                     .executeWith(taskExecutor);
   }

+    private void downloadCoverAndRefresh(BibEntry entry, String previewText) {
+        BackgroundTask.wrap(() -> bookCoverFetcher.downloadCoversForEntry(entry))
+                      .onSuccess((ignored) -> {
+                          if (entry.equals(this.entry)) {
+                              setPreviewText(previewText);
+                          }
+                      })
+                      .executeWith(taskExecutor);
Evidence
update() generates a previewText snapshot and starts an async download. On download completion,
it calls setPreviewText(previewText) using that old snapshot; the only guard is
entry.equals(this.entry), which remains true for the same selected entry even if its content
changed since the snapshot.

jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[213-231]
jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[248-274]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`PreviewViewer.downloadCoverAndRefresh` re-renders using a captured `previewText` from an earlier async generation. If the same entry changes while the cover download runs, the completion callback can overwrite newer preview content.
### Issue Context
The current equality check (`entry.equals(this.entry)`) only prevents updates when the user switches to a different entry; it does not protect against in-place modifications to the same selected entry.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java[200-232]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


8. Cooldown breaks with clock skew 🐞 Bug ⛯ Reliability
Description
If the .not-available file has a last-modified timestamp in the future (e.g., user/system clock
changed), Duration.between(lastModified, now) becomes negative and the < 24h check will always
skip downloads until real time catches up.
Code

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[R64-86]

+            Optional<Duration> timeSincePreviousAttempt = timeSincePreviousAttempt(name, directory);
+            if (!timeSincePreviousAttempt.isEmpty() && timeSincePreviousAttempt.get().toHours() < IMAGE_DOWNLOAD_COOLDOWN_HOURS) {
+                LOGGER.info("Skipped download attempt for {}, attempted less than 24 hours ago", name);
+                return;
+            }
           final String url = getImageUrl(isbn);
           downloadCoverImage(url, name, directory);
       }
   }

+    private Optional<Duration> timeSincePreviousAttempt(String name, Path directory) {
+        Optional<Path> notAvailablePathOptional = resolveNameWithType(directory, name, NOT_AVAILABLE_FILE_TYPE);
+        if (notAvailablePathOptional.isEmpty()) {
+            LOGGER.warn("Could not find not available file path for: {}", name);
+            return Optional.empty();
+        }
+        Path notAvailablePath = notAvailablePathOptional.get();
+        if (Files.exists(notAvailablePath)) {
+            try {
+                FileTime lastModifiedTimeStamp = Files.getLastModifiedTime(notAvailablePath);
+                Duration timeSinceLastModification = Duration.between(lastModifiedTimeStamp.toInstant(), Instant.now());
+                return Optional.of(timeSinceLastModification);
+            } catch (IOException e) {
Evidence
The cooldown uses a raw Duration.between(fileMtime, Instant.now()) and compares toHours() < 24.
For negative durations, toHours() is negative, so the condition is true and the download is
skipped.

jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-68]
jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[83-86]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Cooldown computation can produce a negative `Duration` when `.not-available` mtime is in the future, causing the `&amp;lt; 24h` condition to skip downloads indefinitely (until the system clock catches up).
### Issue Context
This can happen after manual clock adjustments or time sync corrections.
### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[74-90]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[64-68]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

@calixtus

Copy link
Copy Markdown
Member

Yes, there are several other contributors in the last weeks who just submitted some initial pr but forget about them, we became more strict with the prs. If someone is not reacting - or at least commenting - in reasonable time (about a week) we close to get the number rof open prs down.

Comment on lines +126 to 132
download.toFile(destination);
deleteNotAvailableFileIfExists(name, directory);
} catch (FetcherClientException | FetcherServerException e) {
LOGGER.info("Remote book cover does not exist or server returned an error for URL: {}", url);
LOGGER.info("Flagging book cover as not available");
flagAsNotAvailable(name, directory);
} catch (FetcherException e) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

1. Caches transient http errors 🐞 Bug ⛯ Reliability

BookCoverFetcher flags a cover as not available (creating a .not-available file that triggers a 24h
cooldown) for any HTTP 5xx and all 4xx responses, which can suppress retries for transient
conditions like 503 or 429 for an entire day.
Agent Prompt
### Issue description
`BookCoverFetcher` currently creates/updates the `.not-available` marker for *all* `FetcherClientException` (any 4xx) and `FetcherServerException` (any 5xx). This turns transient outages and rate limiting (e.g., HTTP 503/429) into a 24-hour suppression of download attempts.

### Issue Context
`URLDownload.openConnection()` maps HTTP statuses to these exception types, and `FetcherException` provides access to the `SimpleHttpResponse` (status code) via `getHttpResponse()`.

### Fix Focus Areas
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[119-134]
- jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java[61-71]
- jablib/src/main/java/org/jabref/logic/net/URLDownload.java[379-387]
- jablib/src/main/java/org/jabref/logic/importer/FetcherException.java[110-124]

### Implementation direction
- Split the catch blocks:
  - For `FetcherClientException`, inspect `e.getHttpResponse().map(SimpleHttpResponse::statusCode)`:
    - If 404/410 (or other permanent “no cover” statuses you decide), create/update `.not-available`.
    - If 429 or other potentially transient 4xx, do **not** create `.not-available` (or create a separate marker with a short cooldown).
  - For `FetcherServerException` (5xx), do **not** create `.not-available`; log and allow retry later (or implement a short backoff).
- Keep deleting `.not-available` on successful download.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@Siedlerchr

Copy link
Copy Markdown
Member

No activity for the last two weeks

@Siedlerchr Siedlerchr closed this Mar 25, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This pull requests was closed without merging. You have been unassigned from the respective issue #14848. In case you closed the PR for yourself, you can re-open it. Please also check After submission of a pull request in CONTRIBUTING.md.

@melkerolle

melkerolle commented Mar 27, 2026

Copy link
Copy Markdown
Contributor Author

I have not made changes since I was waiting for you to respond to some of my comments @Siedlerchr

@melkerolle

Copy link
Copy Markdown
Contributor Author

@Siedlerchr, @calixtus?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

first contrib good first issue An issue intended for project-newcomers. Varies in difficulty. status: changes-required Pull requests that are not yet complete

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Download book cover when entry editor is showing the book cover

6 participants