From a4db8f8a12f4df53d0f1e230a3e2bcc403343402 Mon Sep 17 00:00:00 2001 From: openaudible Date: Sun, 12 Aug 2018 02:03:59 -0700 Subject: [PATCH] version 1.1.5 updates New web page. Attempt to work better with connecting to audible. --- .gitignore | 1 + src/main/java/org/openaudible/Audible.java | 22 ++- .../openaudible/audible/AudibleScraper.java | 116 ++++++++---- .../audible/ConnectionNotifier.java | 26 ++- .../openaudible/audible/LibraryParser.java | 37 ++-- src/main/java/org/openaudible/books/Book.java | 48 +++-- .../org/openaudible/convert/ConvertJob.java | 9 +- .../desktop/swt/manager/AudibleGUI.java | 18 +- .../desktop/swt/manager/Prefs.java | 2 +- .../desktop/swt/manager/Version.java | 2 +- .../swt/manager/menu/CommandCenter.java | 12 +- .../swt/manager/views/AudibleBrowser.java | 15 ++ .../desktop/swt/manager/views/BookTable.java | 7 +- .../swt/manager/views/BookTableColumn.java | 4 +- .../swt/manager/views/Preferences.java | 1 - .../feeds/pagebuilder/BookInfo.java | 2 +- .../feeds/pagebuilder/WebPage.java | 105 ++++++----- .../org/openaudible/util/TimeToSeconds.java | 14 ++ src/main/webapp/assets/audible.png | Bin 526 -> 0 bytes src/main/webapp/assets/download.jpg | Bin 574 -> 0 bytes src/main/webapp/assets/loading.gif | Bin 0 -> 8529 bytes src/main/webapp/assets/openaudible.js | 125 +++++++++++++ src/main/webapp/books.html | 139 -------------- src/main/webapp/index.html | 177 ++++++++++++++++++ 24 files changed, 608 insertions(+), 274 deletions(-) delete mode 100644 src/main/webapp/assets/audible.png delete mode 100644 src/main/webapp/assets/download.jpg create mode 100644 src/main/webapp/assets/loading.gif create mode 100644 src/main/webapp/assets/openaudible.js delete mode 100644 src/main/webapp/books.html create mode 100644 src/main/webapp/index.html diff --git a/.gitignore b/.gitignore index 5fa93ea..b06e5ea 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,7 @@ unused/ caches Desktop.ini Thumbs.db +*/Thumbs.db # java *.class diff --git a/src/main/java/org/openaudible/Audible.java b/src/main/java/org/openaudible/Audible.java index 9ba1c4d..36a6509 100644 --- a/src/main/java/org/openaudible/Audible.java +++ b/src/main/java/org/openaudible/Audible.java @@ -121,6 +121,18 @@ public class Audible implements IQueueListener { } } + // fix book info + private Book normalizeBook(Book b) + { + String link = b.getInfoLink(); + if (link.startsWith("/")) + { + // convert to full URL. + b.setInfoLink("https://www.audible.com"+link); + } + return b; + } + boolean takeBook(Book b) { if (!ok(b)) { LOG.warn("invalid book: " + checkBook(b)); @@ -130,6 +142,9 @@ public class Audible implements IQueueListener { if (!hasBook(b)) { if (!ignoreBook(b)) { synchronized (books) { + normalizeBook(b); + + books.put(b.getProduct_id(), b); } BookNotifier.getInstance().bookAdded(b); @@ -363,8 +378,9 @@ public class Audible implements IQueueListener { booksUpdated++; } } - if (booksUpdated > 0 && quick) - updateLibrary(false); + +// if (booksUpdated > 0 && quick) +// updateLibrary(false); LOG.info("Updated " + list.size() + " books"); @@ -722,7 +738,7 @@ public class Audible implements IQueueListener { @Override public void jobCompleted(ThreadedQueue queue, IQueueJob job, Book b) { - LOG.info(queue.toString() + " completed:" + b + " size:" + queue.size()); + LOG.info(queue.toString() + " completed:" + b + " remaining queue size:" + queue.size()); if (queue == downloadQueue) { try { AAXParser.instance.update(b); diff --git a/src/main/java/org/openaudible/audible/AudibleScraper.java b/src/main/java/org/openaudible/audible/AudibleScraper.java index eb182ef..1bd5d99 100644 --- a/src/main/java/org/openaudible/audible/AudibleScraper.java +++ b/src/main/java/org/openaudible/audible/AudibleScraper.java @@ -108,7 +108,10 @@ public class AudibleScraper { for (BasicClientCookie bc : list) { Cookie c = new Cookie(bc.getDomain(), bc.getName(), bc.getValue()); cm.addCookie(c); + // LOG.info("Cookie: "+c); + } + LOG.info("Loaded "+list.size()+" cookies"); } } @@ -164,8 +167,12 @@ public class AudibleScraper { FileUtils.writeByteArrayToFile(cookiesFile, o.getBytes()); } - + static int maxLoginAttempts = 2; protected boolean login() throws IOException { + return login(0); + } + + protected boolean login(int attempt) throws IOException { AudibleAccountPrefs copy = account; @@ -187,6 +194,8 @@ public class AudibleScraper { HtmlForm login = page.getFormByName("signIn"); if (login == null) { + // TODO: find sign-in anchor and click it.. + LOG.info("login form not found for page:" + page.getTitleText()); return false; } @@ -220,7 +229,7 @@ public class AudibleScraper { HtmlElement captchaImageDiv = findById("ap_captcha_img"); if (captchaImageDiv != null || ap_captcha_table != null) { - + LOG.info("Appears to be a captcha... I am a bot."); return false; } @@ -240,7 +249,10 @@ public class AudibleScraper { LOG.info(page.getUrl()); LOG.info("Login failed, see html files at:" + HTMLUtil.debugFile("submitting-credentials").getAbsolutePath() + " and " + HTMLUtil.debugFile("login failed").getAbsolutePath()); - + if (attempt implements ConnectionListener { public static final ConnectionNotifier instance = new ConnectionNotifier(); State state = State.Not_Connected; + private static final Log LOG = LogFactory.getLog(ConnectionNotifier.class); + + private String lastURL=""; + + private ConnectionNotifier() { } @@ -18,11 +25,15 @@ public class ConnectionNotifier extends EventNotifier implem @Override public void connectionChanged(boolean connected) { - state = connected ? State.Connected : State.Disconnected; - for (ConnectionListener l : getListeners()) { - l.connectionChanged(connected); + State newState = connected ? State.Connected : State.Disconnected; + if (state!=newState) { + state = newState; + for (ConnectionListener l : getListeners()) { + l.connectionChanged(connected); + } } + } public boolean isConnected() { @@ -57,6 +68,15 @@ public class ConnectionNotifier extends EventNotifier implem return getState() == State.Disconnected; } + public String getLastURL() { + return lastURL; + } + + public void setLastURL(String lastURL) { + this.lastURL = lastURL; + LOG.info("Setting lastURL to:"+lastURL); + } + // not connected is unknown. // connected means in account diff --git a/src/main/java/org/openaudible/audible/LibraryParser.java b/src/main/java/org/openaudible/audible/LibraryParser.java index 9a9cec7..199e6f3 100644 --- a/src/main/java/org/openaudible/audible/LibraryParser.java +++ b/src/main/java/org/openaudible/audible/LibraryParser.java @@ -8,6 +8,7 @@ import org.openaudible.books.BookElement; import org.openaudible.util.HTMLUtil; import org.openaudible.util.Util; +import java.net.URL; import java.util.ArrayList; import java.util.HashMap; import java.util.List; @@ -64,11 +65,11 @@ public enum LibraryParser { } - public ArrayList parseLibraryFragment(DomNode fragment) { + public ArrayList parseLibraryFragment(HtmlPage p) { ArrayList list = new ArrayList<>(); ArrayList colNames = new ArrayList<>(); - HtmlTable table = fragment.getFirstByXPath("//table"); + HtmlTable table = p.getFirstByXPath("//table"); if (table == null) return list; @@ -103,7 +104,7 @@ public enum LibraryParser { rindex++; if (rindex == 1) continue; // skip header row. - Book b = parseLibraryRow(r); + Book b = parseLibraryRow(p, r); if (b != null) { String chk = b.checkBook(); @@ -125,7 +126,7 @@ public enum LibraryParser { String debugString = "OR_ORIG"; - private Book parseLibraryRow(HtmlTableRow r) { + private Book parseLibraryRow(HtmlPage p, HtmlTableRow r) { String xml = Util.cleanString(r.asXml()); if (r.getCells().size() == 0) @@ -160,14 +161,14 @@ public enum LibraryParser { for (BookColumns col : BookColumns.parseOrder) { HtmlElement cell = cells.get(col.ordinal()); - parseBookColumn(col, cell, b); + parseBookColumn(p, col, cell, b); } return b; } - private void parseBookColumn(BookColumns col, HtmlElement cell, Book b) { + private void parseBookColumn(HtmlPage p, BookColumns col, HtmlElement cell, Book b) { // HTMLUtil.debugNode(cell, col.name()+".xml"); String text = Util.cleanString(cell.asText()); @@ -192,24 +193,32 @@ public enum LibraryParser { anchors = cell.getElementsByTagName("a"); for (int x = 0; x < anchors.size(); x++) { HtmlAnchor a = (HtmlAnchor) anchors.get(x); - String url = a.getHrefAttribute(); + String href = a.getHrefAttribute(); // /pd/Fiction/Exodus-Audiobook/B008I3VMMQ? - if (url.startsWith("/pd/")) { - int q = url.indexOf("?"); + if (href.startsWith("/pd/")) { + URL url = p.getUrl(); + String protocol = url.getProtocol(); + String host = url.getHost(); + + int q = href.indexOf("?"); if (q != -1) - url = url.substring(0, q); + href = href.substring(0, q); boolean ok = false; - if (b.has(BookElement.asin) && url.contains(b.getAsin())) + if (b.has(BookElement.asin) && href.contains(b.getAsin())) ok = true; - if (b.has(BookElement.product_id) && url.contains(b.getProduct_id())) + if (b.has(BookElement.product_id) && href.contains(b.getProduct_id())) ok = true; - if (ok) - b.setInfoLink(url); + if (ok) { + + String full_url = String.format("%s://%s%s", protocol, host, href); + + b.setInfoLink(full_url); + } else LOG.info("Unknown product link for " + b + " at " + url); } diff --git a/src/main/java/org/openaudible/books/Book.java b/src/main/java/org/openaudible/books/Book.java index 7dfaeb7..f812c80 100644 --- a/src/main/java/org/openaudible/books/Book.java +++ b/src/main/java/org/openaudible/books/Book.java @@ -1,7 +1,12 @@ package org.openaudible.books; +import org.openaudible.util.TimeToSeconds; + import java.io.Serializable; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.HashMap; public class Book implements Comparable, Serializable { @@ -203,26 +208,26 @@ public class Book implements Comparable, Serializable { return get(BookElement.rating_average); } - public void setRating_average(String rating_average) { - set(BookElement.rating_average, rating_average); - } - public void setRating_average(double rating_average) { set(BookElement.rating_average, "" + rating_average); } + public void setRating_average(String rating_average) { + set(BookElement.rating_average, rating_average); + } + public String getRating_count() { return get(BookElement.rating_count); } - public void setRating_count(String rating_count) { - set(BookElement.rating_count, rating_count); - } - public void setRating_count(int rating_count) { set(BookElement.rating_count, "" + rating_count); } + public void setRating_count(String rating_count) { + set(BookElement.rating_count, rating_count); + } + public String getRelease_date() { return get(BookElement.release_date); } @@ -287,6 +292,28 @@ public class Book implements Comparable, Serializable { set(BookElement.purchase_date, purchaseDateText); } + public String getDurationHHMM() { + long seconds = TimeToSeconds.parseTimeStringToSeconds(getDuration()); + return TimeToSeconds.secondsToHHMM(seconds); + } + + public String getReleaseDateSortable() { + String date = getRelease_date(); + if (!date.isEmpty()) { + // 11-MAR-2015 + SimpleDateFormat parseFormat = new SimpleDateFormat("dd-MMM-yyyy"); + try { + Date d = parseFormat.parse(date); + SimpleDateFormat dispalyFormat = new SimpleDateFormat("yyyy-MM-dd"); + String out = dispalyFormat.format(d); + return out; + } catch (ParseException e) { + e.printStackTrace(); + } + } + + return date; + } public String getPurchaseDateSortable() { @@ -294,10 +321,9 @@ public class Book implements Comparable, Serializable { if (!date.isEmpty()) { String dt[] = date.split("-"); if (dt.length == 3) { - return "20"+ dt[2] + "-" + dt[0] + "-" + dt[1]; // yyyy-mm-dd for sorting and viewing + return "20" + dt[2] + "-" + dt[0] + "-" + dt[1]; // yyyy-mm-dd for sorting and viewing // warning, y3k bug - } else - { + } else { } } diff --git a/src/main/java/org/openaudible/convert/ConvertJob.java b/src/main/java/org/openaudible/convert/ConvertJob.java index 0791ba7..f4356cc 100644 --- a/src/main/java/org/openaudible/convert/ConvertJob.java +++ b/src/main/java/org/openaudible/convert/ConvertJob.java @@ -274,8 +274,13 @@ public class ConvertJob implements IQueueJob, LineListener { ok = true; if (progress != null) progress.setTask(null, "Complete"); + + long time = System.currentTimeMillis() - start; + + LOG.info("Converted your "+book+" to mp3: "+mp3.getAbsolutePath()+" size="+mp3.length()+" in " + (time / 1000L) + " seconds."); + } catch (Exception e) { - LOG.error("Error converting book:" + book, e); + LOG.error("Error converting book to MP3:" + book, e); if (progress != null) { progress.setSubTask(e.getMessage()); } @@ -288,9 +293,7 @@ public class ConvertJob implements IQueueJob, LineListener { mp3.delete(); } } - long time = System.currentTimeMillis() - start; - LOG.info("converted " + mp3.getAbsolutePath() + " " + (int) time / 1000 + " seconds."); return mp3; } diff --git a/src/main/java/org/openaudible/desktop/swt/manager/AudibleGUI.java b/src/main/java/org/openaudible/desktop/swt/manager/AudibleGUI.java index 62fa4f7..09c422b 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/AudibleGUI.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/AudibleGUI.java @@ -612,7 +612,7 @@ public class AudibleGUI implements BookListener, ConnectionListener { } - public void exportWebPage() { + public void exportWebPage(boolean showUserInterface) { try { File destDir = Directories.getDir(Directories.WEB); @@ -622,18 +622,18 @@ public class AudibleGUI implements BookListener, ConnectionListener { // sort by purchase date. list.sort((b1, b2) -> -1 * b1.getPurchaseDate().compareTo(b2.getPurchaseDate())); - PageBuilderTask task = new PageBuilderTask(destDir, list); + PageBuilderTask task = new PageBuilderTask(destDir, list, prefs.webPageIncludeMP3); ProgressDialog.doProgressTask(task); - File index = new File(destDir, "books.html"); + File index = new File(destDir, "index.html"); if (index.exists()) { - LOG.info("Book html file is: "+index.getAbsolutePath()); try { URI i = index.toURI(); String u = i.toString(); - AudibleGUI.instance.browse(u); - + LOG.info("Book html file is: "+index.getAbsolutePath()+" url="+u); + if (showUserInterface ) + AudibleGUI.instance.browse(u); } catch (Exception e) { showError(e, "displaying web page"); } @@ -978,9 +978,9 @@ public class AudibleGUI implements BookListener, ConnectionListener { final WebPage pageBuilder; final List books; - PageBuilderTask(File destDir, final List list) { + PageBuilderTask(File destDir, final List list, boolean includeMP3) { super("Creating Your Audiobook Web Page"); - pageBuilder = new WebPage(destDir, this); + pageBuilder = new WebPage(destDir, this, includeMP3); books = list; } @@ -1011,7 +1011,7 @@ public class AudibleGUI implements BookListener, ConnectionListener { if (dl.size()==0 && conv.size()==0 && prefs.autoWebPage) { - exportWebPage(); + exportWebPage(false); } } diff --git a/src/main/java/org/openaudible/desktop/swt/manager/Prefs.java b/src/main/java/org/openaudible/desktop/swt/manager/Prefs.java index c14cbb0..79f579b 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/Prefs.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/Prefs.java @@ -5,7 +5,7 @@ public class Prefs { public boolean autoConvert = true; public boolean autoDownload = false; public boolean autoWebPage = false; - + public boolean webPageIncludeMP3=true; int concurrentConversions = 3; int concurrentDownloads = 3; diff --git a/src/main/java/org/openaudible/desktop/swt/manager/Version.java b/src/main/java/org/openaudible/desktop/swt/manager/Version.java index 355b5f1..d5ec9f5 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/Version.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/Version.java @@ -3,7 +3,7 @@ package org.openaudible.desktop.swt.manager; public interface Version { String appName = "OpenAudible"; - String appVersion = "1.1.4"; + String appVersion = "1.1.5"; boolean appDebug = false; String appLink = "http://openaudible.org"; String versionLink = "http://openaudible.org/swt_version.json"; diff --git a/src/main/java/org/openaudible/desktop/swt/manager/menu/CommandCenter.java b/src/main/java/org/openaudible/desktop/swt/manager/menu/CommandCenter.java index 75feca5..e326901 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/menu/CommandCenter.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/menu/CommandCenter.java @@ -23,7 +23,9 @@ import org.openaudible.desktop.swt.manager.views.Preferences; import org.openaudible.desktop.swt.util.shop.WidgetShop; /** - * The CommandCenter is responsible to react on user-action. User action may for example occur when any item from the main menu is selected. The execute command is the main switch for running commands + * The CommandCenter is responsible to react on user-action. + * User action may for example occur when any item from the main menu is selected. + * The execute command is the main switch for running commands */ public class CommandCenter { @@ -43,10 +45,6 @@ public class CommandCenter { cb = new Clipboard(display); } - public void search() { - - } - public void userError(String s) { MessageBoxFactory.showMessage(shell, SWT.ICON_INFORMATION, GUI.i18n.getTranslation("Unexpected event"), s); } @@ -179,7 +177,7 @@ public class CommandCenter { public void execute(Command c) { CommandCenter e = this; - logger.info("Execute: " + c); + logger.info("Command: " + c); switch (c) { case About: @@ -232,7 +230,7 @@ public class CommandCenter { VersionCheck.instance.checkForUpdate(shell, true); break; case Export_Web_Page: - AudibleGUI.instance.exportWebPage(); + AudibleGUI.instance.exportWebPage(true); break; case Refresh_Book_Info: AudibleGUI.instance.refreshBookInfo(); diff --git a/src/main/java/org/openaudible/desktop/swt/manager/views/AudibleBrowser.java b/src/main/java/org/openaudible/desktop/swt/manager/views/AudibleBrowser.java index e33dc9e..f726753 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/views/AudibleBrowser.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/views/AudibleBrowser.java @@ -15,6 +15,7 @@ import org.eclipse.swt.layout.FormLayout; import org.eclipse.swt.widgets.*; import org.openaudible.Directories; import org.openaudible.audible.AudibleClient; +import org.openaudible.audible.ConnectionNotifier; import org.openaudible.desktop.swt.gui.MessageBoxFactory; import org.openaudible.desktop.swt.gui.SWTAsync; import org.openaudible.util.Platform; @@ -250,6 +251,20 @@ public class AudibleBrowser { data.bottom = new FormAttachment(100, -5); progressBar.setLayoutData(data); + LocationListener ll = new LocationListener() { + @Override + public void changing(LocationEvent locationEvent) { + + } + + @Override + public void changed(LocationEvent locationEvent) { + ConnectionNotifier.instance.setLastURL(locationEvent.location); + } + }; + + browser.addLocationListener(ll); + browser.addStatusTextListener(event -> status.setText(event.text)); } diff --git a/src/main/java/org/openaudible/desktop/swt/manager/views/BookTable.java b/src/main/java/org/openaudible/desktop/swt/manager/views/BookTable.java index 463f4e4..d44e00d 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/views/BookTable.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/views/BookTable.java @@ -125,10 +125,9 @@ public class BookTable extends EnumTable implements BookL public String getColumnDisplayable(BookTableColumn column, Book b) { String s; if (column.equals(BookTableColumn.Time)) { - //long seconds = TimeToSeconds.parseTimeStringToSeconds(b.getDuration()); // TimeToSeconds.secondsToTime() - return b.getDuration(); + return b.getDurationHHMM(); } s = super.getColumnDisplayable(column, b); @@ -153,11 +152,15 @@ public class BookTable extends EnumTable implements BookL */ case Narrated_By: return b.getNarratedBy(); + case Time: // compare duration as seconds, not as a string.. return TimeToSeconds.parseTimeStringToSeconds(b.getDuration()); case Title: return b.getFullTitle(); + + case Released: + return b.getReleaseDateSortable(); case Purchased: return b.getPurchaseDateSortable(); diff --git a/src/main/java/org/openaudible/desktop/swt/manager/views/BookTableColumn.java b/src/main/java/org/openaudible/desktop/swt/manager/views/BookTableColumn.java index 1e4d476..fa6d517 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/views/BookTableColumn.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/views/BookTableColumn.java @@ -1,8 +1,8 @@ package org.openaudible.desktop.swt.manager.views; public enum BookTableColumn { - File, Title, Author, Narrated_By, Time, Purchased; - static int widths[] = {22, 250, 150, 150, 60, 90}; + File, Title, Author, Narrated_By, Time, Purchased, Released; + static int widths[] = {22, 250, 150, 150, 50, 90, 90}; // HasAAX, HasMP3, public static int[] getWidths() { diff --git a/src/main/java/org/openaudible/desktop/swt/manager/views/Preferences.java b/src/main/java/org/openaudible/desktop/swt/manager/views/Preferences.java index bce216a..8919d40 100644 --- a/src/main/java/org/openaudible/desktop/swt/manager/views/Preferences.java +++ b/src/main/java/org/openaudible/desktop/swt/manager/views/Preferences.java @@ -78,7 +78,6 @@ public class Preferences extends Dialog { autoDownload.setSelection(AudibleGUI.instance.prefs.autoDownload); autoWebPage.setSelection(AudibleGUI.instance.prefs.autoWebPage); - } private void fetch() { diff --git a/src/main/java/org/openaudible/feeds/pagebuilder/BookInfo.java b/src/main/java/org/openaudible/feeds/pagebuilder/BookInfo.java index e56f479..e011ea3 100644 --- a/src/main/java/org/openaudible/feeds/pagebuilder/BookInfo.java +++ b/src/main/java/org/openaudible/feeds/pagebuilder/BookInfo.java @@ -3,6 +3,6 @@ package org.openaudible.feeds.pagebuilder; // The book info we want to export via json to be accessible via javascript public class BookInfo { - String title, author, narratedBy, mp3, image, description, summary, run_time, rating_average, rating_count, audible, purchased; + String title, author, narrated_by, mp3, image, description, summary, duration, rating_average, rating_count, link_url, purchase_date, release_date; } diff --git a/src/main/java/org/openaudible/feeds/pagebuilder/WebPage.java b/src/main/java/org/openaudible/feeds/pagebuilder/WebPage.java index e5f45a2..1a70d30 100644 --- a/src/main/java/org/openaudible/feeds/pagebuilder/WebPage.java +++ b/src/main/java/org/openaudible/feeds/pagebuilder/WebPage.java @@ -2,6 +2,8 @@ package org.openaudible.feeds.pagebuilder; import com.google.gson.Gson; import org.apache.commons.io.FileUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.eclipse.jetty.util.IO; import org.openaudible.Audible; import org.openaudible.BookToFilenameStrategy; @@ -18,17 +20,20 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.util.ArrayList; +import java.util.Date; import java.util.List; public class WebPage { + private static final Log LOG = LogFactory.getLog(WebPage.class); final File webDir; final IProgressTask progress; // required int thumbSize = 200; // If changed, need to change html - // final static String indexName = "books.html"; + final boolean includeMP3; - public WebPage(File dir, IProgressTask t) { + public WebPage(File dir, IProgressTask t, boolean includeMP3) { webDir = dir; progress = t; + this.includeMP3 = includeMP3; assert (t != null); } @@ -36,26 +41,28 @@ public class WebPage { BookInfo i = new BookInfo(); i.title = b.get(BookElement.fullTitle); i.author = b.get(BookElement.author); - i.narratedBy = b.get(BookElement.narratedBy); + i.narrated_by = b.get(BookElement.narratedBy); i.summary = b.get(BookElement.summary); - i.run_time = b.get(BookElement.duration); + i.duration = b.getDurationHHMM(); i.rating_average = b.get(BookElement.rating_average); i.rating_count = b.get(BookElement.rating_count); - i.audible = b.get(BookElement.infoLink); + + i.link_url = b.getInfoLink(); + i.description = b.get(BookElement.description); - i.purchased = b.getPurchaseDateSortable(); + i.purchase_date = b.getPurchaseDateSortable(); + i.release_date = b.getReleaseDateSortable(); return i; } - public void subtask(Book b, String s) throws Exception { + public void subtask(Book b, String s) throws Exception { String n = b.getShortTitle(); if (n.length() > 32) n = n.substring(0, 28) + "..."; progress.setSubTask(s + " " + n); if (progress.wasCanceled()) throw new Exception("User canceled"); - } public void buildPage(List books) throws Exception { @@ -64,61 +71,73 @@ public class WebPage { File coverImages = new File(webDir, "cover"); File thumbImages = new File(webDir, "thumb"); - if (!mp3Dir.exists()) - mp3Dir.mkdirs(); if (!coverImages.exists()) coverImages.mkdirs(); if (!thumbImages.exists()) thumbImages.mkdirs(); - - - Gson gson = new Gson(); ArrayList list = new ArrayList<>(); - ArrayList toCopy = new ArrayList<>(); - for (Book b : books) { - File mp3 = Audible.instance.getMP3FileDest(b); - if (!mp3.exists()) - continue; - String fileName = getFileName(b); // human readable, without extension. - String mp3Name = fileName + ".mp3"; - File mp3File = new File(mp3Dir, mp3Name); + if (includeMP3) { + if (!mp3Dir.exists()) + mp3Dir.mkdirs(); - if (!mp3File.exists() || mp3File.length() != mp3.length()) { - toCopy.add(b); - } - } - if (toCopy.size() > 0) { progress.setTask("Copying MP3s to Web Page Directory", ""); - int count = 1; - for (Book b : toCopy) { - if (progress.wasCanceled()) - throw new Exception("Canceled"); + ArrayList toCopy = new ArrayList<>(); + for (Book b : books) { File mp3 = Audible.instance.getMP3FileDest(b); + if (!mp3.exists()) + continue; String fileName = getFileName(b); // human readable, without extension. String mp3Name = fileName + ".mp3"; File mp3File = new File(mp3Dir, mp3Name); - progress.setTask("Copying book " + count + " of " + toCopy.size() + " to " + mp3File.getAbsolutePath()); - CopyWithProgress.copyWithProgress(progress, mp3, mp3File); + if (!mp3File.exists()) { + toCopy.add(b); + } else { + long s1 = mp3File.length(); + long s2 = mp3.length(); + long m1 = mp3File.lastModified(); + long m2 = mp3.lastModified(); - count++; + if (s1 != s2) { + String d1 = m1 != 0 ? new Date(m1).toString() : "0"; + String d2 = m2 != 0 ? new Date(m2).toString() : "0"; + LOG.info("Replacing book " + mp3.getPath() + " with " + mp3File.getPath() + " s1=" + s1 + " s2=" + s2 + " d1=" + d1 + " d2=" + d2); + boolean ok = mp3.delete(); + if (ok) + toCopy.add(b); + } + } } + if (toCopy.size() > 0) { + int count = 1; + for (Book b : toCopy) { + if (progress.wasCanceled()) + throw new Exception("Canceled"); + + File mp3 = Audible.instance.getMP3FileDest(b); + String fileName = getFileName(b); // human readable, without extension. + String mp3Name = fileName + ".mp3"; + File mp3File = new File(mp3Dir, mp3Name); + progress.setTask("Copying book " + count + " of " + toCopy.size() + " to " + mp3File.getAbsolutePath()); + CopyWithProgress.copyWithProgress(progress, mp3, mp3File); + count++; + } + } } - progress.setTask("Creating Book Web Page", ""); - for (Book b : books) { File mp3 = Audible.instance.getMP3FileDest(b); - subtask(b, "Reading"); + subtask(b, "Compiling book list"); - // only export mp3 - if (!mp3.exists()) + + // only export mp3 + if (includeMP3 && !mp3.exists()) continue; File img = Audible.instance.getImageFileDest(b); @@ -130,10 +149,11 @@ public class WebPage { File coverFile = new File(coverImages, coverName); File thumbFile = new File(thumbImages, thumbName); - File mp3File = new File(mp3Dir, mp3Name); BookInfo i = toBookInfo(b); - i.mp3 = mp3Name; + if (includeMP3) + i.mp3 = mp3Name; + if (img.exists()) { if (!coverFile.exists() || coverFile.length() != img.length()) { @@ -151,13 +171,14 @@ public class WebPage { i.image = ""; list.add(i); + if (progress.wasCanceled()) + throw new Exception("User canceled"); } - if (progress.wasCanceled()) - throw new Exception("User canceled"); progress.setTask(null, "Exporting web data"); + Gson gson = new Gson(); String json = gson.toJson(list); try (FileWriter writer = new FileWriter(new File(webDir, "books.json"))) { diff --git a/src/main/java/org/openaudible/util/TimeToSeconds.java b/src/main/java/org/openaudible/util/TimeToSeconds.java index 3434f8a..519c78a 100644 --- a/src/main/java/org/openaudible/util/TimeToSeconds.java +++ b/src/main/java/org/openaudible/util/TimeToSeconds.java @@ -70,4 +70,18 @@ public class TimeToSeconds { } } + public static String secondsToHHMM(long sec) { + + int m = (int) Math.round(sec/60.0); + int h = m/60; + m = m%60; + if (m>0 || h>0) { + String hh = h < 10 ? "0" + h : "" + h; + String mm = m < 10 ? "0" + m : "" + m; + return hh + ":" + mm; + } + return ""; + + } + } diff --git a/src/main/webapp/assets/audible.png b/src/main/webapp/assets/audible.png deleted file mode 100644 index 7c3da03cf8c46068a08aa33f29e06500f9999ca7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 526 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 literal 0 HcmV?d00001 diff --git a/src/main/webapp/assets/openaudible.js b/src/main/webapp/assets/openaudible.js new file mode 100644 index 0000000..3fae660 --- /dev/null +++ b/src/main/webapp/assets/openaudible.js @@ -0,0 +1,125 @@ +// OpenAudible.js web page to display audio book library. + +// filter out (hide) books not in filter text. If no filter text, all books are shown. +function filter() { + const text = $("#filter").val(); + const rex = new RegExp(text, 'i'); + $('.searchable tr').hide(); + $('.searchable tr').filter(function () { + return rex.test($(this).text()); + }).show(); +} + +function asString(str, len) { + if (!str) return ""; + if (len) + str = str.substring(0, len); + return str.replace(/(?:\r\n|\r|\n)/g, ' ').replace(' ', ' '); +} + +function mp3Link(book, content) { + if (!book || !book.mp3 || !content) + return ""; + return "" + content + " "; +} + +function bookImage(book, addLink) { + var image = (book && book.image !== undefined) ? ("") + : ""; + if (addLink) return mp3Link(book, image); + return image; +} +// convert the json book data to a format for the book. +function populateBooks(arr, table) { + + let i; + const data = []; + + for (i = 0; i < arr.length; i++) { + const book = arr[i]; + + // Thumbnail size 200x200 + const row = {}; + const narrated_by = asString(book.narrated_by); + const author = asString(book.author); + const title = asString(book.title); + const duration = asString(book.duration); + + row['book'] = book; + row['title'] = title; + row['narrated_by'] = narrated_by; + row['author'] = author; + row['duration'] = duration; + + row['purchase_date'] = asString(book.purchase_date); + row['release_date'] = asString(book.release_date); + row['rating'] = asString(book.rating_average); + row['summary'] = asString(book.summary, 500); + row['description'] = asString(book.description, 800); + row['mp3'] = mp3Link(book, book.mp3); + row['image'] = bookImage(book, true); + + let info = "" + title + "
"; + if (author.length > 0) + info += "by " + author + "
"; + if (narrated_by.length > 0) + info += "Narrated by " + narrated_by + "
"; + info += duration; + info += " "; + info += asString(book.rating_average, 99); + row['info'] = info; + + data.push(row); + } + + // create bootstrapTable and populate table with data. + if (table && arr.length) + { + $(table).bootstrapTable( {data: data}) + .on('click-row.bs.table', function (e, row, elem) { + showBook(row.book); // row click handler. + }); + } + + return data; +} + +// display a single book in a modal dialog. +function showBook(book) { + $("#detail_title").text(asString(book.title)); + + const image = bookImage(book, true); + $("#detail_image").html(image); + + $('#title').text(asString(book.title)); + $('#narrated_by').text(asString(book.narrated_by)); + $('#author').text(asString(book.author)); + $('#purchased').text(asString(book.purchased)); + let rating = asString(book.rating_average); + + if (rating) { + if (book.rating_count) + rating += " (" + book.rating_count + ")"; + } + + $('#rating').text(rating); + $('#duration').text(asString(book.duration)); + const summary = asString(book.summary, 9999).replace(/(?:\r\n|\r|\n)/g, '

'); + $('#summary').text(summary); + + let audible = ""; + if (book.link_url) { + audible = "" + book.audible + ""; + } + $('#audible').html(audible); + var mp3 = mp3Link(book, book.mp3); + if (mp3) { + $("#mp3").html(mp3Link(book, book.mp3)); + $("mp3_details").show(); + } else + { + $("mp3_details").hide(); + } + $("#detail_modal").modal('show'); +} + diff --git a/src/main/webapp/books.html b/src/main/webapp/books.html deleted file mode 100644 index af2d1e5..0000000 --- a/src/main/webapp/books.html +++ /dev/null @@ -1,139 +0,0 @@ - - - - - - - - - - - - - - - - - - - OpenAudible collection of audiobooks - - - - -

-
-
-
- -
- -
- - -
-
-
- - -
-
- - - - - - - - - - - - - -
TitleInfoPurchasedSummary
-
-
-
- - - - diff --git a/src/main/webapp/index.html b/src/main/webapp/index.html new file mode 100644 index 0000000..e145139 --- /dev/null +++ b/src/main/webapp/index.html @@ -0,0 +1,177 @@ + + + + + + + + + + + + + + OpenAudible + + + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+
+ + + + +
+ + +
+
+ + + + + + + + + + + + + + +
TitleAuthorNarratorDurationReleasedPurchasedDescription
+
+
+ + +
+
+ + + + + + + + + + + + +
TitleInfoReleasedPurchasedSummary
+
+
+ + +
+ + + + +