From d29ab3bd02683f18c49389f2d8fba533a4cee3ce Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Fri, 17 Oct 2025 03:43:05 +1100 Subject: [PATCH 01/54] Added initial work to get basic image covers working properly --- .../org/jabref/gui/preview/PreviewViewer.java | 78 +++++++++++++++++-- jablib/src/main/abbrv.jabref.org | 2 +- .../externalfiles/LinkedFileHandler.java | 26 ++----- .../org/jabref/logic/util/io/FileUtil.java | 6 +- .../org/jabref/model/entry/LinkedFile.java | 16 ++++ jablib/src/main/resources/csl-locales | 2 +- jablib/src/main/resources/csl-styles | 2 +- 7 files changed, 98 insertions(+), 34 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index ae05d601ac8..ba145f8649b 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -4,8 +4,10 @@ import java.io.PrintWriter; import java.io.StringWriter; import java.net.MalformedURLException; +import java.nio.file.Path; import java.util.List; import java.util.Objects; +import java.util.Optional; import javafx.beans.InvalidationListener; import javafx.beans.Observable; @@ -27,13 +29,18 @@ import org.jabref.gui.theme.ThemeManager; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.gui.util.WebViewStore; +import org.jabref.gui.externalfiletype.ExternalFileType; +import org.jabref.gui.externalfiletype.ExternalFileTypes; import org.jabref.logic.l10n.Localization; import org.jabref.logic.layout.format.Number; import org.jabref.logic.preview.PreviewLayout; import org.jabref.logic.util.BackgroundTask; import org.jabref.logic.util.TaskExecutor; +import org.jabref.logic.util.io.FileUtil; import org.jabref.model.database.BibDatabaseContext; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; +import org.jabref.model.entry.types.StandardEntryType; import org.jabref.model.search.query.SearchQuery; import org.jabref.model.strings.StringUtil; @@ -220,13 +227,25 @@ private String formatError(BibEntry entry, Throwable exception) { } private void setPreviewText(String text) { - layoutText = """ - - -
%s
- - - """.formatted(text); + Optional image = getCoverImageURI(); + if (image.isPresent()) { + layoutText = """ + + +
%s
+ + + + """.formatted(text, image.get()); + } else { + layoutText = """ + + +
%s
+ + + """.formatted(text); + } highlightLayoutText(); setHvalue(0); } @@ -246,6 +265,51 @@ private void highlightLayoutText() { } } + private Optional getCoverImageURI() { + if (shouldShowCoverImage()) { + String nameFromFormat = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileNamePattern()).orElse("cover"); + List linkedFiles = entry.getFiles(); + for (LinkedFile file : linkedFiles) { + String fileName = FileUtil.getBaseName(file.getFileName()); + + // matches images that are either named according to the preferred file name format + // or images with "COVER" in their description, to allow setting the cover to any image regardless of name. + if (isFileTypeAValidCoverImage(file.getFileType()) && (fileName.equals(nameFromFormat) || file.getDescription().contains("COVER"))) { + if (file.isOnlineLink()) { + return Optional.of(file.getLink()); + } else { + Optional fileLocation = file.findIn(databaseContext, preferences.getFilePreferences()); + if (fileLocation.isPresent()) { + return Optional.of(fileLocation.get().toUri().toString()); + } + } + } + } + } + return Optional.empty(); + } + + private boolean shouldShowCoverImage() { + if (entry == null) return false; + + return switch (entry.getType()) { + case StandardEntryType.Book, StandardEntryType.Booklet, StandardEntryType.BookInBook, StandardEntryType.InBook, StandardEntryType.MvBook -> true; + default -> false; + }; + } + + private boolean isFileTypeAValidCoverImage(String fileType) { + // needed because most image type names are stored in a localisation dependent way + Optional actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences()); + if (actualFileType.isPresent()) { + return switch (actualFileType.get().getMimeType()) { + case "image/png", "image/gif", "image/jpeg", "image/tiff", "image/vnd.djuv" -> true; + default -> false; + }; + } + return false; + } + public void print() { PrinterJob job = PrinterJob.createPrinterJob(); if (job == null) { diff --git a/jablib/src/main/abbrv.jabref.org b/jablib/src/main/abbrv.jabref.org index 176c06c4727..fc3ef9ee1ff 160000 --- a/jablib/src/main/abbrv.jabref.org +++ b/jablib/src/main/abbrv.jabref.org @@ -1 +1 @@ -Subproject commit 176c06c4727fa1005117ead24e8d5b9051e4f3ab +Subproject commit fc3ef9ee1ff8f6770971164963361f9d31549800 diff --git a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java index 8882dacaeca..8b442ab8fd9 100644 --- a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java +++ b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java @@ -242,33 +242,17 @@ public String getSuggestedFileName() { } /** - * Determines the file name based on the pattern specified in the preferences and valid for the file system. + * Determines the suggested file name based on the pattern specified in the preferences and valid for the file system. * * @param extension The extension of the file. If empty, no extension is added. - * @return A filename based on the pattern specified in the preferences and valid for the file system. + * @return The suggested filename, including extension. */ public String getSuggestedFileName(@NonNull String extension) { Optional targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern()); if (targetFileName.isEmpty() && linkedFile.isOnlineLink()) { - String oldFileName = linkedFile.getLink(); - int lastSlashIndex = oldFileName.lastIndexOf('/'); - if (lastSlashIndex >= 0 && lastSlashIndex < oldFileName.length() - 1) { - String fileNameFromUrl = oldFileName.substring(lastSlashIndex + 1); - int queryIndex = fileNameFromUrl.indexOf('?'); - if (queryIndex > 0) { - fileNameFromUrl = fileNameFromUrl.substring(0, queryIndex); - } - if (!fileNameFromUrl.isEmpty()) { - if (!extension.isEmpty()) { - Optional existingExtension = FileUtil.getFileExtension(fileNameFromUrl); - if (existingExtension.isEmpty() || !existingExtension.get().equalsIgnoreCase(extension)) { - String baseName = FileUtil.getBaseName(fileNameFromUrl); - fileNameFromUrl = baseName + "." + extension; - } - } - return FileUtil.getValidFileName(fileNameFromUrl); - } - } + String fullLinkedFileName = linkedFile.getFileName(); + extension = FileUtil.getFileExtension(fullLinkedFileName).orElse(""); + targetFileName = Optional.of(FileUtil.getBaseName(fullLinkedFileName)); } String baseName = targetFileName.orElse("file"); diff --git a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java index 16f7c9ce114..b90f9fba2e7 100644 --- a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -89,21 +89,21 @@ public static Optional getFileExtension(String fileName) { /** * Returns the extension of a file or Optional.empty() if the file does not have one (no . in name). * - * @return the extension (without leading dot), trimmed and in lowercase. + * @return the extension (without leading dot), trimmed and in lowercase */ public static Optional getFileExtension(Path file) { return getFileExtension(file.getFileName().toString()); } /** - * Returns the name part of a file name (i.e., everything in front of last "."). + * @return the name part of a file name (i.e., everything before last ".") */ public static String getBaseName(String fileNameWithExtension) { return com.google.common.io.Files.getNameWithoutExtension(fileNameWithExtension); } /** - * Returns the name part of a file name (i.e., everything in front of last "."). + * @return the name part of a file name (i.e., everything before last ".") */ public static String getBaseName(Path fileNameWithExtension) { return getBaseName(fileNameWithExtension.getFileName().toString()); diff --git a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java index 8482ecc1279..53440c4ae1e 100644 --- a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -142,6 +142,22 @@ public void setLink(String link) { } } + public String getFileName() { + String linkName = getLink(); + assert linkName != null; + int lastSlashIndex = linkName.lastIndexOf('/'); + if (lastSlashIndex >= 0 && lastSlashIndex < linkName.length() - 1) { + linkName = linkName.substring(lastSlashIndex + 1); + } + if (isOnlineLink() && !linkName.isEmpty()) { + int queryIndex = linkName.indexOf('?'); + if (queryIndex > 0) { + linkName = linkName.substring(0, queryIndex); + } + } + return linkName; + } + public String getSourceUrl() { return sourceURL.get(); } diff --git a/jablib/src/main/resources/csl-locales b/jablib/src/main/resources/csl-locales index fbb76f61297..8c149db3008 160000 --- a/jablib/src/main/resources/csl-locales +++ b/jablib/src/main/resources/csl-locales @@ -1 +1 @@ -Subproject commit fbb76f6129728a234c4e42ba598c7bbbedd73301 +Subproject commit 8c149db30089aa45956d3373dbb0b269f7bd104f diff --git a/jablib/src/main/resources/csl-styles b/jablib/src/main/resources/csl-styles index 72350250645..b61592ea58b 160000 --- a/jablib/src/main/resources/csl-styles +++ b/jablib/src/main/resources/csl-styles @@ -1 +1 @@ -Subproject commit 723502506457f6b3a1b202c2debb8b8cf085098a +Subproject commit b61592ea58b94d790fa36708048153989840552c From 2865553ee41112ff52803871535ed1ef72a49ae6 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Sat, 18 Oct 2025 17:38:56 +1100 Subject: [PATCH 02/54] fixed order of cover images, refactored some of the cover code --- .../org/jabref/gui/preview/PreviewViewer.java | 71 +++++++++---------- 1 file changed, 33 insertions(+), 38 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index ba145f8649b..79f545e8ed1 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -228,43 +228,23 @@ private String formatError(BibEntry entry, Throwable exception) { private void setPreviewText(String text) { Optional image = getCoverImageURI(); + String coverIfAny = ""; if (image.isPresent()) { - layoutText = """ - - -
%s
- - - - """.formatted(text, image.get()); - } else { - layoutText = """ - - -
%s
- - - """.formatted(text); + coverIfAny = "
".formatted(image.get()); } + + layoutText = """ + + + %s
%s
+ + + """.formatted(coverIfAny, text); + System.out.println(layoutText); highlightLayoutText(); setHvalue(0); } - private void highlightLayoutText() { - if (layoutText == null) { - return; - } - - String queryText = searchQueryProperty.get(); - if (StringUtil.isNotBlank(queryText)) { - SearchQuery searchQuery = new SearchQuery(queryText); - String highlighted = Highlighter.highlightHtml(layoutText, searchQuery); - UiTaskExecutor.runInJavaFXThread(() -> previewView.getEngine().loadContent(highlighted)); - } else { - UiTaskExecutor.runInJavaFXThread(() -> previewView.getEngine().loadContent(layoutText)); - } - } - private Optional getCoverImageURI() { if (shouldShowCoverImage()) { String nameFromFormat = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileNamePattern()).orElse("cover"); @@ -288,9 +268,12 @@ private Optional getCoverImageURI() { } return Optional.empty(); } - + private boolean shouldShowCoverImage() { - if (entry == null) return false; + //entry is sometimes null when setPreviewText is called + if (entry == null) { + return false; + } return switch (entry.getType()) { case StandardEntryType.Book, StandardEntryType.Booklet, StandardEntryType.BookInBook, StandardEntryType.InBook, StandardEntryType.MvBook -> true; @@ -299,17 +282,29 @@ private boolean shouldShowCoverImage() { } private boolean isFileTypeAValidCoverImage(String fileType) { - // needed because most image type names are stored in a localisation dependent way + // needed because most image type names are stored in a localization dependent way Optional actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences()); if (actualFileType.isPresent()) { - return switch (actualFileType.get().getMimeType()) { - case "image/png", "image/gif", "image/jpeg", "image/tiff", "image/vnd.djuv" -> true; - default -> false; - }; + return actualFileType.get().getMimeType().startsWith("image/"); } return false; } + private void highlightLayoutText() { + if (layoutText == null) { + return; + } + + String queryText = searchQueryProperty.get(); + if (StringUtil.isNotBlank(queryText)) { + SearchQuery searchQuery = new SearchQuery(queryText); + String highlighted = Highlighter.highlightHtml(layoutText, searchQuery); + UiTaskExecutor.runInJavaFXThread(() -> previewView.getEngine().loadContent(highlighted)); + } else { + UiTaskExecutor.runInJavaFXThread(() -> previewView.getEngine().loadContent(layoutText)); + } + } + public void print() { PrinterJob job = PrinterJob.createPrinterJob(); if (job == null) { From 7842c6e04dd472ebd75536cc7bab6ecb2d6a1d94 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Thu, 13 Nov 2025 00:58:01 +1100 Subject: [PATCH 03/54] rudimentary cover fetching --- .../org/jabref/gui/preview/PreviewViewer.java | 28 ++++--- .../externalfiles/LinkedFileHandler.java | 20 ++++- .../fetcher/isbntobibtex/IsbnFetcher.java | 38 ++++++++++ .../org/jabref/model/entry/LinkedFile.java | 76 ++++++++++++++----- 4 files changed, 122 insertions(+), 40 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 79f545e8ed1..c3b30bf383a 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -230,7 +230,8 @@ private void setPreviewText(String text) { Optional image = getCoverImageURI(); String coverIfAny = ""; if (image.isPresent()) { - coverIfAny = "
".formatted(image.get()); + //TODO: figure out if should to use style field like this, something else, or just use unstyled image + coverIfAny = "
".formatted(image.get()); } layoutText = """ @@ -240,7 +241,6 @@ private void setPreviewText(String text) { """.formatted(coverIfAny, text); - System.out.println(layoutText); highlightLayoutText(); setHvalue(0); } @@ -248,21 +248,14 @@ private void setPreviewText(String text) { private Optional getCoverImageURI() { if (shouldShowCoverImage()) { String nameFromFormat = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileNamePattern()).orElse("cover"); + List linkedFiles = entry.getFiles(); for (LinkedFile file : linkedFiles) { - String fileName = FileUtil.getBaseName(file.getFileName()); - // matches images that are either named according to the preferred file name format - // or images with "COVER" in their description, to allow setting the cover to any image regardless of name. - if (isFileTypeAValidCoverImage(file.getFileType()) && (fileName.equals(nameFromFormat) || file.getDescription().contains("COVER"))) { - if (file.isOnlineLink()) { - return Optional.of(file.getLink()); - } else { - Optional fileLocation = file.findIn(databaseContext, preferences.getFilePreferences()); - if (fileLocation.isPresent()) { - return Optional.of(fileLocation.get().toUri().toString()); - } - } + // or images with case-insensitive "(cover)" in their description, to allow using any image regardless of name + + if (file.getDescription().toLowerCase().contains("(cover)") || isFileTypeAValidCoverImage(file.getFileType()) && (FileUtil.getBaseName(file.getFileName()).equals(nameFromFormat))) { + return file.getURI(databaseContext, preferences.getFilePreferences()); } } } @@ -272,7 +265,7 @@ private Optional getCoverImageURI() { private boolean shouldShowCoverImage() { //entry is sometimes null when setPreviewText is called if (entry == null) { - return false; + return false; } return switch (entry.getType()) { @@ -282,8 +275,13 @@ private boolean shouldShowCoverImage() { } private boolean isFileTypeAValidCoverImage(String fileType) { + // to allow url links + if (fileType.equals("")) { + return true; + } // needed because most image type names are stored in a localization dependent way Optional actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences()); + if (actualFileType.isPresent()) { return actualFileType.get().getMimeType().startsWith("image/"); } diff --git a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java index 8b442ab8fd9..ce327d1aba9 100644 --- a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java +++ b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java @@ -245,14 +245,26 @@ public String getSuggestedFileName() { * Determines the suggested file name based on the pattern specified in the preferences and valid for the file system. * * @param extension The extension of the file. If empty, no extension is added. - * @return The suggested filename, including extension. + * @return the suggested filename, including extension */ public String getSuggestedFileName(@NonNull String extension) { Optional targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern()); + if (targetFileName.isEmpty() && linkedFile.isOnlineLink()) { - String fullLinkedFileName = linkedFile.getFileName(); - extension = FileUtil.getFileExtension(fullLinkedFileName).orElse(""); - targetFileName = Optional.of(FileUtil.getBaseName(fullLinkedFileName)); + String linkedName = linkedFile.getLink(); + + int lastSlashIndex = linkedName.lastIndexOf('/'); + if (lastSlashIndex >= 0 && lastSlashIndex < linkedName.length() - 1) { + linkedName = linkedName.substring(lastSlashIndex + 1); + } + + int queryIndex = linkedName.indexOf('?'); + if (!linkedName.isEmpty() && queryIndex > 0) { + linkedName = linkedName.substring(0, queryIndex); + } + + extension = FileUtil.getFileExtension(linkedName).orElse(""); + targetFileName = Optional.of(FileUtil.getBaseName(linkedName)); } String baseName = targetFileName.orElse("file"); diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java index 1c024421082..2c28849a95f 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java @@ -14,10 +14,17 @@ import org.jabref.logic.importer.fetcher.AbstractIsbnFetcher; import org.jabref.logic.importer.fetcher.GvkFetcher; import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.identifier.ISBN; import org.jabref.model.util.OptionalUtil; +import java.util.regex.Pattern; +import java.util.regex.Matcher; +import java.net.MalformedURLException; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.net.URLDownload; + import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -35,6 +42,10 @@ public class IsbnFetcher implements EntryBasedFetcher, IdBasedFetcher { protected final ImportFormatPreferences importFormatPreferences; private final List retryIsbnFetcher; private final GvkFetcher gvkIsbnFetcher; + + private static final String bookCoverUrlSource = "https://bookcover.longitood.com/bookcover/"; + private static final String BOOK_COVER_JSON_URL_REGEX = "^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"; + private static final Pattern BOOK_COVER_JSON_URL_PATTERN = Pattern.compile(BOOK_COVER_JSON_URL_REGEX); public IsbnFetcher(ImportFormatPreferences importFormatPreferences) { this.importFormatPreferences = importFormatPreferences; @@ -85,10 +96,37 @@ public Optional performSearchById(String identifier) throws FetcherExc if (bibEntry.isEmpty()) { LOGGER.debug("Could not found a entry for ISBN {}", identifier); + } else { + Optional cover = getCoverImageURLFromStringOfISBN(identifier); + if (cover.isPresent()) { + bibEntry.get().addFile(cover.get()); + } } return bibEntry; } + + private static Optional getCoverImageURLFromStringOfISBN(String identifier) { + try { + URLDownload downloader = new URLDownload(bookCoverUrlSource + identifier); + String json = downloader.asString(); + Matcher matches = BOOK_COVER_JSON_URL_PATTERN.matcher(json); + if (matches.find()) { + String coverUrlString = matches.group(1); + if (coverUrlString != null) { + URLDownload downloader = new URLDownload(coverUrlString); + importFormatPreferences.filePreferences + System.out.println(coverUrlString); + return Optional.of(new LinkedFile("(cover)", coverUrlString, "")); + } + } + } catch (FetcherException e) { + return Optional.empty(); + } catch (MalformedURLException e) { + return Optional.empty(); + } + return Optional.empty(); + } @Override public List performSearch(@NonNull BibEntry entry) throws FetcherException { diff --git a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java index 53440c4ae1e..716cf636960 100644 --- a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -142,22 +142,6 @@ public void setLink(String link) { } } - public String getFileName() { - String linkName = getLink(); - assert linkName != null; - int lastSlashIndex = linkName.lastIndexOf('/'); - if (lastSlashIndex >= 0 && lastSlashIndex < linkName.length() - 1) { - linkName = linkName.substring(lastSlashIndex + 1); - } - if (isOnlineLink() && !linkName.isEmpty()) { - int queryIndex = linkName.indexOf('?'); - if (queryIndex > 0) { - linkName = linkName.substring(0, queryIndex); - } - } - return linkName; - } - public String getSourceUrl() { return sourceURL.get(); } @@ -240,17 +224,67 @@ public boolean isEmpty() { public boolean isOnlineLink() { return isOnlineLink(link.get()); } + + public String getFileName() { + String linkedName = link.get(); + if (isOnlineLink(linkedName)) { + int lastSlashIndex = linkedName.lastIndexOf('/'); + if (lastSlashIndex == linkedName.length() - 1) { + linkedName = linkedName.substring(0,lastSlashIndex); + lastSlashIndex = linkedName.lastIndexOf('/'); + } + if (lastSlashIndex >= 0) { + linkedName = linkedName.substring(lastSlashIndex + 1); + } + + int queryIndex = linkedName.indexOf('?'); + if (queryIndex >= 0) { + linkedName = linkedName.substring(0, queryIndex); + } + + return linkedName; + } else { + try { + return Path.of(linkedName).getFileName().toString(); + } catch (InvalidPathException ex) { + return ""; + } + } + } + + public Optional getURI(BibDatabaseContext databaseContext, FilePreferences filePreferences) { + List dirs = databaseContext.getFileDirectories(filePreferences); + return getURI(dirs); + } + + public Optional getURI(List directories) { + String linkedName = link.get(); + if (isOnlineLink(linkedName)) { + if (linkedName.startsWith("www.")) { + linkedName = "https://"+linkedName; + } + return Optional.of(linkedName); + } else { + Optional fileLocation = findIn(directories); + if (fileLocation.isPresent()) { + return Optional.of(fileLocation.get().toUri().toString()); + } + return Optional.empty(); + } + } public Optional findIn(BibDatabaseContext databaseContext, FilePreferences filePreferences) { List dirs = databaseContext.getFileDirectories(filePreferences); return findIn(dirs); } - /// Tries to locate the file. - /// In case the path is absolute, the path is checked. - /// In case the path is relative, the given directories are used as base directories. - /// - /// @return absolute path if found. + /** + * Tries to locate the file. + * In case the path is absolute, the path is checked. + * In case the path is relative, the given directories are used as base directories. + * + * @return absolute path if found. + */ public Optional findIn(List directories) { try { if (link.get().isEmpty()) { From 3f919549e5c84bade55584957faaed42776bc3a7 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Thu, 13 Nov 2025 01:22:06 +1100 Subject: [PATCH 04/54] changed tabs to spaces --- .../org/jabref/gui/preview/PreviewViewer.java | 4 +- .../fetcher/isbntobibtex/IsbnFetcher.java | 56 +++++++++---------- .../org/jabref/model/entry/LinkedFile.java | 2 +- 3 files changed, 30 insertions(+), 32 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index c3b30bf383a..db644af2820 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -275,9 +275,9 @@ private boolean shouldShowCoverImage() { } private boolean isFileTypeAValidCoverImage(String fileType) { - // to allow url links + // to allow url links if (fileType.equals("")) { - return true; + return true; } // needed because most image type names are stored in a localization dependent way Optional actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences()); diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java index 2c28849a95f..94881410a48 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java @@ -24,6 +24,7 @@ import java.net.MalformedURLException; import org.jabref.logic.importer.FetcherException; import org.jabref.logic.net.URLDownload; +import org.jabref.gui.linkedfile.DownloadLinkedFileAction; import org.jspecify.annotations.NonNull; import org.slf4j.Logger; @@ -43,9 +44,8 @@ public class IsbnFetcher implements EntryBasedFetcher, IdBasedFetcher { private final List retryIsbnFetcher; private final GvkFetcher gvkIsbnFetcher; - private static final String bookCoverUrlSource = "https://bookcover.longitood.com/bookcover/"; - private static final String BOOK_COVER_JSON_URL_REGEX = "^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"; - private static final Pattern BOOK_COVER_JSON_URL_PATTERN = Pattern.compile(BOOK_COVER_JSON_URL_REGEX); + private static final String bookCoverSource = "https://bookcover.longitood.com/bookcover/"; + private static final Pattern BOOK_COVER_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$";); public IsbnFetcher(ImportFormatPreferences importFormatPreferences) { this.importFormatPreferences = importFormatPreferences; @@ -97,36 +97,14 @@ public Optional performSearchById(String identifier) throws FetcherExc if (bibEntry.isEmpty()) { LOGGER.debug("Could not found a entry for ISBN {}", identifier); } else { - Optional cover = getCoverImageURLFromStringOfISBN(identifier); - if (cover.isPresent()) { - bibEntry.get().addFile(cover.get()); - } + Optional cover = getCoverImageURLFromStringOfISBN(identifier); + if (cover.isPresent()) { + bibEntry.get().addFile(cover.get()); + } } return bibEntry; } - - private static Optional getCoverImageURLFromStringOfISBN(String identifier) { - try { - URLDownload downloader = new URLDownload(bookCoverUrlSource + identifier); - String json = downloader.asString(); - Matcher matches = BOOK_COVER_JSON_URL_PATTERN.matcher(json); - if (matches.find()) { - String coverUrlString = matches.group(1); - if (coverUrlString != null) { - URLDownload downloader = new URLDownload(coverUrlString); - importFormatPreferences.filePreferences - System.out.println(coverUrlString); - return Optional.of(new LinkedFile("(cover)", coverUrlString, "")); - } - } - } catch (FetcherException e) { - return Optional.empty(); - } catch (MalformedURLException e) { - return Optional.empty(); - } - return Optional.empty(); - } @Override public List performSearch(@NonNull BibEntry entry) throws FetcherException { @@ -146,4 +124,24 @@ public IsbnFetcher addRetryFetcher(@NonNull AbstractIsbnFetcher retryFetcher) { private String removeNewlinesAndSpacesFromIdentifier(String identifier) { return NEWLINE_SPACE_PATTERN.matcher(identifier).replaceAll(""); } + + private static Optional getCoverImageURLFromStringOfISBN(String identifier) { + try { + URLDownload downloader = new URLDownload(bookCoverSource + identifier); + String json = downloader.asString(); + Matcher matches = BOOK_COVER_PATTERN.matcher(json); + if (matches.find()) { + String coverUrlString = matches.group(1); + if (coverUrlString != null) { + URLDownload downloader = new URLDownload(coverUrlString); + return Optional.of(new LinkedFile("(cover)", coverUrlString, "")); + } + } + } catch (FetcherException e) { + return Optional.empty(); + } catch (MalformedURLException e) { + return Optional.empty(); + } + return Optional.empty(); + } } diff --git a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java index 716cf636960..def78b3cdba 100644 --- a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -269,7 +269,7 @@ public Optional getURI(List directories) { if (fileLocation.isPresent()) { return Optional.of(fileLocation.get().toUri().toString()); } - return Optional.empty(); + return Optional.empty(); } } From 112a095e4f445640f015f459b3cc173049389644 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Thu, 13 Nov 2025 03:23:25 +1100 Subject: [PATCH 05/54] fixed problems and changed cover tag parenthesis for display --- .../src/main/java/org/jabref/gui/preview/PreviewViewer.java | 4 ++-- .../logic/importer/fetcher/isbntobibtex/IsbnFetcher.java | 6 ++---- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 4d3f5208089..8b24500f442 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -254,9 +254,9 @@ private Optional getCoverImageURI() { List linkedFiles = entry.getFiles(); for (LinkedFile file : linkedFiles) { // matches images that are either named according to the preferred file name format - // or images with case-insensitive "(cover)" in their description, to allow using any image regardless of name + // or images with case-insensitive "[cover]" in their description, to allow using any image regardless of name - if (file.getDescription().toLowerCase().contains("(cover)") || isFileTypeAValidCoverImage(file.getFileType()) && (FileUtil.getBaseName(file.getFileName()).equals(nameFromFormat))) { + if (file.getDescription().toLowerCase().contains("[cover]") || isFileTypeAValidCoverImage(file.getFileType()) && (FileUtil.getBaseName(file.getFileName()).equals(nameFromFormat))) { return file.getURI(databaseContext, preferences.getFilePreferences()); } } diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java index 94881410a48..9b5ab87b932 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java @@ -24,7 +24,6 @@ import java.net.MalformedURLException; import org.jabref.logic.importer.FetcherException; import org.jabref.logic.net.URLDownload; -import org.jabref.gui.linkedfile.DownloadLinkedFileAction; import org.jspecify.annotations.NonNull; import org.slf4j.Logger; @@ -45,7 +44,7 @@ public class IsbnFetcher implements EntryBasedFetcher, IdBasedFetcher { private final GvkFetcher gvkIsbnFetcher; private static final String bookCoverSource = "https://bookcover.longitood.com/bookcover/"; - private static final Pattern BOOK_COVER_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$";); + private static final Pattern BOOK_COVER_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"); public IsbnFetcher(ImportFormatPreferences importFormatPreferences) { this.importFormatPreferences = importFormatPreferences; @@ -133,8 +132,7 @@ private static Optional getCoverImageURLFromStringOfISBN(String iden if (matches.find()) { String coverUrlString = matches.group(1); if (coverUrlString != null) { - URLDownload downloader = new URLDownload(coverUrlString); - return Optional.of(new LinkedFile("(cover)", coverUrlString, "")); + return Optional.of(new LinkedFile("[cover]", coverUrlString, "")); } } } catch (FetcherException e) { From 56475f3b432f4a8da91e98286099ba852ba8e862 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Sat, 15 Nov 2025 23:18:23 +1100 Subject: [PATCH 06/54] fixed small issues, re-re-factoring --- .../org/jabref/gui/preview/PreviewViewer.java | 14 +++++++------- .../logic/externalfiles/LinkedFileHandler.java | 15 +-------------- .../fetcher/isbntobibtex/IsbnFetcher.java | 10 ++++------ 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java index 8b24500f442..e66e29472b8 100644 --- a/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java +++ b/jabgui/src/main/java/org/jabref/gui/preview/PreviewViewer.java @@ -229,11 +229,10 @@ private String formatError(BibEntry entry, Throwable exception) { } private void setPreviewText(String text) { - Optional image = getCoverImageURI(); String coverIfAny = ""; + Optional image = getCoverImageURI(); if (image.isPresent()) { - //TODO: figure out if should to use style field like this, something else, or just use unstyled image - coverIfAny = "
".formatted(image.get()); + coverIfAny = "
".formatted(image.get()); } layoutText = """ @@ -250,12 +249,12 @@ private void setPreviewText(String text) { private Optional getCoverImageURI() { if (shouldShowCoverImage()) { String nameFromFormat = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileNamePattern()).orElse("cover"); - + List linkedFiles = entry.getFiles(); for (LinkedFile file : linkedFiles) { // matches images that are either named according to the preferred file name format // or images with case-insensitive "[cover]" in their description, to allow using any image regardless of name - + if (file.getDescription().toLowerCase().contains("[cover]") || isFileTypeAValidCoverImage(file.getFileType()) && (FileUtil.getBaseName(file.getFileName()).equals(nameFromFormat))) { return file.getURI(databaseContext, preferences.getFilePreferences()); } @@ -281,9 +280,10 @@ private boolean isFileTypeAValidCoverImage(String fileType) { if (fileType.equals("")) { return true; } - // needed because most image type names are stored in a localization dependent way + + // needed because type names are stored in a localization dependent way Optional actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences()); - + if (actualFileType.isPresent()) { return actualFileType.get().getMimeType().startsWith("image/"); } diff --git a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java index ce327d1aba9..da5fbdef736 100644 --- a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java +++ b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java @@ -251,20 +251,7 @@ public String getSuggestedFileName(@NonNull String extension) { Optional targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern()); if (targetFileName.isEmpty() && linkedFile.isOnlineLink()) { - String linkedName = linkedFile.getLink(); - - int lastSlashIndex = linkedName.lastIndexOf('/'); - if (lastSlashIndex >= 0 && lastSlashIndex < linkedName.length() - 1) { - linkedName = linkedName.substring(lastSlashIndex + 1); - } - - int queryIndex = linkedName.indexOf('?'); - if (!linkedName.isEmpty() && queryIndex > 0) { - linkedName = linkedName.substring(0, queryIndex); - } - - extension = FileUtil.getFileExtension(linkedName).orElse(""); - targetFileName = Optional.of(FileUtil.getBaseName(linkedName)); + return linkedFile.getFileName(); } String baseName = targetFileName.orElse("file"); diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java index 9b5ab87b932..016d84b6d7f 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java @@ -1,10 +1,12 @@ package org.jabref.logic.importer.fetcher.isbntobibtex; +import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; +import java.util.regex.Matcher; import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.EntryBasedFetcher; @@ -13,18 +15,14 @@ import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.fetcher.AbstractIsbnFetcher; import org.jabref.logic.importer.fetcher.GvkFetcher; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.net.URLDownload; import org.jabref.model.entry.BibEntry; import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.identifier.ISBN; import org.jabref.model.util.OptionalUtil; -import java.util.regex.Pattern; -import java.util.regex.Matcher; -import java.net.MalformedURLException; -import org.jabref.logic.importer.FetcherException; -import org.jabref.logic.net.URLDownload; - import org.jspecify.annotations.NonNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; From 68e55c627ebe0a660191b7ac89b92b2b529e6440 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Sat, 15 Nov 2025 23:21:16 +1100 Subject: [PATCH 07/54] incorrect name, now getCoverImageFileFromStringOfISBN --- .../logic/importer/fetcher/isbntobibtex/IsbnFetcher.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java index 016d84b6d7f..2fa11ff525d 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java @@ -94,7 +94,7 @@ public Optional performSearchById(String identifier) throws FetcherExc if (bibEntry.isEmpty()) { LOGGER.debug("Could not found a entry for ISBN {}", identifier); } else { - Optional cover = getCoverImageURLFromStringOfISBN(identifier); + Optional cover = getCoverImageFileFromStringOfISBN(identifier); if (cover.isPresent()) { bibEntry.get().addFile(cover.get()); } @@ -122,7 +122,7 @@ private String removeNewlinesAndSpacesFromIdentifier(String identifier) { return NEWLINE_SPACE_PATTERN.matcher(identifier).replaceAll(""); } - private static Optional getCoverImageURLFromStringOfISBN(String identifier) { + private static Optional getCoverImageFileFromStringOfISBN(String identifier) { try { URLDownload downloader = new URLDownload(bookCoverSource + identifier); String json = downloader.asString(); From 71fba8369fa1386b7cbd00114f5c51a8750b2064 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Sun, 16 Nov 2025 23:30:17 +1100 Subject: [PATCH 08/54] fixed pattern naming somewhat, improved handling of edge cases and added experimental "covers" subfolder --- .../gui/externalfiles/ImportHandler.java | 7 +- .../jabref/gui/importer/BookCoverFetcher.java | 131 ++++++++++++++++++ .../gui/newentry/NewEntryViewModel.java | 9 +- .../externalfiles/LinkedFileHandler.java | 25 ++-- .../fetcher/isbntobibtex/IsbnFetcher.java | 32 ----- .../java/org/jabref/logic/util/URLUtil.java | 15 -- .../org/jabref/logic/util/io/FileUtil.java | 19 +++ .../org/jabref/model/entry/LinkedFile.java | 16 +-- 8 files changed, 176 insertions(+), 78 deletions(-) create mode 100644 jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java diff --git a/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java b/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java index bce9ff7cb2b..3e7b6f820ce 100644 --- a/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java +++ b/jabgui/src/main/java/org/jabref/gui/externalfiles/ImportHandler.java @@ -414,7 +414,7 @@ public List handleStringData(String data) throws FetcherException { LOGGER.trace("Checking if URL is a PDF: {}", data); if (URLUtil.isURL(data)) { - String fileName = data.substring(data.lastIndexOf('/') + 1); + String fileName = FileUtil.getFileNameFromUrl(data); if (FileUtil.isPDFFile(Path.of(fileName))) { try { return handlePdfUrl(data); @@ -481,7 +481,10 @@ private List handlePdfUrl(String pdfUrl) throws IOException { return List.of(); } URLDownload urlDownload = new URLDownload(pdfUrl); - String filename = URLUtil.getFileNameFromUrl(pdfUrl); + String filename = FileUtil.getFileNameFromUrl(pdfUrl); + if (filename.isBlank()) { + filename = "downloaded.pdf"; + } Path targetFile = targetDirectory.get().resolve(filename); try { urlDownload.toFile(targetFile); diff --git a/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java b/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java new file mode 100644 index 00000000000..88250281cdd --- /dev/null +++ b/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java @@ -0,0 +1,131 @@ +package org.jabref.gui.importer; + +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.net.URL; +import java.io.File; +import java.nio.file.Path; +import java.util.Optional; +import java.util.regex.Pattern; +import java.util.regex.Matcher; + +import org.apache.hc.core5.net.URIBuilder; + +import org.jabref.gui.externalfiletype.ExternalFileType; +import org.jabref.gui.externalfiletype.ExternalFileTypes; +import org.jabref.gui.frame.ExternalApplicationsPreferences; +import org.jabref.model.database.BibDatabaseContext; +import org.jabref.logic.FilePreferences; +import org.jabref.model.entry.BibEntry; +import org.jabref.model.entry.identifier.ISBN; +import org.jabref.model.entry.LinkedFile; +import org.jabref.logic.importer.FetcherException; +import org.jabref.logic.net.URLDownload; +import org.jabref.logic.util.io.FileUtil; + +import kong.unirest.core.UnirestException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Searches web resources for bibliographic information. + */ +public class BookCoverFetcher { + + private static final Logger LOGGER = LoggerFactory.getLogger(BookCoverFetcher.class); + + private static final Pattern BOOK_COVER_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"); + + public static Optional withAttachedCoverFileIfExists(Optional possible, BibDatabaseContext databaseContext, FilePreferences filePreferences, ExternalApplicationsPreferences externalApplicationsPreferences) { + if (possible.isPresent()) { + BibEntry entry = possible.get(); + Optional isbn = entry.getISBN(); + if (isbn.isPresent()) { + final String url = getCoverImageURLForIsbn(isbn.get()); + final Path directory = databaseContext.getFirstExistingFileDir(filePreferences).orElse(filePreferences.getWorkingDirectory()); + + // Cannot use pattern for name, as auto-generated citation keys aren't available where function is used + final String name = "isbn-"+isbn.get().asString(); + + Optional file = tryToDownloadLinkedFile(externalApplicationsPreferences, directory, url, name); + if (file.isPresent()) { + entry.addFile(file.get()); + } + } + possible = Optional.of(entry); + } + return possible; + } + + private static String getCoverImageURLForIsbn(ISBN isbn) { + if (isbn.isIsbn13()) { + String url = "https://bookcover.longitood.com/bookcover/" + isbn.asString(); + try { + LOGGER.info("Downloading book cover url from {}", url); + + URLDownload download = new URLDownload(url); + download.canBeReached(); + + String json = download.asString(); + Matcher matches = BOOK_COVER_PATTERN.matcher(json); + + if (matches.find()) { + String coverUrlString = matches.group(1); + if (coverUrlString != null) { + return coverUrlString; + } + } + } catch (MalformedURLException | FetcherException e) { + LOGGER.error("Error while querying cover url, using fallback", e); + } + } + return "https://covers.openlibrary.org/b/isbn/" + isbn.asString() + "-L.jpg"; + } + + private static Optional tryToDownloadLinkedFile(ExternalApplicationsPreferences externalApplicationsPreferences, Path directory, String url, String name) { + File covers = directory.resolve("covers").toFile(); + covers.mkdirs(); + + if (covers.exists()) { + final Optional extension = FileUtil.getFileExtension(FileUtil.getFileNameFromUrl(url)); + final Path destination = directory.resolve("covers").resolve(extension.map(x -> name + "." + x).orElse(name)); + final String link = directory.relativize(destination).toString(); + + if (destination.toFile().exists()) { + return Optional.of(new LinkedFile("[cover]", link, inferFileTypeFromExtension(externalApplicationsPreferences, extension), url)); + + } else try { + LOGGER.info("Downloading cover image file from {}", url); + + URLDownload download = new URLDownload(url); + download.canBeReached(); + + final String type = inferFileType(externalApplicationsPreferences, download.getMimeType(), extension); + download.toFile(destination); + return Optional.of(new LinkedFile("[cover]", link, type, url)); + + } catch (UnirestException | FetcherException | MalformedURLException e) { + LOGGER.error("Error while downloading cover image file", e); + } + } else { + LOGGER.warn("File directory not available while downloading cover image {}. Storing as URL in file field.", url); + return Optional.of(new LinkedFile("[cover]", url, "")); + } + + return Optional.empty(); + } + + private static String inferFileType(ExternalApplicationsPreferences externalApplicationsPreferences, Optional mime, Optional extension) { + if (mime.isPresent()) { + Optional suggested = ExternalFileTypes.getExternalFileTypeByMimeType(mime.get(), externalApplicationsPreferences); + if (suggested.isPresent()) { + return suggested.get().getName(); + } + } + return inferFileTypeFromExtension(externalApplicationsPreferences, extension); + } + + private static String inferFileTypeFromExtension(ExternalApplicationsPreferences externalApplicationsPreferences, Optional extension) { + return extension.map(x -> ExternalFileTypes.getExternalFileTypeByExt(x, externalApplicationsPreferences).map(t -> t.getName()).orElse("")).orElse(""); + } +} \ No newline at end of file diff --git a/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java b/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java index 215abc26e2c..de86549d285 100644 --- a/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java +++ b/jabgui/src/main/java/org/jabref/gui/newentry/NewEntryViewModel.java @@ -25,6 +25,7 @@ import org.jabref.gui.preferences.GuiPreferences; import org.jabref.gui.util.UiTaskExecutor; import org.jabref.logic.ai.AiService; +import org.jabref.gui.importer.BookCoverFetcher; import org.jabref.logic.importer.CompositeIdFetcher; import org.jabref.logic.importer.FetcherClientException; import org.jabref.logic.importer.FetcherException; @@ -234,6 +235,10 @@ public StringProperty bibtexTextProperty() { public ReadOnlyBooleanProperty bibtexTextValidatorProperty() { return bibtexTextValidator.getValidationStatus().validProperty(); } + + private Optional withCoversAttached(Optional entry) { + return BookCoverFetcher.withAttachedCoverFileIfExists(entry, libraryTab.getBibDatabaseContext(), preferences.getFilePreferences(), preferences.getExternalApplicationsPreferences()); + } private class WorkerLookupId extends Task> { @Override @@ -245,7 +250,7 @@ protected Optional call() throws FetcherException { return Optional.empty(); } - return fetcher.performSearchById(text); + return withCoversAttached(fetcher.performSearchById(text)); } } @@ -260,7 +265,7 @@ protected Optional call() throws FetcherException { return Optional.empty(); } - return fetcher.performSearchById(text); + return withCoversAttached(fetcher.performSearchById(text)); } } diff --git a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java index da5fbdef736..e3e410e4250 100644 --- a/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java +++ b/jablib/src/main/java/org/jabref/logic/externalfiles/LinkedFileHandler.java @@ -236,9 +236,17 @@ public boolean renameToName(String targetFileName, boolean overwriteExistingFile } public String getSuggestedFileName() { - String extension = FileUtil.getFileExtension(linkedFile.getLink()) - .orElse(linkedFile.getFileType()); - return getSuggestedFileName(extension); + // Cannot get extension from type because would need both ExternalApplicationsPreferences, as type is stored as a localisation dependent string. + String filename = linkedFile.getFileName(); + Optional targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern()); + if (targetFileName.isPresent()) { + Optional extension = FileUtil.getFileExtension(filename); + if (extension.isPresent()) { + return FileUtil.getValidFileName(targetFileName.get() + "." + extension.get()); + } + return FileUtil.getValidFileName(targetFileName.get()); + } + return filename; } /** @@ -249,15 +257,8 @@ public String getSuggestedFileName() { */ public String getSuggestedFileName(@NonNull String extension) { Optional targetFileName = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, filePreferences.getFileNamePattern()); - - if (targetFileName.isEmpty() && linkedFile.isOnlineLink()) { - return linkedFile.getFileName(); - } - - String baseName = targetFileName.orElse("file"); - String suggestedName = extension.isEmpty() ? baseName : baseName + "." + extension; - - return FileUtil.getValidFileName(suggestedName); + String basename = targetFileName.orElse(FileUtil.getBaseName(linkedFile.getFileName())); + return FileUtil.getValidFileName(basename + "." + extension); } /** diff --git a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java index 2fa11ff525d..1c024421082 100644 --- a/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java +++ b/jablib/src/main/java/org/jabref/logic/importer/fetcher/isbntobibtex/IsbnFetcher.java @@ -1,12 +1,10 @@ package org.jabref.logic.importer.fetcher.isbntobibtex; -import java.net.MalformedURLException; import java.util.ArrayList; import java.util.Iterator; import java.util.List; import java.util.Optional; import java.util.regex.Pattern; -import java.util.regex.Matcher; import org.jabref.logic.help.HelpFile; import org.jabref.logic.importer.EntryBasedFetcher; @@ -15,10 +13,7 @@ import org.jabref.logic.importer.ImportFormatPreferences; import org.jabref.logic.importer.fetcher.AbstractIsbnFetcher; import org.jabref.logic.importer.fetcher.GvkFetcher; -import org.jabref.logic.importer.FetcherException; -import org.jabref.logic.net.URLDownload; import org.jabref.model.entry.BibEntry; -import org.jabref.model.entry.LinkedFile; import org.jabref.model.entry.field.StandardField; import org.jabref.model.entry.identifier.ISBN; import org.jabref.model.util.OptionalUtil; @@ -40,9 +35,6 @@ public class IsbnFetcher implements EntryBasedFetcher, IdBasedFetcher { protected final ImportFormatPreferences importFormatPreferences; private final List retryIsbnFetcher; private final GvkFetcher gvkIsbnFetcher; - - private static final String bookCoverSource = "https://bookcover.longitood.com/bookcover/"; - private static final Pattern BOOK_COVER_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"); public IsbnFetcher(ImportFormatPreferences importFormatPreferences) { this.importFormatPreferences = importFormatPreferences; @@ -93,11 +85,6 @@ public Optional performSearchById(String identifier) throws FetcherExc if (bibEntry.isEmpty()) { LOGGER.debug("Could not found a entry for ISBN {}", identifier); - } else { - Optional cover = getCoverImageFileFromStringOfISBN(identifier); - if (cover.isPresent()) { - bibEntry.get().addFile(cover.get()); - } } return bibEntry; @@ -121,23 +108,4 @@ public IsbnFetcher addRetryFetcher(@NonNull AbstractIsbnFetcher retryFetcher) { private String removeNewlinesAndSpacesFromIdentifier(String identifier) { return NEWLINE_SPACE_PATTERN.matcher(identifier).replaceAll(""); } - - private static Optional getCoverImageFileFromStringOfISBN(String identifier) { - try { - URLDownload downloader = new URLDownload(bookCoverSource + identifier); - String json = downloader.asString(); - Matcher matches = BOOK_COVER_PATTERN.matcher(json); - if (matches.find()) { - String coverUrlString = matches.group(1); - if (coverUrlString != null) { - return Optional.of(new LinkedFile("[cover]", coverUrlString, "")); - } - } - } catch (FetcherException e) { - return Optional.empty(); - } catch (MalformedURLException e) { - return Optional.empty(); - } - return Optional.empty(); - } } diff --git a/jablib/src/main/java/org/jabref/logic/util/URLUtil.java b/jablib/src/main/java/org/jabref/logic/util/URLUtil.java index 9788f72330a..8af18ca37f7 100644 --- a/jablib/src/main/java/org/jabref/logic/util/URLUtil.java +++ b/jablib/src/main/java/org/jabref/logic/util/URLUtil.java @@ -150,19 +150,4 @@ public static URI createUri(String url) { throw new IllegalArgumentException(e); } } - - /** - * Extracts the filename from a URL. - * If the URL doesn't have a filename (ends with '/'), returns a default name. - * - * @param url the URL string to extract the filename from - * @return the extracted filename or a default name if none found - */ - public static String getFileNameFromUrl(String url) { - String fileName = url.substring(url.lastIndexOf('/') + 1); - if (fileName.isBlank()) { - fileName = "downloaded.pdf"; - } - return FileUtil.getValidFileName(fileName); - } } diff --git a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java index b90f9fba2e7..4bbb21cef11 100644 --- a/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java +++ b/jablib/src/main/java/org/jabref/logic/util/io/FileUtil.java @@ -108,6 +108,25 @@ public static String getBaseName(String fileNameWithExtension) { public static String getBaseName(Path fileNameWithExtension) { return getBaseName(fileNameWithExtension.getFileName().toString()); } + + /** + * Extracts the filename from a URL. + * If the URL doesn't have a filename (ends with '/'), returns an empty string. + * + * @param link the URL string to extract the filename from + * @return the extracted filename + */ + public static String getFileNameFromUrl(String link) { + int slash = link.lastIndexOf('/'); + if (slash >= 0) { + link = link.substring(slash + 1); + } + int query = link.indexOf('?'); + if (query >= 0) { + link = link.substring(0, query); + } + return getValidFileName(link); + } /** * Returns a valid filename for most operating systems. diff --git a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java index faed97c46ca..a694ced70cc 100644 --- a/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java +++ b/jablib/src/main/java/org/jabref/model/entry/LinkedFile.java @@ -228,21 +228,7 @@ public boolean isOnlineLink() { public String getFileName() { String linkedName = link.get(); if (isOnlineLink(linkedName)) { - int lastSlashIndex = linkedName.lastIndexOf('/'); - if (lastSlashIndex == linkedName.length() - 1) { - linkedName = linkedName.substring(0,lastSlashIndex); - lastSlashIndex = linkedName.lastIndexOf('/'); - } - if (lastSlashIndex >= 0) { - linkedName = linkedName.substring(lastSlashIndex + 1); - } - - int queryIndex = linkedName.indexOf('?'); - if (queryIndex >= 0) { - linkedName = linkedName.substring(0, queryIndex); - } - - return linkedName; + return FileUtil.getFileNameFromUrl(linkedName); } else { try { return Path.of(linkedName).getFileName().toString(); From 0d97ac5aaf1af3df30ad63ec10a39a1337d81ef9 Mon Sep 17 00:00:00 2001 From: Aydan Whitton Date: Mon, 17 Nov 2025 02:36:50 +1100 Subject: [PATCH 09/54] Added preferences for cover images --- .../jabref/gui/importer/BookCoverFetcher.java | 26 +++++++-------- .../linkedfiles/LinkedFilesTab.java | 12 ++++++- .../linkedfiles/LinkedFilesTabViewModel.java | 14 ++++++++ .../linkedfiles/LinkedFilesTab.fxml | 7 ++++ .../org/jabref/logic/FilePreferences.java | 32 ++++++++++++++++++- .../preferences/JabRefCliPreferences.java | 11 ++++++- 6 files changed, 86 insertions(+), 16 deletions(-) diff --git a/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java b/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java index 88250281cdd..7c5f10db30e 100644 --- a/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java +++ b/jabgui/src/main/java/org/jabref/gui/importer/BookCoverFetcher.java @@ -34,20 +34,20 @@ public class BookCoverFetcher { private static final Logger LOGGER = LoggerFactory.getLogger(BookCoverFetcher.class); - private static final Pattern BOOK_COVER_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"); + private static final Pattern JSON_CONTAINING_URL_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"); public static Optional withAttachedCoverFileIfExists(Optional possible, BibDatabaseContext databaseContext, FilePreferences filePreferences, ExternalApplicationsPreferences externalApplicationsPreferences) { - if (possible.isPresent()) { + if (possible.isPresent() && filePreferences.shouldDownloadCovers()) { BibEntry entry = possible.get(); Optional isbn = entry.getISBN(); if (isbn.isPresent()) { final String url = getCoverImageURLForIsbn(isbn.get()); final Path directory = databaseContext.getFirstExistingFileDir(filePreferences).orElse(filePreferences.getWorkingDirectory()); - // Cannot use pattern for name, as auto-generated citation keys aren't available where function is used + // Cannot use pattern for name, as auto-generated citation keys aren't available where function is used (org.jabref.gui.newentry.NewEntryViewModel#withCoversAttached) final String name = "isbn-"+isbn.get().asString(); - Optional file = tryToDownloadLinkedFile(externalApplicationsPreferences, directory, url, name); + Optional file = tryToDownloadLinkedFile(externalApplicationsPreferences, directory, url, filePreferences.coversDownloadLocation(), name); if (file.isPresent()) { entry.addFile(file.get()); } @@ -67,7 +67,7 @@ private static String getCoverImageURLForIsbn(ISBN isbn) { download.canBeReached(); String json = download.asString(); - Matcher matches = BOOK_COVER_PATTERN.matcher(json); + Matcher matches = JSON_CONTAINING_URL_PATTERN.matcher(json); if (matches.find()) { String coverUrlString = matches.group(1); @@ -82,13 +82,13 @@ private static String getCoverImageURLForIsbn(ISBN isbn) { return "https://covers.openlibrary.org/b/isbn/" + isbn.asString() + "-L.jpg"; } - private static Optional tryToDownloadLinkedFile(ExternalApplicationsPreferences externalApplicationsPreferences, Path directory, String url, String name) { - File covers = directory.resolve("covers").toFile(); - covers.mkdirs(); - - if (covers.exists()) { - final Optional extension = FileUtil.getFileExtension(FileUtil.getFileNameFromUrl(url)); - final Path destination = directory.resolve("covers").resolve(extension.map(x -> name + "." + x).orElse(name)); + private static Optional tryToDownloadLinkedFile(ExternalApplicationsPreferences externalApplicationsPreferences, Path directory, String url, String location, String name) { + final Path subdirectory = directory.resolve(location); + + subdirectory.toFile().mkdirs(); + if (subdirectory.toFile().exists()) { + final Optional extension = FileUtil.getFileExtension(FileUtil.getFileNameFromUrl(url)); + final Path destination = subdirectory.resolve(extension.map(x -> name + "." + x).orElse(name)); final String link = directory.relativize(destination).toString(); if (destination.toFile().exists()) { @@ -117,7 +117,7 @@ private static Optional tryToDownloadLinkedFile(ExternalApplications private static String inferFileType(ExternalApplicationsPreferences externalApplicationsPreferences, Optional mime, Optional extension) { if (mime.isPresent()) { - Optional suggested = ExternalFileTypes.getExternalFileTypeByMimeType(mime.get(), externalApplicationsPreferences); + Optional suggested = ExternalFileTypes.getExternalFileTypeByMimeType(mime.get(), externalApplicationsPreferences); if (suggested.isPresent()) { return suggested.get().getName(); } diff --git a/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java b/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java index f4683d4a202..606e4bc2f19 100644 --- a/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java +++ b/jabgui/src/main/java/org/jabref/gui/preferences/linkedfiles/LinkedFilesTab.java @@ -7,6 +7,7 @@ import javafx.scene.control.ComboBox; import javafx.scene.control.RadioButton; import javafx.scene.control.TextField; +import javafx.scene.control.Label; import org.jabref.gui.actions.ActionFactory; import org.jabref.gui.actions.StandardActions; @@ -25,6 +26,11 @@ public class LinkedFilesTab extends AbstractPreferenceTabView + + + + +