diff --git a/.jshintrc b/.jshintrc index de03ce5..e6e25b7 100644 --- a/.jshintrc +++ b/.jshintrc @@ -26,6 +26,8 @@ "randomString": true, "Queue": true, "chrome": true, - "TEMPORARY": true + "TEMPORARY": true, + "current_version": true, + "MetaData": true } } diff --git a/download.zip b/download.zip index 451fe2a..044a9f9 100644 Binary files a/download.zip and b/download.zip differ diff --git a/extension.crx b/extension.crx index eb90083..40a435d 100644 Binary files a/extension.crx and b/extension.crx differ diff --git a/source/backgroundManager.js b/source/backgroundManager.js index 7aa4ba6..d9c7816 100644 --- a/source/backgroundManager.js +++ b/source/backgroundManager.js @@ -140,7 +140,7 @@ getSettings(function() { callback({ what: "completed_zipping" }); - }); + }, request.comment); return true; } else if(request.what == "download_blob") { diff --git a/source/comicReader.js b/source/comicReader.js index 93a2fa2..db61759 100644 --- a/source/comicReader.js +++ b/source/comicReader.js @@ -74,7 +74,8 @@ getSettings(function() { port.send({ what: "message_to_opener", message: { - what: "ready_to_download" + what: "ready_to_download", + data: getName() } }, function(start) { if(start) @@ -94,7 +95,7 @@ getSettings(function() { data: perc } }); - }); + }, start.metaData); else if(start.exploit) { port.send({ what: "unlink_from_opener" @@ -530,7 +531,7 @@ function setupSelectors() { // run a DOM scan to analyse how the reader DOM tree } // download the opened comic. a callback and a step function can be used. -function loadComic(callback, step) { +function loadComic(callback, step, metaData) { addTopBar(); overlay.style.display = "block"; @@ -551,10 +552,12 @@ function loadComic(callback, step) { if(!dom.getCanvasContainer() || dom.loaderVisible() || !dom.countCanvas()) // delay download if comic isn't displayed yet => reader not ready, first page is not loaded yet, first page is not displayed yet return setTimeout(function() { - loadComic(callback, step); + loadComic(callback, step, metaData); }, 100); var pos = -1, + skipCount = 0, + skipStreak = 0, numLength = String(l - 1).length, nextPage = function(callback) { clearTimeout(noChangeTimeout); @@ -575,13 +578,20 @@ function loadComic(callback, step) { swapPage(function() { // Only swap if no other actor already swapped pages: var canvasContainer = dom.canvasContainer; + if(changeWaiter === callback) { + + // Delay page skips if they occurred rarely so far or more than one in a row: + var delayFactor = Math.min(skipCount / pos, 1) > 0.3 && skipStreak < 2 ? 1 : 2; + noChangeTimeout = setTimeout(function() { if(changeWaiter === callback && !dom.isLoading() && dom.isActivePage(fig) && canvasContainer === dom.canvasContainer) { changeWaiter = null; + skipCount++; + skipStreak++; nextPage(callback); } - }, settings.pageSkipDelay); + }, settings.pageSkipDelay * delayFactor); realClick(fig); } }); @@ -593,6 +603,7 @@ function loadComic(callback, step) { interval = function() { nextPage(function() { getOpenedPage(function(page) { + skipStreak = 0; port.send({ what: "add_page", page: (settings.container != 2 ? page : null), @@ -636,7 +647,7 @@ function loadComic(callback, step) { zipImages(function() { step("save"); downloadBlob(getName() + "." + (settings.container ? "zip" : "cbz"), done); - }); + }, metaData); } }, rmListener = function() { @@ -755,7 +766,7 @@ function downloadData(name, data, overwrite, callback) { // overwrite is not use } // compress and download all pages that were backuped by this tab in the loadComic function -function zipImages(callback) { +function zipImages(callback, comment) { if(settings.container == 2) return typeof callback === "function" ? callback() : undefined; renderFaviconPercentage(1); @@ -763,7 +774,8 @@ function zipImages(callback) { div.style.lineHeight = "50px"; port.send({ - what: "start_zipping" + what: "start_zipping", + comment: comment }, function(result) { renderFaviconPercentage(1); div.innerHTML = "Saving comic..."; diff --git a/source/common.js b/source/common.js index 6d9ec45..744b04d 100644 --- a/source/common.js +++ b/source/common.js @@ -1,6 +1,6 @@ "use strict"; -var current_version = 117, +var current_version = 118, div, linkStyle = "color:#ffffff;font-weight:bold;background:linear-gradient(to bottom, rgb(115, 152, 200) 0%,rgb(179, 206, 233) 1%,rgb(82, 142, 204) 5%,rgb(79, 137, 200) 20%,rgb(66, 120, 184) 50%,rgb(49, 97, 161) 100%);padding:3px;text-decoration:none;display:inline-block;width:70px;text-align:center;height:22px;box-sizing:border-box;line-height:14px;border:1px solid rgb(49,96,166);", settings; diff --git a/source/manifest.json b/source/manifest.json index 06ab69a..f3714a2 100644 --- a/source/manifest.json +++ b/source/manifest.json @@ -3,7 +3,7 @@ "name": "Comixology Backup", "description": "Save your Comixology comics as standard CBZ files.", - "version": "2.2.0", + "version": "2.3.0", "minimum_chrome_version": "39", "icons": { @@ -32,16 +32,16 @@ }, "content_scripts": [{ - "matches": ["*://www.comixology.com/comic-reader/*", "*://www.comixology.eu/comic-reader/*", "*://www.comixology.co.uk/comic-reader/*", "*://www.comixology.fr/comic-reader/*", "*://www.comixology.de/comic-reader/*", "*://comicstore.marvel.com/comic-reader/*"], + "matches": ["*://www.comixology.com/comic-reader/*", "*://www.comixology.eu/comic-reader/*", "*://www.comixology.co.uk/comic-reader/*", "*://www.comixology.fr/comic-reader/*", "*://www.comixology.de/comic-reader/*", "*://comicstore.marvel.com/comic-reader/*", "*://www.readdcentertainment.com/comic-reader/*"], "js": ["common.js", "reactivateDom.js"], "run_at": "document_start" }, { - "matches": ["*://www.comixology.com/comic-reader/*", "*://www.comixology.eu/comic-reader/*", "*://www.comixology.co.uk/comic-reader/*", "*://www.comixology.fr/comic-reader/*", "*://www.comixology.de/comic-reader/*", "*://comicstore.marvel.com/comic-reader/*"], + "matches": ["*://www.comixology.com/comic-reader/*", "*://www.comixology.eu/comic-reader/*", "*://www.comixology.co.uk/comic-reader/*", "*://www.comixology.fr/comic-reader/*", "*://www.comixology.de/comic-reader/*", "*://comicstore.marvel.com/comic-reader/*", "*://www.readdcentertainment.com/comic-reader/*"], "js": ["diffMatchPatch.js", "common.js", "comicReader.js"], "run_at": "document_idle" }, { - "matches": ["*://www.comixology.com/my-*", "*://www.comixology.eu/my-*", "*://www.comixology.co.uk/my-*", "*://www.comixology.fr/my-*", "*://www.comixology.de/my-*", "*://comicstore.marvel.com/my-*", "*://www.comixology.com/wishlists*", "*://www.comixology.eu/wishlists", "*://www.comixology.co.uk/wishlists", "*://www.comixology.fr/wishlists", "*://www.comixology.de/wishlists", "*://comicstore.marvel.com/wishlists"], - "js": ["common.js", "myBooks.js"], + "matches": ["*://www.comixology.com/my-*", "*://www.comixology.eu/my-*", "*://www.comixology.co.uk/my-*", "*://www.comixology.fr/my-*", "*://www.comixology.de/my-*", "*://comicstore.marvel.com/my-*", "*://www.readdcentertainment.com/my-*", "*://www.comixology.com/wishlists*", "*://www.comixology.eu/wishlists", "*://www.comixology.co.uk/wishlists", "*://www.comixology.fr/wishlists", "*://www.comixology.de/wishlists", "*://comicstore.marvel.com/wishlists", "*://www.readdcentertainment.com/wishlists"], + "js": ["common.js", "myBooks.js", "metaData.js"], "run_at": "document_end" }], diff --git a/source/metaData.js b/source/metaData.js new file mode 100644 index 0000000..7e77591 --- /dev/null +++ b/source/metaData.js @@ -0,0 +1,261 @@ +"use strict"; + +function MetaData() { + + //We may want to support different outputmodes later on + //For now, there is only one: https://code.google.com/p/comicbookinfo/wiki/Example + //This is used by Calibre + this.outputMode = "CBI"; + + //This object will contain the actual metadata in a format + //that should allow easy download via STRINGIFY + this.JSONMD = { + appID: "Comixology Backup/" + current_version, + lastModified: new Date().toISOString().replace(/T|Z/g, " ") + "+0000", + "ComicBookInfo/1.0": { + series: "", + title: "", + publisher: "", + publicationMonth: "", + publicationYear: "", + issue: "", + numberOfIssues: "", + volume: "", + nubmerOfVolumes: "", + rating: "", + genre: "", + language: "", + country: "", + credits: [] + } + }; + + //This is a shortcut to the actual data, it's there so I can fill detail + //data in without having to repeat the key - I doubt it'll ever change, + //but if we ever switch to ComicBookInfo/2.0 , it'll be easier to maintain :-) + this.JSONData = this.JSONMD["ComicBookInfo/1.0"]; + + //In order to declare the first person in a role, I need to remember somwhere if we + //already had a first - so I'm creating a simply array with all the known roles + //that will go INTO the metadata (this is NOT what comes from Comixology!) + this.firstRoles = { + Writer: true, + Artist: true, + Letterer: true, + Colorer: true, + Editor: true, + Cover: true + }; + + //I need to know the first time I add credits, because PUSH apparently doesn't work on + //empty arrays + this.firstCredit = true; +} + +MetaData.prototype = { + scanMeta(p_bookItem) { + var bookItem = p_bookItem; + //To find the metadata, we need to find the top container for this comic + //unfortunately, they're not marked with a special id :-( + //var bookItem = readButton.parentNode; + + //Try to find the detail container, do nothing if it's not there - it means we're not on a detail page + if(bookItem.className != null) + try { + if(bookItem.className == "lv2-item-action-row") { + while(bookItem !== undefined && bookItem.className != "lv2-item-detail") + bookItem = bookItem.parentNode; + + //A bit of a gamble, I'm assuming there's always just one + + var itemTitleRaw = bookItem.getElementsByClassName("lv2-title-container")[0]; + var itemCreditRaw = bookItem.getElementsByTagName("aside")[0]; + + //Ignore the actual hierarchy, just take all DLs, which should contain DD/DT pairs with credits + var allCredits = itemCreditRaw.getElementsByTagName("dl"); + for(var i = 0; i < allCredits.length; i++) { + //There should be only one DT/DD anyway + var oneDT = allCredits[i].getElementsByTagName("dt")[0]; + var oneDD = allCredits[i].getElementsByTagName("dd")[0]; + var oneDTlc = oneDT.innerText.toLowerCase(); + + if(oneDTlc == "full series") { + this.addSeries(oneDD.innerText); + } + else if(oneDTlc == "writer" || oneDTlc == "written by" || oneDTlc == "by") { + this.addWriter(oneDD.innerText); + } + else if(oneDTlc == "inks") { + this.addInks(oneDD.innerText); + } + else if(oneDTlc == "cover by" || oneDTlc == "cover") { + this.addCover(oneDD.innerText); + } + else if(oneDTlc == "art" || oneDTlc == "penciler" || oneDTlc == "pencils") { + this.addPencil(oneDD.innerText); + } + else if(oneDTlc == "colored by" || oneDTlc == "colorist") { + this.addColor(oneDD.innerText); + } + else if(oneDTlc == "editor") { + this.addEditor(oneDD.innerText); + } + } + + //We know that under lv2-title-container there should be a single node with lv2-item-number + var itemNumber = itemTitleRaw.getElementsByClassName("lv2-item-number")[0].innerText.match(/\d+/); + //This will only work in some cases - apparently sometimes there are title additions in the issue field + //still, better than nothing? + itemNumber = parseInt(itemNumber && itemNumber[0]); + if(!isNaN(itemNumber)) + this.addIssue(itemNumber); + } + } + catch(err) { + //Die silently... + //alert(err); + } + + return this; + }, + + addPerson(newPerson, Role) { + var aPerson = { + person: newPerson, + role: Role, + primary: this.firstRoles[Role] + }; + this.firstRoles[Role] = false; + + if(this.firstCredit === true) + this.JSONData.credits = [aPerson]; + else + this.JSONData.credits.push(aPerson); + this.firstCredit = false; + + return this; + }, + + //Add a list of allowed modes to check against? + changeOutputMode(newOutputMode) { + this.outputMode = newOutputMode; + + return this; + }, + + //Since all data is currently in a JSON object, we need methods + //to fill them, instead of simply accessing the object + addSeries(newSeries) { + this.JSONData.series = newSeries; + + return this; + }, + addTitle(newTitle) { + this.JSONData.title = newTitle; + + return this; + }, + addPublisher(newPublisher) { + this.JSONData.publisher = newPublisher; + + return this; + }, + addPublicationMonth(newPublicationMonth) { + this.JSONData.publicationMonth = newPublicationMonth; + + return this; + }, + addPublicationYear(newPublicationYear) { + this.JSONData.publicationYear = newPublicationYear; + + return this; + }, + addIssue(newIssue) { + this.JSONData.issue = newIssue; + + return this; + }, + addNumberOfIssues(newNumberOfIssues) { + this.JSONData.numberOfIssues = newNumberOfIssues; + + return this; + }, + addVolume(newVolume) { + this.JSONData.volume = newVolume; + + return this; + }, + addNumberOfVolumes(newNumberOfVolumes) { + this.JSONData.numberOfVolumes = newNumberOfVolumes; + + return this; + }, + addRating(newRating) { + this.JSONData.rating = newRating; + + return this; + }, + addGenre(newGenre) { + this.JSONData.genre = newGenre; + + return this; + }, + addLanguage(newLanguage) { + this.JSONData.language = newLanguage; + + return this; + }, + addCountry(newCountry) { + this.JSONData.country = newCountry; + + return this; + }, + + //Persons get their own functions, that way we can control where specific roles go + //Inks and Pencils may end up in Artist for now, but perhaps later they'll be split? + addWriter(newWriter) { + this.addPerson(newWriter, "Writer"); + + return this; + }, + addEditor(newEditor) { + this.addPerson(newEditor, "Editor"); + + return this; + }, + addInks(newInks) { + this.addPerson(newInks, "Artist"); + + return this; + }, + addCover(newCover) { + this.addPerson(newCover, "Cover"); + + return this; + }, + addPencil(newPencil) { + this.addPerson(newPencil, "Artist"); + + return this; + }, + addColor(newColor) { + this.addPerson(newColor, "Colorer"); + + return this; + }, + + //Output - there may be multiple output modes later, so don't just + //use JSON.STRINGIFY from the outside + toString(outputMode) { + var retString = ""; + var useMode = outputMode; + + if(useMode === undefined) + useMode = this.outputMode; + + if(useMode == "CBI") + retString = JSON.stringify(this.JSONMD); + + return retString; + } +}; diff --git a/source/myBooks.js b/source/myBooks.js index aa77ca0..a2504d7 100644 --- a/source/myBooks.js +++ b/source/myBooks.js @@ -31,7 +31,7 @@ function init() { Download.cleanUp(); var readButtons = document.body.querySelectorAll(readButtonSelector); for(var i = 0; i < readButtons.length; i++) - new Download(readButtons[i].href).show(readButtons[i]); + new Download(readButtons[i].href, readButtons[i]).show(readButtons[i]); } var cssClass = randomString(20, 40), @@ -45,11 +45,14 @@ var cssClass = randomString(20, 40), }, downloadEvents = {}; -function Download(comicHref) { +function Download(comicHref, readButton) { if(Download.connections[comicHref]) return Download.connections[comicHref]; + //Metadata container, will be used in "show" + var metaData = this.metaData = new MetaData(); + this.comicHref = comicHref; this.id = Download.counter++; @@ -124,6 +127,9 @@ Download.prototype = { if(button.href !== this.comicHref || this.buttons.has(button)) // after switching pages via ajax new button html elements are created, those will be linked to the internal download object return; + //Get Metadata + this.metaData.scanMeta(button.parentNode); + var clone = button.cloneNode(false), buttonComputedStyle = window.getComputedStyle(button); @@ -146,7 +152,6 @@ Download.prototype = { text: clone.querySelector("span.text." + randomId), progressBG: "linear-gradient(to right, rgba(0,0,0,0.4) 0%, rgba(0,0,0,0.4) {X}%, rgba(0,0,0,0) {X}%, rgba(0,0,0,0) 100%), " + buttonComputedStyle.background }); - if(settings.selectors) { clone.addEventListener("click", function() { this[this.cancelable ? "cancel" : "start"](); @@ -226,14 +231,15 @@ Download.prototype = { // event handlers (receiving messages from the downloading tab): events: { - ready_to_download(callback) { + ready_to_download(callback, comicName) { if(this.inactive) callback({ exploit: true }); else { callback({ - download: true + download: true, + metaData: this.metaData.addTitle(comicName).toString() }); this.showProgress(0); } diff --git a/source/options.html b/source/options.html index a4b28de..6cebde6 100644 --- a/source/options.html +++ b/source/options.html @@ -118,7 +118,7 @@ - v2.2.0 by Cortys + v2.3.0 by Cortys

Comixology Backup Settings

Backup behaviour

diff --git a/source/zip/zip.js b/source/zip/zip.js index c4fe6ef..4fe12e2 100755 --- a/source/zip/zip.js +++ b/source/zip/zip.js @@ -800,7 +800,7 @@ else writeFile(); }, - close : function(callback) { + close : function(callback, comment) { if (this._worker) { this._worker.terminate(); this._worker = null; @@ -811,7 +811,7 @@ file = files[filenames[indexFilename]]; length += 46 + file.filename.length + file.comment.length; } - data = getDataHelper(length + 22); + data = getDataHelper(length + 22 + comment.length); for (indexFilename = 0; indexFilename < filenames.length; indexFilename++) { file = files[filenames[indexFilename]]; data.view.setUint32(index, 0x504b0102); @@ -830,6 +830,14 @@ data.view.setUint16(index + 10, filenames.length, true); data.view.setUint32(index + 12, length, true); data.view.setUint32(index + 16, datalength, true); + data.view.setUint16(index + 20, comment.length, true); + + //ZIP comments (not file comments!) are at the end of the file + for (var i = 0; i < comment.length ; i++) + { + data.view.setUint8(index + i + 22, comment.charCodeAt(i)); + } + writer.writeUint8Array(data.array, function() { writer.getData(callback); }, onwriteerror); diff --git a/version b/version index 5bc6609..415196e 100644 --- a/version +++ b/version @@ -1 +1 @@ -117 +118