husjon / tampermonkey

3 stars 1 forks source link

Feature Request: Return & Delete library book #7

Open hillcountryfare opened 1 year ago

hillcountryfare commented 1 year ago

After you are finished with a library book it must be returned and then deleted. I added the code below to automatically return the library book, but would love to have the book automatically deleted as well. However, I wasn't sure how to code determining if a book was from a library or not. There is a line of text next to the book that reads: "This book was a Kindle digital library loan Expired on (date) This title is unavailable for download and transfer"


          log(`Clicking Return:  ${confirm}`, "debug");
    document.querySelector(`#RETURN_CONTENT_ACTION_${asin}_CONFIRM > span`).click();

I can also send you an export of the page.

husjon commented 1 year ago

I don't think this will be part of the download script, other than possible ignoring the books that are from Libraries during download.
I do not have a library card and have not used this feature so not sure how it works unfortunately.

There is a button already on the Digital Content for removing books from your personal library, does this work for removing these books or do you have to return these books individually?

husjon commented 1 year ago

Could you export the page and create a gist for it, then I can take a further look at it.

hillcountryfare commented 1 year ago

Thanks – I appreciate any help since you said this wasn’t a feature you use. If it’s too cumbersome, no worries at all. I do think many of the folks at mobileread.com would love this script, if you’d like to post about it there.

Here you go: https://gist.github.com/hillcountryfare/36b60c27a0014fa7a6955dd1e24ccee0

husjon commented 1 year ago

I've started looking into this.

To support more possible features the userscript needed some work (reference https://github.com/husjon/tampermonkey/issues/3#issuecomment-1691821682), I just pushed a rewrite on the dev which should allow me to continue looking into this feature. I've also created a separate branch for this issue in particular 7-feature-request-return-delete-library-book. For now it only have the button with no logic since I need to make sure I only target library books (your gists helps a lot).

husjon commented 1 year ago

I've added a quick example in the branch 7-feature-request-return-delete-library-book. Currently it only logs the books that it finds that matches the criteria of having the following texts assigned to it:

If it finds one, it will log Removed expired book: ${asin} then wait for 1.5 seconds before checking the next.

When done, the button will show Done!

Since I do not have any library books, could you give it a quick test to see how it acts? Attach the console log if possible, thanks in advance.

hillcountryfare commented 1 year ago

Thank you! The logic there worked well, and only indicated it was deleting on the proper books. I tested with purchased, checked out, and returned / expired.

I had previously written a script to delete the confirmation / success dialog box because I found it overly annoying and cumbersome – as one could easily tell from something else that the command was successful. I had to disable this script to get yours to work, because the script would fail referencing the element. const todelete = document.getElementById('NOTIFICATION_SUCCESS'); if (todelete) { todelete.remove(); }

husjon commented 1 year ago

Thank you for testing.

I've now updated the branch to now include the click event as you mentioned in your OP. (https://github.com/husjon/tampermonkey/pull/9/commits/df7dca40fea3534dbfa1b4f189264b81545c575d)

document.querySelector(`#RETURN_CONTENT_ACTION_${asin}_CONFIRM > span`).click();

Prior to merging, I'd appreciate a final test whenever you have the chance.

Thanks in advance. :)

hillcountryfare commented 1 year ago

That worked, but the flow for the buttons doesn’t make sense. I also realized not everyone wants a book returned as soon as they download it. My thought was to have a separate “download and return” button that: Downloads any checked downloadable book If it’s a library book: returns it If it’s a returned or expired library book: deletes it

I also put an if statement around the notification-close element, which allowed me to leave the TM script I wrote in place to delete this element. One issue I encountered with the notification-close element is a new one would be created for each book activity (download, return, etc). If you had 3 books processed by the script you’d have to close this dialog 3 times.

I attached my updated code attempt.

I sent you a message through Ko-Fi and can provide you instructions on accessing a library to checkout books.

// ==UserScript== // @name Download Kindle Books // @namespace https://github.com/husjon/tampermonkey // @version 0.3.31-issue-7 // @description Helper script for backing up a users Kindle Books // @author @husjon // @updateURL https://github.com/husjon/tampermonkey/raw/main/kindle_download_books.js // @downloadURL https://github.com/husjon/tampermonkey/raw/main/kindle_download_books.js // @supportURL https://github.com/husjon/tampermonkey/issues/new?title=Kindle%20Download%20v0.3.3%20-%20 // @match https://www.amazon.com/hz/mycd/digital-console/contentlist/booksAll/* // @icon https://www.google.com/s2/favicons?sz=64&domain=amazon.com // @grant none // ==/UserScript==

(function () { const $ = document.querySelector.bind(document); const $$ = document.querySelectorAll.bind(document); const sleep = (ms) => new Promise((r) => setTimeout(r, ms));

const buttons_container = "#FLOATING_TASK_BAR > div.filter-container > div.content-filter-item"; let button_style = null;

const download_button = document.createElement("div"); const download_and_return_button = document.createElement("div"); const remove_expired_button = document.createElement("div"); let selected_books = [];

function waitForElement(selector) { // from: https://stackoverflow.com/a/61511955 return new Promise((resolve) => { if (document.querySelector(selector)) { return resolve(document.querySelector(selector)); }

  const observer = new MutationObserver((mutations) => {
    if (document.querySelector(selector)) {
      observer.disconnect();
      resolve(document.querySelector(selector));
    }
  });

  observer.observe(document.body, {
    childList: true,
    subtree: true,
  });
});

}

function add_button(base_button, id, text, listener) { base_button.className = "action_button"; base_button.id = id; base_button.innerText = text; base_button.style.cssText = button_style; base_button.style.width = "auto"; base_button.style.padding = "0px 5px"; base_button.style.opacity = selected_books.length > 0 ? 1.0 : 0.25; base_button.style.marginLeft = "0.8rem";

base_button.addEventListener("click", listener);

$(buttons_container).append(base_button);
console.log("Added download button");

}

function update_selection(event) { const asin = event.target.id.replace(":KindleEBook", "");

if (event.target.checked) {
  selected_books.push(asin);
} else {
  selected_books = selected_books.filter((item) => {
    return item !== asin;
  });
}

download_button.style.opacity = selected_books.length > 0 ? 1.0 : 0.25;
remove_expired_button.style.opacity = selected_books.length > 0 ? 1.0 : 0.25;
download_and_return_button.style.opacity = selected_books.length > 0 ? 1.0 : 0.25;

}

function update_event_listeners() { let all_checkboxes = [...$$("[type=checkbox]")].filter((checkbox) => checkbox.id.includes("KindleEBook"), );

all_checkboxes.map((element) => {
  element.removeEventListener("change", update_selection);
  element.addEventListener("change", update_selection);
});

let search_button = $("#search-button > div");
search_button.removeEventListener("click", initialize);
search_button.addEventListener("click", initialize);

$$("#pagination .page-item").forEach((button) => {
  button.addEventListener("click", initialize);
});

console.log("Updated event listeners");

}

async function remove_expired_books() { for (const asin of selected_books) { await remove_expired_book(asin); }

let original_button_text = remove_expired_button.innerText;
remove_expired_button.innerText = "Done!";
await sleep(3000);
remove_expired_button.innerText = original_button_text;

}

async function remove_expired_book(asin) { console.log(Checking for expiry: ${asin});

let title = $(`#content-title-${asin}`);
let rows = title.parentElement.querySelectorAll(".information_row span");
if (
  rows[0].innerText === "This book was a Kindle digital library loan" &&
  rows[1].innerText === "Expire on"
) {
  document
    .querySelector(`#RETURN_CONTENT_ACTION_${asin}_CONFIRM > span`)
    .click();

  console.log(`Removed expired book: ${asin}`);
  await sleep(1500);
}

return;

}

async function download_books() { console.log(Downloading: ${selected_books.join(", ")}); for (const asin of selected_books) { console.log(Downloading: ${asin}); await download(asin); await sleep(1500); // Delay needed to let the previous download to start. } }

async function download_and_return_books() {
    console.log(`Downloading then returning: ${selected_books.join(", ")}`);
    for (const asin of selected_books) {
        console.log(`Downloading: ${asin}`);
        await download(asin);
        await sleep(1500); // Delay needed to let the previous download to start.
     if (document.getElementById("#RETURN_CONTENT_ACTION_${asin}")) {
        console.log('Returning Book during loop');
        document
    .querySelector(`#RETURN_CONTENT_ACTION_${asin}_CONFIRM > span`)
    .click();
    await sleep(1500)}; // everything else needs a delay
        console.log('Deleting Book during loop');
     if (document.getElementById("#DELETE_TITLE_ACTION_${asin}")) {
        document
        .querySelector(`#DELETE_TITLE_ACTION_${asin}_CONFIRM > span`)
    .click()};

    }
}

async function download(asin) { if (document.getElementById("#download_and_transferlist${asin}_0")) { $(#download_and_transfer_list_${asin}_0).click(); await waitForElement( #DOWNLOAD_AND_TRANSFER_ACTION_${asin}_CONFIRM > span, ).then((obj) => { obj.click(); })}; if (document.getElementById("#notification-close")) { await waitForElement("#notification-close").then((obj) => { obj.click(); })}; }

function initialize() { waitForElement("#CONTENT_LIST").then(() => { button_style = $("#SELECT-ALL").style.cssText + "font-size: 13px;";

  // Add `Download Selected` button
  add_button(
    download_button,
    "DOWNLOAD",
    "Download Selected",
    download_books,
  );
  // Add `Download & Return Selected` button
  add_button(
    download_and_return_button,
    "DOWNLOADRETURN",
    "Download and Return Selected",
    download_and_return_books,
  );
  // Add `Remove Expired` button
  add_button(
    remove_expired_button,
    "REMOVE_EXPIRED_BOOKS",
    "Removed Expired",
    remove_expired_books,
  );

  // Add Event Listeners for pagination buttons and checkboxes
  update_event_listeners();
});

}

initialize(); })();

hillcountryfare commented 1 year ago

Hey, I found the issue with my if statements. I’ve got this working 99% the way I want, now to just clean up the logging. I’ll submit a fixed version today.

From: Adam Brock Sent: Saturday, September 9, 2023 20:30 To: husjon/tampermonkey @.***> Subject: RE: [husjon/tampermonkey] Feature Request: Return & Delete library book (Issue #7)

That worked, but the flow for the buttons doesn’t make sense. I also realized not everyone wants a book returned as soon as they download it. My thought was to have a separate “download and return” button that: Downloads any checked downloadable book If it’s a library book: returns it If it’s a returned or expired library book: deletes it

I also put an if statement around the notification-close element, which allowed me to leave the TM script I wrote in place to delete this element. One issue I encountered with the notification-close element is a new one would be created for each book activity (download, return, etc). If you had 3 books processed by the script you’d have to close this dialog 3 times.

I attached my updated code attempt.

I sent you a message through Ko-Fi and can provide you instructions on accessing a library to checkout books.

hillcountryfare commented 1 year ago

I've got this to a point where I'm content, but feel free to make changes. The process does require running the script twice (with a page refresh) to go from downloading and returning a book because when you click return the book disappears from the page. You also can't delete without first returning, but this isn't a big deal for me. Download Kindle Books.txt

husjon commented 1 year ago

Awesome and thank you. I'll take a look at it some time this coming week. I should be able to get around the running the script twice part as the list of books just needs to be updated if a book was removed.

husjon commented 1 year ago

As we discussed on Discord, I've rewritten the script as a Library extending it will different functionality as described in userscripts/libraries/kindle_helper/README.md. An example userscript using this library can be found at userscripts/scripts/kindle_download.user.js (Click Raw to install it). It will work exactly like the original script.

All of this is currently on a separate branch while some further testing happens but it will be merged very soon.