-
-
Notifications
You must be signed in to change notification settings - Fork 3.1k
Add support for book front covers #14330
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 37 commits
d29ab3b
7b33850
05d21d4
2865553
7842c6e
3f91954
bb58664
112a095
56475f3
68e55c6
a9e8120
71fba83
0d97ac5
25b996c
f4946cd
2c61ef3
dc614c4
5e9b8af
2d9abbc
b56b664
45ff913
4f6a3b6
8b05127
9d705fc
73831aa
d7a6537
692db5f
23f3203
c5d7d7e
e5df521
b3473b0
77398d8
ca5dcb2
cd33ce2
df05e83
a41f391
9ef2bb4
cbb3340
4124d98
4f6607f
99fe001
777ad41
9fc23be
966fd67
c68ef91
bc9f0b4
622838b
ca67eb3
3ebdc55
f2d7a1a
7f17cd9
477d845
6824940
b0be871
609371a
5813bcc
11b3570
fb1deab
2cb5c99
05d3561
ef72987
8b87815
d642f6c
0d474a3
8f39853
3d9b17f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,134 @@ | ||
| package org.jabref.gui.importer; | ||
|
|
||
| import java.net.MalformedURLException; | ||
| import java.io.IOException; | ||
| import java.nio.file.InvalidPathException; | ||
| import java.nio.file.Path; | ||
| import java.nio.file.Files; | ||
| import java.util.Optional; | ||
| import java.util.regex.Matcher; | ||
| import java.util.regex.Pattern; | ||
|
|
||
| import org.jabref.gui.externalfiletype.ExternalFileType; | ||
| import org.jabref.gui.externalfiletype.ExternalFileTypes; | ||
| import org.jabref.gui.frame.ExternalApplicationsPreferences; | ||
| import org.jabref.logic.FilePreferences; | ||
| import org.jabref.logic.importer.FetcherException; | ||
| import org.jabref.logic.net.URLDownload; | ||
| 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.identifier.ISBN; | ||
|
|
||
| import org.slf4j.Logger; | ||
| import org.slf4j.LoggerFactory; | ||
|
|
||
| /** | ||
| * Provides functions for downloading book covers for new entries. | ||
| */ | ||
| public class BookCoverFetcher { | ||
|
|
||
| private static final Logger LOGGER = LoggerFactory.getLogger(BookCoverFetcher.class); | ||
|
|
||
| private static final Pattern URL_JSON_PATTERN = Pattern.compile("^\\s*\\{\\s*\"url\"\\s*:\\s*\"([^\"]*)\"\\s*\\}\\s*$"); | ||
|
|
||
| private static final String URL_FETCHER_URL = "https://bookcover.longitood.com/bookcover/"; | ||
| private static final String IMAGE_FALLBACK_URL = "https://covers.openlibrary.org/b/isbn/"; | ||
| private static final String IMAGE_FALLBACK_SUFFIX = "-L.jpg"; | ||
|
|
||
| public static Optional<BibEntry> withAttachedCoverFileIfExists(Optional<BibEntry> possible, BibDatabaseContext databaseContext, FilePreferences filePreferences, ExternalApplicationsPreferences externalApplicationsPreferences) { | ||
| if (possible.isPresent() && filePreferences.shouldDownloadCovers()) { | ||
| BibEntry entry = possible.get(); | ||
| Optional<ISBN> isbn = entry.getISBN(); | ||
| if (isbn.isPresent()) { | ||
| final String url = getCoverImageURLForIsbn(isbn.get()); | ||
| final Optional<Path> directory = databaseContext.getFirstExistingFileDir(filePreferences); | ||
|
|
||
| // 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(); | ||
bblhd marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| Optional<LinkedFile> file = tryToDownloadLinkedFile(externalApplicationsPreferences, url, directory, filePreferences.coversDownloadLocation().trim(), 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 = URL_FETCHER_URL + isbn.asString(); | ||
| try { | ||
| LOGGER.info("Downloading book cover url from {}", url); | ||
|
|
||
| URLDownload download = new URLDownload(url); | ||
|
|
||
| String json = download.asString(); | ||
| Matcher matches = URL_JSON_PATTERN.matcher(json); | ||
|
|
||
| if (matches.find()) { | ||
| String coverUrlString = matches.group(1); | ||
| if (coverUrlString != null) { | ||
| return coverUrlString; | ||
| } | ||
| } | ||
| } catch (FetcherException | MalformedURLException e) { | ||
| LOGGER.error("Error while querying cover url, using fallback", e); | ||
| } | ||
| } | ||
| return IMAGE_FALLBACK_URL + isbn.asString() + IMAGE_FALLBACK_SUFFIX; | ||
| } | ||
|
|
||
| private static Optional<LinkedFile> tryToDownloadLinkedFile(ExternalApplicationsPreferences externalApplicationsPreferences, String url, Optional<Path> directory, String location, String name) { | ||
bblhd marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Optional<Path> subdirectory = resolveRealSubdirectory(directory, location); | ||
| if (subdirectory.isPresent()) { | ||
| Optional<String> extension = FileUtil.getFileExtension(FileUtil.getFileNameFromUrl(url)); | ||
| Path destination = subdirectory.get().resolve(extension.map(x -> name + "." + x).orElse(name)); | ||
|
|
||
| String type = inferFileTypeFromExtension(externalApplicationsPreferences, extension); | ||
| String link = directory.get().relativize(destination).toString(); | ||
|
|
||
| if (Files.notExists(destination)) { | ||
| try { | ||
| LOGGER.info("Downloading cover image file from {}", url); | ||
|
|
||
| URLDownload download = new URLDownload(url); | ||
|
|
||
| download.toFile(destination); | ||
| } catch (FetcherException | MalformedURLException e) { | ||
| LOGGER.error("Error while downloading cover image file, Storing as URL in file field", e); | ||
| return Optional.of(new LinkedFile("[cover]", url, "")); | ||
| } | ||
| } | ||
| return Optional.of(new LinkedFile("[cover]", link, type, url)); | ||
| } 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, "")); | ||
| } | ||
| } | ||
|
|
||
| private static Optional<Path> resolveRealSubdirectory(Optional<Path> directory, String location) { | ||
| if ("".equals(location)) { | ||
| return directory; | ||
| } | ||
| if (directory.isPresent()) { | ||
| try { | ||
| final Path subdirectory = directory.get().resolve(location); | ||
| Files.createDirectories(subdirectory); | ||
| if (Files.exists(subdirectory)) { | ||
koppor marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| return Optional.of(subdirectory); | ||
| } | ||
| } catch (IOException | InvalidPathException e) { | ||
| return Optional.empty(); | ||
| } | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private static String inferFileTypeFromExtension(ExternalApplicationsPreferences externalApplicationsPreferences, Optional<String> extension) { | ||
| return extension.map(x -> ExternalFileTypes.getExternalFileTypeByExt(x, externalApplicationsPreferences).map(t -> t.getName()).orElse("")).orElse(""); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,7 @@ | |
| import java.net.MalformedURLException; | ||
| import java.util.List; | ||
| import java.util.Objects; | ||
| import java.util.Optional; | ||
|
|
||
| import javafx.beans.InvalidationListener; | ||
| import javafx.beans.Observable; | ||
|
|
@@ -20,6 +21,8 @@ | |
| import org.jabref.gui.StateManager; | ||
| import org.jabref.gui.desktop.os.NativeDesktop; | ||
| import org.jabref.gui.exporter.ExportToClipboardAction; | ||
| import org.jabref.gui.externalfiletype.ExternalFileType; | ||
| import org.jabref.gui.externalfiletype.ExternalFileTypes; | ||
| import org.jabref.gui.preferences.GuiPreferences; | ||
| import org.jabref.gui.search.Highlighter; | ||
| import org.jabref.gui.theme.ThemeManager; | ||
|
|
@@ -30,9 +33,12 @@ | |
| 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.logic.util.strings.StringUtil; | ||
| 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 com.airhacks.afterburner.injection.Injector; | ||
|
|
@@ -222,17 +228,73 @@ private String formatError(BibEntry entry, Throwable exception) { | |
| } | ||
|
|
||
| private void setPreviewText(String text) { | ||
| String coverIfAny = ""; | ||
| Optional<String> image = getCoverImageURI(); | ||
| if (image.isPresent()) { | ||
| coverIfAny = "<img style=\"border-width:1px; border-style:solid; border-color:black; display:block; height:12rem;\" src=\"%s\"> <br>".formatted(image.get()); | ||
| } | ||
|
|
||
| layoutText = """ | ||
| <html> | ||
| <body id="previewBody"> | ||
| <div id="content"> %s </div> | ||
| </body> | ||
| </html> | ||
| """.formatted(text); | ||
| <html> | ||
| <body id="previewBody"> | ||
| %s <div id="content"> %s </div> | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is OK for a quick hack. One should introduce a The default preview style needs to be adapted accordingly. Not sure whether we should make exceptions of the general preview handling for this because of images. @InAnYan |
||
| </body> | ||
| </html> | ||
| """.formatted(coverIfAny, text); | ||
| highlightLayoutText(); | ||
| setHvalue(0); | ||
| } | ||
|
|
||
| private Optional<String> getCoverImageURI() { | ||
bblhd marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| if (shouldShowCoverImage()) { | ||
| String nameFromFormat = FileUtil.createFileNameFromPattern(databaseContext.getDatabase(), entry, preferences.getFilePreferences().getFileNamePattern()).orElse("cover"); | ||
|
|
||
| List<LinkedFile> 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()); | ||
| } | ||
| } | ||
| } | ||
| return Optional.empty(); | ||
| } | ||
|
|
||
| private boolean shouldShowCoverImage() { | ||
| // 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; | ||
| default -> | ||
| false; | ||
| }; | ||
| } | ||
|
|
||
| private boolean isFileTypeAValidCoverImage(String fileType) { | ||
| // to allow url links | ||
| if ("".equals(fileType)) { | ||
| return true; | ||
| } | ||
|
|
||
| // needed because type names are stored in a localization dependent way | ||
| Optional<ExternalFileType> actualFileType = ExternalFileTypes.getExternalFileTypeByName(fileType, preferences.getExternalApplicationsPreferences()); | ||
|
|
||
| if (actualFileType.isPresent()) { | ||
| return actualFileType.get().getMimeType().startsWith("image/"); | ||
| } | ||
| return false; | ||
| } | ||
|
|
||
| private void highlightLayoutText() { | ||
| if (layoutText == null) { | ||
| return; | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.