version 1.1.5 updates

New web page. Attempt to work better with connecting to audible.
This commit is contained in:
openaudible 2018-08-12 02:03:59 -07:00
parent 05ca633393
commit a4db8f8a12
24 changed files with 608 additions and 274 deletions

1
.gitignore vendored
View file

@ -19,6 +19,7 @@ unused/
caches caches
Desktop.ini Desktop.ini
Thumbs.db Thumbs.db
*/Thumbs.db
# java # java
*.class *.class

View file

@ -121,6 +121,18 @@ public class Audible implements IQueueListener<Book> {
} }
} }
// 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) { boolean takeBook(Book b) {
if (!ok(b)) { if (!ok(b)) {
LOG.warn("invalid book: " + checkBook(b)); LOG.warn("invalid book: " + checkBook(b));
@ -130,6 +142,9 @@ public class Audible implements IQueueListener<Book> {
if (!hasBook(b)) { if (!hasBook(b)) {
if (!ignoreBook(b)) { if (!ignoreBook(b)) {
synchronized (books) { synchronized (books) {
normalizeBook(b);
books.put(b.getProduct_id(), b); books.put(b.getProduct_id(), b);
} }
BookNotifier.getInstance().bookAdded(b); BookNotifier.getInstance().bookAdded(b);
@ -363,8 +378,9 @@ public class Audible implements IQueueListener<Book> {
booksUpdated++; booksUpdated++;
} }
} }
if (booksUpdated > 0 && quick)
updateLibrary(false); // if (booksUpdated > 0 && quick)
// updateLibrary(false);
LOG.info("Updated " + list.size() + " books"); LOG.info("Updated " + list.size() + " books");
@ -722,7 +738,7 @@ public class Audible implements IQueueListener<Book> {
@Override @Override
public void jobCompleted(ThreadedQueue<Book> queue, IQueueJob job, Book b) { public void jobCompleted(ThreadedQueue<Book> 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) { if (queue == downloadQueue) {
try { try {
AAXParser.instance.update(b); AAXParser.instance.update(b);

View file

@ -108,7 +108,10 @@ public class AudibleScraper {
for (BasicClientCookie bc : list) { for (BasicClientCookie bc : list) {
Cookie c = new Cookie(bc.getDomain(), bc.getName(), bc.getValue()); Cookie c = new Cookie(bc.getDomain(), bc.getName(), bc.getValue());
cm.addCookie(c); 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()); FileUtils.writeByteArrayToFile(cookiesFile, o.getBytes());
} }
static int maxLoginAttempts = 2;
protected boolean login() throws IOException { protected boolean login() throws IOException {
return login(0);
}
protected boolean login(int attempt) throws IOException {
AudibleAccountPrefs copy = account; AudibleAccountPrefs copy = account;
@ -187,6 +194,8 @@ public class AudibleScraper {
HtmlForm login = page.getFormByName("signIn"); HtmlForm login = page.getFormByName("signIn");
if (login == null) { if (login == null) {
// TODO: find sign-in anchor and click it..
LOG.info("login form not found for page:" + page.getTitleText()); LOG.info("login form not found for page:" + page.getTitleText());
return false; return false;
} }
@ -220,7 +229,7 @@ public class AudibleScraper {
HtmlElement captchaImageDiv = findById("ap_captcha_img"); HtmlElement captchaImageDiv = findById("ap_captcha_img");
if (captchaImageDiv != null || ap_captcha_table != null) { if (captchaImageDiv != null || ap_captcha_table != null) {
LOG.info("Appears to be a captcha... I am a bot.");
return false; return false;
} }
@ -240,7 +249,10 @@ public class AudibleScraper {
LOG.info(page.getUrl()); LOG.info(page.getUrl());
LOG.info("Login failed, see html files at:" + HTMLUtil.debugFile("submitting-credentials").getAbsolutePath() + " and " + HTMLUtil.debugFile("login failed").getAbsolutePath()); LOG.info("Login failed, see html files at:" + HTMLUtil.debugFile("submitting-credentials").getAbsolutePath() + " and " + HTMLUtil.debugFile("login failed").getAbsolutePath());
if (attempt<maxLoginAttempts)
{
login(attempt+1);
}
} else { } else {
HTMLUtil.debugFile("submitting-credentials").delete(); HTMLUtil.debugFile("submitting-credentials").delete();
@ -278,13 +290,12 @@ public class AudibleScraper {
if (signOut == null && accountDetails == null && signIn == null) { if (signOut == null && accountDetails == null && signIn == null) {
HTMLUtil.debugNode(page, "checkLoggedIn"); HTMLUtil.debugNode(page, "checkLoggedIn");
} }
return isLoggedIn(); return isLoggedIn();
} }
public String homeURL() { public String homeURL() {
return "/access"; return "/";
} }
public String getPageURL() { public String getPageURL() {
@ -308,8 +319,7 @@ public class AudibleScraper {
if (true) if (true)
getWebClient().setJavascriptEnabled(true); getWebClient().setJavascriptEnabled(true);
try { try {
setURL(homeURL()); setURL(homeURL(), "Loading web page...");
// HTMLUtil.debugNode(page, "homeURL");
if (checkLoggedIn()) if (checkLoggedIn())
return; return;
@ -355,32 +365,66 @@ public class AudibleScraper {
return null; return null;
} }
/* public boolean setURLAndLogIn(String u) throws IOException {
public boolean clickLib() throws Exception { setURL(u);
HtmlAnchor lib=null; if (!checkLoggedIn()) {
for (HtmlAnchor n : page.getAnchors()) { LOG.info("not logged in after going to:"+u);
if (n.getHrefAttribute().contains("/lib")) // trouble.. try again
lib = n; login();
return checkLoggedIn();
} }
return true;
if (lib!=null) {
setPage(lib.click());
return true;
} else {
return false;
}
} }
*/
public void lib() throws Exception { public void lib() throws Exception {
String browserURL = ConnectionNotifier.instance.getLastURL();
if (!browserURL.isEmpty())
{
LOG.info("Using library location from browser: "+browserURL);
if (setURLAndLogIn(browserURL))
return;
}
if (!setURLAndLogIn("/lib"))
throw new Exception("Unable to access your library. Try logging in with Browser (Cmd-B) to view your library page and try again.. \n\nThere may also be a change in audible's web site that has broken this code.");
/*
HtmlAnchor lib=null;
if (page!=null)
{
String debug = "";
for (HtmlAnchor a : page.getAnchors())
{
String ref = a.getHrefAttribute();
debug += ref+"\n";
if (a.getHrefAttribute().startsWith("/lib"))
{
lib = a;
browserURL = ref;
break;
}
}
// LOG.info(debug);
}
if (lib!=null)
{
page = lib.click();
if (!checkLoggedIn()) {
LOG.info("Clicked lib, but not logged in anymore.");
// trouble.. try again
login();
if (!checkLoggedIn())
throw new Exception("Got logged out. Try logging in with Browser to your library page and try again..");
}
return;
}
boolean ok = setURLAndLogIn("/lib");
setURL("/lib"); setURL("/lib");
if (!checkLoggedIn()) { if (!checkLoggedIn()) {
@ -391,6 +435,7 @@ public class AudibleScraper {
setURL("/lib"); setURL("/lib");
} }
*/
} }
@ -493,7 +538,8 @@ public class AudibleScraper {
if (next == null) { if (next == null) {
assert (pageNum == 1); assert (pageNum == 1);
setURL("/lib", "Reading Library..."); lib();
// setURL("/lib", "Reading Library...");
setPageFilter(); setPageFilter();
} else { } else {
@ -565,8 +611,8 @@ public class AudibleScraper {
private void setPageFilter() { private void setPageFilter() {
try { try {
DomElement purchaseDateFilter = page.getElementByName("purchaseDateFilter"); DomElement purchaseDateFilter = page.getElementByName("purchaseDateFilter");
if (purchaseDateFilter != null && purchaseDateFilter instanceof HtmlSelect) {
HtmlSelect h = (HtmlSelect) purchaseDateFilter; HtmlSelect h = (HtmlSelect) purchaseDateFilter;
int i = h.getSelectedIndex(); int i = h.getSelectedIndex();
if (i != 0) { if (i != 0) {
HtmlOption all = h.getOption(0); HtmlOption all = h.getOption(0);
@ -590,11 +636,11 @@ public class AudibleScraper {
} }
return; return;
} else {
LOG.info("warning: did not find library filter htmlSelect.");
}
} catch (Throwable th) { } catch (Throwable th) {
LOG.error("Unable to set purchaseDateFilter.", th); LOG.info("Unable to set purchaseDateFilter. Writing debug log to no_date.html. This may mean we are unable to get all of your books. You may need to log in with the Browser and set the filters to show all books.");
HTMLUtil.debugNode(page, "no_date.html");
} }
} }

View file

@ -1,6 +1,8 @@
package org.openaudible.audible; package org.openaudible.audible;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.openaudible.AudibleAccountPrefs; import org.openaudible.AudibleAccountPrefs;
import org.openaudible.util.EventNotifier; import org.openaudible.util.EventNotifier;
@ -8,6 +10,11 @@ import org.openaudible.util.EventNotifier;
public class ConnectionNotifier extends EventNotifier<ConnectionListener> implements ConnectionListener { public class ConnectionNotifier extends EventNotifier<ConnectionListener> implements ConnectionListener {
public static final ConnectionNotifier instance = new ConnectionNotifier(); public static final ConnectionNotifier instance = new ConnectionNotifier();
State state = State.Not_Connected; State state = State.Not_Connected;
private static final Log LOG = LogFactory.getLog(ConnectionNotifier.class);
private String lastURL="";
private ConnectionNotifier() { private ConnectionNotifier() {
} }
@ -18,11 +25,15 @@ public class ConnectionNotifier extends EventNotifier<ConnectionListener> implem
@Override @Override
public void connectionChanged(boolean connected) { public void connectionChanged(boolean connected) {
state = connected ? State.Connected : State.Disconnected;
for (ConnectionListener l : getListeners()) { State newState = connected ? State.Connected : State.Disconnected;
l.connectionChanged(connected); if (state!=newState) {
state = newState;
for (ConnectionListener l : getListeners()) {
l.connectionChanged(connected);
}
} }
} }
public boolean isConnected() { public boolean isConnected() {
@ -57,6 +68,15 @@ public class ConnectionNotifier extends EventNotifier<ConnectionListener> implem
return getState() == State.Disconnected; 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. // not connected is unknown.
// connected means in account // connected means in account

View file

@ -8,6 +8,7 @@ import org.openaudible.books.BookElement;
import org.openaudible.util.HTMLUtil; import org.openaudible.util.HTMLUtil;
import org.openaudible.util.Util; import org.openaudible.util.Util;
import java.net.URL;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
@ -64,11 +65,11 @@ public enum LibraryParser {
} }
public ArrayList<Book> parseLibraryFragment(DomNode fragment) { public ArrayList<Book> parseLibraryFragment(HtmlPage p) {
ArrayList<Book> list = new ArrayList<>(); ArrayList<Book> list = new ArrayList<>();
ArrayList<String> colNames = new ArrayList<>(); ArrayList<String> colNames = new ArrayList<>();
HtmlTable table = fragment.getFirstByXPath("//table"); HtmlTable table = p.getFirstByXPath("//table");
if (table == null) if (table == null)
return list; return list;
@ -103,7 +104,7 @@ public enum LibraryParser {
rindex++; rindex++;
if (rindex == 1) if (rindex == 1)
continue; // skip header row. continue; // skip header row.
Book b = parseLibraryRow(r); Book b = parseLibraryRow(p, r);
if (b != null) { if (b != null) {
String chk = b.checkBook(); String chk = b.checkBook();
@ -125,7 +126,7 @@ public enum LibraryParser {
String debugString = "OR_ORIG"; String debugString = "OR_ORIG";
private Book parseLibraryRow(HtmlTableRow r) { private Book parseLibraryRow(HtmlPage p, HtmlTableRow r) {
String xml = Util.cleanString(r.asXml()); String xml = Util.cleanString(r.asXml());
if (r.getCells().size() == 0) if (r.getCells().size() == 0)
@ -160,14 +161,14 @@ public enum LibraryParser {
for (BookColumns col : BookColumns.parseOrder) { for (BookColumns col : BookColumns.parseOrder) {
HtmlElement cell = cells.get(col.ordinal()); HtmlElement cell = cells.get(col.ordinal());
parseBookColumn(col, cell, b); parseBookColumn(p, col, cell, b);
} }
return 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"); // HTMLUtil.debugNode(cell, col.name()+".xml");
String text = Util.cleanString(cell.asText()); String text = Util.cleanString(cell.asText());
@ -192,24 +193,32 @@ public enum LibraryParser {
anchors = cell.getElementsByTagName("a"); anchors = cell.getElementsByTagName("a");
for (int x = 0; x < anchors.size(); x++) { for (int x = 0; x < anchors.size(); x++) {
HtmlAnchor a = (HtmlAnchor) anchors.get(x); HtmlAnchor a = (HtmlAnchor) anchors.get(x);
String url = a.getHrefAttribute(); String href = a.getHrefAttribute();
// /pd/Fiction/Exodus-Audiobook/B008I3VMMQ? // /pd/Fiction/Exodus-Audiobook/B008I3VMMQ?
if (url.startsWith("/pd/")) { if (href.startsWith("/pd/")) {
int q = url.indexOf("?"); URL url = p.getUrl();
String protocol = url.getProtocol();
String host = url.getHost();
int q = href.indexOf("?");
if (q != -1) if (q != -1)
url = url.substring(0, q); href = href.substring(0, q);
boolean ok = false; boolean ok = false;
if (b.has(BookElement.asin) && url.contains(b.getAsin())) if (b.has(BookElement.asin) && href.contains(b.getAsin()))
ok = true; 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; ok = true;
if (ok) if (ok) {
b.setInfoLink(url);
String full_url = String.format("%s://%s%s", protocol, host, href);
b.setInfoLink(full_url);
}
else else
LOG.info("Unknown product link for " + b + " at " + url); LOG.info("Unknown product link for " + b + " at " + url);
} }

View file

@ -1,7 +1,12 @@
package org.openaudible.books; package org.openaudible.books;
import org.openaudible.util.TimeToSeconds;
import java.io.Serializable; import java.io.Serializable;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
public class Book implements Comparable<Book>, Serializable { public class Book implements Comparable<Book>, Serializable {
@ -203,26 +208,26 @@ public class Book implements Comparable<Book>, Serializable {
return get(BookElement.rating_average); 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) { public void setRating_average(double rating_average) {
set(BookElement.rating_average, "" + 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() { public String getRating_count() {
return get(BookElement.rating_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) { public void setRating_count(int rating_count) {
set(BookElement.rating_count, "" + 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() { public String getRelease_date() {
return get(BookElement.release_date); return get(BookElement.release_date);
} }
@ -287,6 +292,28 @@ public class Book implements Comparable<Book>, Serializable {
set(BookElement.purchase_date, purchaseDateText); 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() { public String getPurchaseDateSortable() {
@ -294,10 +321,9 @@ public class Book implements Comparable<Book>, Serializable {
if (!date.isEmpty()) { if (!date.isEmpty()) {
String dt[] = date.split("-"); String dt[] = date.split("-");
if (dt.length == 3) { 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 // warning, y3k bug
} else } else {
{
} }
} }

View file

@ -274,8 +274,13 @@ public class ConvertJob implements IQueueJob, LineListener {
ok = true; ok = true;
if (progress != null) if (progress != null)
progress.setTask(null, "Complete"); 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) { } catch (Exception e) {
LOG.error("Error converting book:" + book, e); LOG.error("Error converting book to MP3:" + book, e);
if (progress != null) { if (progress != null) {
progress.setSubTask(e.getMessage()); progress.setSubTask(e.getMessage());
} }
@ -288,9 +293,7 @@ public class ConvertJob implements IQueueJob, LineListener {
mp3.delete(); mp3.delete();
} }
} }
long time = System.currentTimeMillis() - start;
LOG.info("converted " + mp3.getAbsolutePath() + " " + (int) time / 1000 + " seconds.");
return mp3; return mp3;
} }

View file

@ -612,7 +612,7 @@ public class AudibleGUI implements BookListener, ConnectionListener {
} }
public void exportWebPage() { public void exportWebPage(boolean showUserInterface) {
try { try {
File destDir = Directories.getDir(Directories.WEB); File destDir = Directories.getDir(Directories.WEB);
@ -622,18 +622,18 @@ public class AudibleGUI implements BookListener, ConnectionListener {
// sort by purchase date. // sort by purchase date.
list.sort((b1, b2) -> -1 * b1.getPurchaseDate().compareTo(b2.getPurchaseDate())); 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); ProgressDialog.doProgressTask(task);
File index = new File(destDir, "books.html"); File index = new File(destDir, "index.html");
if (index.exists()) { if (index.exists()) {
LOG.info("Book html file is: "+index.getAbsolutePath());
try { try {
URI i = index.toURI(); URI i = index.toURI();
String u = i.toString(); 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) { } catch (Exception e) {
showError(e, "displaying web page"); showError(e, "displaying web page");
} }
@ -978,9 +978,9 @@ public class AudibleGUI implements BookListener, ConnectionListener {
final WebPage pageBuilder; final WebPage pageBuilder;
final List<Book> books; final List<Book> books;
PageBuilderTask(File destDir, final List<Book> list) { PageBuilderTask(File destDir, final List<Book> list, boolean includeMP3) {
super("Creating Your Audiobook Web Page"); super("Creating Your Audiobook Web Page");
pageBuilder = new WebPage(destDir, this); pageBuilder = new WebPage(destDir, this, includeMP3);
books = list; books = list;
} }
@ -1011,7 +1011,7 @@ public class AudibleGUI implements BookListener, ConnectionListener {
if (dl.size()==0 && conv.size()==0 && prefs.autoWebPage) if (dl.size()==0 && conv.size()==0 && prefs.autoWebPage)
{ {
exportWebPage(); exportWebPage(false);
} }
} }

View file

@ -5,7 +5,7 @@ public class Prefs {
public boolean autoConvert = true; public boolean autoConvert = true;
public boolean autoDownload = false; public boolean autoDownload = false;
public boolean autoWebPage = false; public boolean autoWebPage = false;
public boolean webPageIncludeMP3=true;
int concurrentConversions = 3; int concurrentConversions = 3;
int concurrentDownloads = 3; int concurrentDownloads = 3;

View file

@ -3,7 +3,7 @@ package org.openaudible.desktop.swt.manager;
public interface Version { public interface Version {
String appName = "OpenAudible"; String appName = "OpenAudible";
String appVersion = "1.1.4"; String appVersion = "1.1.5";
boolean appDebug = false; boolean appDebug = false;
String appLink = "http://openaudible.org"; String appLink = "http://openaudible.org";
String versionLink = "http://openaudible.org/swt_version.json"; String versionLink = "http://openaudible.org/swt_version.json";

View file

@ -23,7 +23,9 @@ import org.openaudible.desktop.swt.manager.views.Preferences;
import org.openaudible.desktop.swt.util.shop.WidgetShop; 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 { public class CommandCenter {
@ -43,10 +45,6 @@ public class CommandCenter {
cb = new Clipboard(display); cb = new Clipboard(display);
} }
public void search() {
}
public void userError(String s) { public void userError(String s) {
MessageBoxFactory.showMessage(shell, SWT.ICON_INFORMATION, GUI.i18n.getTranslation("Unexpected event"), 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) { public void execute(Command c) {
CommandCenter e = this; CommandCenter e = this;
logger.info("Execute: " + c); logger.info("Command: " + c);
switch (c) { switch (c) {
case About: case About:
@ -232,7 +230,7 @@ public class CommandCenter {
VersionCheck.instance.checkForUpdate(shell, true); VersionCheck.instance.checkForUpdate(shell, true);
break; break;
case Export_Web_Page: case Export_Web_Page:
AudibleGUI.instance.exportWebPage(); AudibleGUI.instance.exportWebPage(true);
break; break;
case Refresh_Book_Info: case Refresh_Book_Info:
AudibleGUI.instance.refreshBookInfo(); AudibleGUI.instance.refreshBookInfo();

View file

@ -15,6 +15,7 @@ import org.eclipse.swt.layout.FormLayout;
import org.eclipse.swt.widgets.*; import org.eclipse.swt.widgets.*;
import org.openaudible.Directories; import org.openaudible.Directories;
import org.openaudible.audible.AudibleClient; import org.openaudible.audible.AudibleClient;
import org.openaudible.audible.ConnectionNotifier;
import org.openaudible.desktop.swt.gui.MessageBoxFactory; import org.openaudible.desktop.swt.gui.MessageBoxFactory;
import org.openaudible.desktop.swt.gui.SWTAsync; import org.openaudible.desktop.swt.gui.SWTAsync;
import org.openaudible.util.Platform; import org.openaudible.util.Platform;
@ -250,6 +251,20 @@ public class AudibleBrowser {
data.bottom = new FormAttachment(100, -5); data.bottom = new FormAttachment(100, -5);
progressBar.setLayoutData(data); 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)); browser.addStatusTextListener(event -> status.setText(event.text));
} }

View file

@ -125,10 +125,9 @@ public class BookTable extends EnumTable<Book, BookTableColumn> implements BookL
public String getColumnDisplayable(BookTableColumn column, Book b) { public String getColumnDisplayable(BookTableColumn column, Book b) {
String s; String s;
if (column.equals(BookTableColumn.Time)) { if (column.equals(BookTableColumn.Time)) {
//long seconds = TimeToSeconds.parseTimeStringToSeconds(b.getDuration()); //long seconds = TimeToSeconds.parseTimeStringToSeconds(b.getDuration());
// TimeToSeconds.secondsToTime() // TimeToSeconds.secondsToTime()
return b.getDuration(); return b.getDurationHHMM();
} }
s = super.getColumnDisplayable(column, b); s = super.getColumnDisplayable(column, b);
@ -153,11 +152,15 @@ public class BookTable extends EnumTable<Book, BookTableColumn> implements BookL
*/ */
case Narrated_By: case Narrated_By:
return b.getNarratedBy(); return b.getNarratedBy();
case Time: case Time:
// compare duration as seconds, not as a string.. // compare duration as seconds, not as a string..
return TimeToSeconds.parseTimeStringToSeconds(b.getDuration()); return TimeToSeconds.parseTimeStringToSeconds(b.getDuration());
case Title: case Title:
return b.getFullTitle(); return b.getFullTitle();
case Released:
return b.getReleaseDateSortable();
case Purchased: case Purchased:
return b.getPurchaseDateSortable(); return b.getPurchaseDateSortable();

View file

@ -1,8 +1,8 @@
package org.openaudible.desktop.swt.manager.views; package org.openaudible.desktop.swt.manager.views;
public enum BookTableColumn { public enum BookTableColumn {
File, Title, Author, Narrated_By, Time, Purchased; File, Title, Author, Narrated_By, Time, Purchased, Released;
static int widths[] = {22, 250, 150, 150, 60, 90}; static int widths[] = {22, 250, 150, 150, 50, 90, 90};
// HasAAX, HasMP3, // HasAAX, HasMP3,
public static int[] getWidths() { public static int[] getWidths() {

View file

@ -78,7 +78,6 @@ public class Preferences extends Dialog {
autoDownload.setSelection(AudibleGUI.instance.prefs.autoDownload); autoDownload.setSelection(AudibleGUI.instance.prefs.autoDownload);
autoWebPage.setSelection(AudibleGUI.instance.prefs.autoWebPage); autoWebPage.setSelection(AudibleGUI.instance.prefs.autoWebPage);
} }
private void fetch() { private void fetch() {

View file

@ -3,6 +3,6 @@ package org.openaudible.feeds.pagebuilder;
// The book info we want to export via json to be accessible via javascript // The book info we want to export via json to be accessible via javascript
public class BookInfo { 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;
} }

View file

@ -2,6 +2,8 @@ package org.openaudible.feeds.pagebuilder;
import com.google.gson.Gson; import com.google.gson.Gson;
import org.apache.commons.io.FileUtils; 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.eclipse.jetty.util.IO;
import org.openaudible.Audible; import org.openaudible.Audible;
import org.openaudible.BookToFilenameStrategy; import org.openaudible.BookToFilenameStrategy;
@ -18,17 +20,20 @@ import java.io.File;
import java.io.FileWriter; import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Date;
import java.util.List; import java.util.List;
public class WebPage { public class WebPage {
private static final Log LOG = LogFactory.getLog(WebPage.class);
final File webDir; final File webDir;
final IProgressTask progress; // required final IProgressTask progress; // required
int thumbSize = 200; // If changed, need to change html 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; webDir = dir;
progress = t; progress = t;
this.includeMP3 = includeMP3;
assert (t != null); assert (t != null);
} }
@ -36,26 +41,28 @@ public class WebPage {
BookInfo i = new BookInfo(); BookInfo i = new BookInfo();
i.title = b.get(BookElement.fullTitle); i.title = b.get(BookElement.fullTitle);
i.author = b.get(BookElement.author); 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.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_average = b.get(BookElement.rating_average);
i.rating_count = b.get(BookElement.rating_count); 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.description = b.get(BookElement.description);
i.purchased = b.getPurchaseDateSortable(); i.purchase_date = b.getPurchaseDateSortable();
i.release_date = b.getReleaseDateSortable();
return i; return i;
} }
public void subtask(Book b, String s) throws Exception {
public void subtask(Book b, String s) throws Exception {
String n = b.getShortTitle(); String n = b.getShortTitle();
if (n.length() > 32) if (n.length() > 32)
n = n.substring(0, 28) + "..."; n = n.substring(0, 28) + "...";
progress.setSubTask(s + " " + n); progress.setSubTask(s + " " + n);
if (progress.wasCanceled()) if (progress.wasCanceled())
throw new Exception("User canceled"); throw new Exception("User canceled");
} }
public void buildPage(List<Book> books) throws Exception { public void buildPage(List<Book> books) throws Exception {
@ -64,61 +71,73 @@ public class WebPage {
File coverImages = new File(webDir, "cover"); File coverImages = new File(webDir, "cover");
File thumbImages = new File(webDir, "thumb"); File thumbImages = new File(webDir, "thumb");
if (!mp3Dir.exists())
mp3Dir.mkdirs();
if (!coverImages.exists()) if (!coverImages.exists())
coverImages.mkdirs(); coverImages.mkdirs();
if (!thumbImages.exists()) if (!thumbImages.exists())
thumbImages.mkdirs(); thumbImages.mkdirs();
Gson gson = new Gson();
ArrayList<BookInfo> list = new ArrayList<>(); ArrayList<BookInfo> list = new ArrayList<>();
ArrayList<Book> toCopy = new ArrayList<>(); if (includeMP3) {
for (Book b : books) { if (!mp3Dir.exists())
File mp3 = Audible.instance.getMP3FileDest(b); mp3Dir.mkdirs();
if (!mp3.exists())
continue;
String fileName = getFileName(b); // human readable, without extension.
String mp3Name = fileName + ".mp3";
File mp3File = new File(mp3Dir, mp3Name);
if (!mp3File.exists() || mp3File.length() != mp3.length()) {
toCopy.add(b);
}
}
if (toCopy.size() > 0) {
progress.setTask("Copying MP3s to Web Page Directory", ""); progress.setTask("Copying MP3s to Web Page Directory", "");
int count = 1;
for (Book b : toCopy) {
if (progress.wasCanceled())
throw new Exception("Canceled");
ArrayList<Book> toCopy = new ArrayList<>();
for (Book b : books) {
File mp3 = Audible.instance.getMP3FileDest(b); File mp3 = Audible.instance.getMP3FileDest(b);
if (!mp3.exists())
continue;
String fileName = getFileName(b); // human readable, without extension. String fileName = getFileName(b); // human readable, without extension.
String mp3Name = fileName + ".mp3"; String mp3Name = fileName + ".mp3";
File mp3File = new File(mp3Dir, mp3Name); 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", ""); progress.setTask("Creating Book Web Page", "");
for (Book b : books) { for (Book b : books) {
File mp3 = Audible.instance.getMP3FileDest(b); 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; continue;
File img = Audible.instance.getImageFileDest(b); File img = Audible.instance.getImageFileDest(b);
@ -130,10 +149,11 @@ public class WebPage {
File coverFile = new File(coverImages, coverName); File coverFile = new File(coverImages, coverName);
File thumbFile = new File(thumbImages, thumbName); File thumbFile = new File(thumbImages, thumbName);
File mp3File = new File(mp3Dir, mp3Name);
BookInfo i = toBookInfo(b); BookInfo i = toBookInfo(b);
i.mp3 = mp3Name; if (includeMP3)
i.mp3 = mp3Name;
if (img.exists()) { if (img.exists()) {
if (!coverFile.exists() || coverFile.length() != img.length()) { if (!coverFile.exists() || coverFile.length() != img.length()) {
@ -151,13 +171,14 @@ public class WebPage {
i.image = ""; i.image = "";
list.add(i); 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"); progress.setTask(null, "Exporting web data");
Gson gson = new Gson();
String json = gson.toJson(list); String json = gson.toJson(list);
try (FileWriter writer = new FileWriter(new File(webDir, "books.json"))) { try (FileWriter writer = new FileWriter(new File(webDir, "books.json"))) {

View file

@ -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 "";
}
} }

Binary file not shown.

Before

Width:  |  Height:  |  Size: 526 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 574 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.3 KiB

View file

@ -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 "<a href='mp3/" + encodeURIComponent(book.mp3) + "'>" + content + "</a> ";
}
function bookImage(book, addLink) {
var image = (book && book.image !== undefined) ? ("<img src='thumb/" + encodeURIComponent(book.image) + "' width='200' height='200'>")
: "<img src='assets/no_cover.png' width='200' height='200'>";
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 = "<strong>" + title + "</strong><br>";
if (author.length > 0)
info += "by <i>" + author + "</i><br>";
if (narrated_by.length > 0)
info += "Narrated by " + narrated_by + "<br>";
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, '<p />');
$('#summary').text(summary);
let audible = "";
if (book.link_url) {
audible = "<a href='"+ book.link_url + "'>" + book.audible + "</a>";
}
$('#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');
}

View file

@ -1,139 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.10.1/bootstrap-table.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.10.1/bootstrap-table.min.js"></script>
<link rel="stylesheet" type="text/css" href="assets/books.css">
<!-- Created by OpenAudible export web page. -->
<script src="books.js" type="text/javascript"></script>
<script>
function filter() {
var text = $("#filter").val();
var rex = new RegExp(text, 'i');
$('.searchable tr').hide();
$('.searchable tr').filter(function () {
return rex.test($(this).text());
}).show();
}
$(document).ready(function () {
populateBooks(window.myBooks);
$('#filter').keyup(filter);
});
function trunc(str, len) {
if (str === undefined) return "";
return str.substring(0, len);
}
function resizeSummary(table) {
table.find('th:nth-child(2), td:nth-child(2)').css('width', '200');
}
function populateBooks(arr) {
var i;
var data = [];
for (i = 0; i < arr.length; i++) {
var book = arr[i];
var rating = book.rating_average = (book.rating_average !== undefined) ? book.rating_average : "";
var author = book.author = (book.author !== undefined) ? trunc(book.author, 20) : "";
var narratedBy = book.narratedBy = (book.narratedBy !== undefined) ? trunc(book.narratedBy, 20) : "";
var run_time = book.run_time = (book.run_time !== undefined) ? book.run_time : "";
var summary = trunc(book.description, 2048);
summary = summary.replace(/(?:\r\n|\r|\n)/g, '<p />');
var thumb = "<img src='assets/download.jpg'>";
if (book.image !== undefined)
thumb = "<img src='thumb/" + encodeURIComponent(book.image) + "' width='200' height='200'>"; // Thumbnail size
var linkName = trunc(book.title, 90);
var mp3 = encodeURIComponent(book.mp3);
var thumbLink = "<a href='mp3/" + mp3 + "'>" + thumb + "</a> ";
var info = "<strong>" + trunc(book.title, 50) + "</strong><br>";
if (author.length > 0)
info += "by <i>" + author + "</i><br>";
if (narratedBy.length > 0)
info += "Narrated by " + narratedBy + "<br>";
info += run_time;
info += " ";
info += trunc(book.rating_average, 99);
var row = {};
row['title'] = thumbLink;
row['info'] = info;
row['purchased'] = book.purchased;
row['summary'] = summary;
data.push(row);
}
$('#table').bootstrapTable({data: data});
}
</script>
<title>OpenAudible collection of audiobooks</title>
</head>
<body>
<div id="container">
<div class="row">
<div class="col-md-4 col-md-offset-2">
<div class="input-group">
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button>
</div>
<input type="text" class="form-control" placeholder="Search" name="srch-term" id="filter">
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<table id="table" class="table table-striped">
<thead>
<tr>
<th data-field="title" data-sortable="true">Title</th>
<th data-field="info" data-sortable="true">Info</th>
<th data-field="purchased" data-sortable="true">Purchased</th>
<th data-field="summary" data-sortable="false">Summary</th>
</tr>
</thead>
<tbody class="searchable">
</tbody>
</table>
</div>
</div>
</div>
</body>
</html>

177
src/main/webapp/index.html Normal file
View file

@ -0,0 +1,177 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<!-- Generated by openaudible.org -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.10.1/bootstrap-table.min.css">
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.4/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.6/js/bootstrap.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/bootstrap-table/1.10.1/bootstrap-table.min.js"></script>
<link rel="stylesheet" type="text/css" href="assets/books.css">
<script src="assets/openaudible.js" type="text/javascript"></script>
<script src="books.js" type="text/javascript"></script> <!-- Created by OpenAudible export web page. -->
<title>OpenAudible</title>
</head>
<body>
<div id="container">
<div class="row"> <!-- search filter row -->
<div class="col-md-3">
<div class="input-group">
<div class="input-group-btn">
<button class="btn btn-default" type="submit"><i class="glyphicon glyphicon-search"></i></button>
</div>
<input type="text" class="form-control" placeholder="Search" name="srch-term" id="filter">
</div>
</div>
<div class="col-md-2">
<div class="form-group">
<label><input type="checkbox" name="checkbox" id="show_images">Show Images</label>
</div>
</div>
<div class="col-md-2 align-text-bottom text-center">
<strong><a href="http://openaudible.org">OpenAudible</a></strong>
</div>
</div>
<div class="row" class="book_table" id="compact_view"> <!-- Table row -->
<div class="col-md-12">
<table id="compact_table" class="table table-striped">
<thead>
<tr>
<th data-field="title" data-sortable="true">Title</th>
<th data-field="author" data-sortable="true">Author</th>
<th data-field="narrated_by" data-sortable="true">Narrator</th>
<th data-field="duration" data-sortable="true">Duration</th>
<th data-field="release_date" data-sortable="true">Released</th>
<th data-field="purchase_date" data-sortable="true">Purchased</th>
<th data-field="description" data-sortable="true">Description</th>
</tr>
</thead>
<tbody class="searchable"> <!-- Filled in using json. -->
</tbody>
</table>
</div>
</div>
<div class="row" class="book_table" id="image_view"> <!-- Table row -->
<div class="col-md-12">
<table id="image_table" class="table table-striped">
<thead>
<tr>
<th data-field="image" data-sortable="true">Title</th>
<th data-field="info" data-sortable="true">Info</th>
<th data-field="release_date" data-sortable="true">Released</th>
<th data-field="purchase_date" data-sortable="true">Purchased</th>
<th data-field="summary" data-sortable="false">Summary</th>
</tr>
</thead>
<tbody class="searchable"> <!-- Body of table filled in using populateBooks . -->
</tbody>
</table>
</div>
</div>
<div id="detail_modal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myModalLabel"
aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
<h4 class="modal-title" id="detail_title"></h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-5" id="detail_image">
</div>
<div class="col-md-7" id="detail_details">
<table class="table">
<tbody>
<tr>
<th class="detail_key">Author:</th>
<td class="detail_value" id="author">Value</td>
</tr>
<tr>
<th class="detail_key">Narrated By:</th>
<td class="detail_value" id="narrated_by">Value</td>
</tr>
<tr>
<th class="detail_key">Duration:</th>
<td class="detail_value" id="duration">Value</td>
</tr>
<tr>
<th class="detail_key">Purchased</th>
<td class="detail_value" id="purchase_date">Value</td>
</tr>
<tr>
<th class="detail_key">Rating:</th>
<td class="detail_value" id="rating">Value</td>
</tr>
<tr id="mp3_details">
<th class="detail_key">MP3:</th>
<td class="detail_value" id="mp3">Value</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div id="summary" class='summary'></div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
function showImages(showImages) {
console.log("showImages:" + showImages);
if (showImages) {
$("#image_table").show();
$("#compact_table").hide();
populateBooks(window.myBooks, "#image_table");
} else {
$("#image_table").hide();
$("#compact_table").show();
populateBooks(window.myBooks, "#compact_table");
}
}
$(document).ready(function () {
// handle show image checkbox change
$('#show_images').change(function () {
showImages($(this).prop('checked'));
});
showImages(false); // display
$('#filter').keyup(filter);
});
</script>
</body>
</html>