NewtokiRipper

Download Images From Newtoki

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

// ==UserScript==
// @name        NewtokiRipper
// @namespace   adrian
// @author      adrian
// @match       https://newtoki468.com/webtoon/*
// @include     /^https:\/\/newtoki[0-9]+\.com\/webtoon/.*$/
// @version     1.5
// @description Download Images From Newtoki
// @require     https://cdn.jsdelivr.net/npm/@violentmonkey/shortcut@1
// @require     https://unpkg.com/@zip.js/[email protected]/dist/zip-full.min.js
// @grant       GM_registerMenuCommand
// @license     MIT
// ==/UserScript==

// helpers

const fetchImage = async (url) => {
  const blob = await fetch(url, {
    headers: {
      accept:
        "image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8",
      "accept-language": "en-US,en;q=0.9",
      "sec-ch-ua": '"Chromium";v="135", "Not-A.Brand";v="8"',
      "sec-ch-ua-mobile": "?0",
      "sec-ch-ua-platform": '"macOS"',
      "sec-fetch-dest": "image",
      "sec-fetch-mode": "no-cors",
      "sec-fetch-site": "cross-site",
      "sec-fetch-storage-access": "active",
    },
    referrer: "https://newtoki469.com/",
    referrerPolicy: "strict-origin-when-cross-origin",
    body: null,
    method: "GET",
    mode: "cors",
    credentials: "omit",
  }).then((r) => r.blob());
  return blob;
};

function getImages() {
  return [...document.getElementsByTagName("img")].flatMap((img) => {
    const attr = [...img.attributes].find((a) =>
      /^data-[a-zA-Z0-9]{1,20}/.test(a.name),
    );
    const src = attr?.value;
    return src?.startsWith("https://img") && src?.includes("newtoki")
      ? [src]
      : [];
  });
}

// UI
const UI_ID = Array.from(
  { length: 12 },
  () =>
    "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"[
      Math.floor(Math.random() * 52)
    ],
).join("");

const css = `
  @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&display=swap');

  #${UI_ID}-btn {
    position: fixed;
    right: 16px;
    bottom: 16px;
    z-index: 999999;
    padding: 8px 14px;
    background: #e00;
    color: #fff;
    font: 700 13px/1 'JetBrains Mono', monospace;
    letter-spacing: 0.05em;
    border: none;
    border-radius: 4px;
    box-shadow: 0 2px 0 #900, 0 4px 12px rgba(220,0,0,.3);
    cursor: pointer;
    transition: box-shadow .15s, transform .1s, background .15s;
  }
  #${UI_ID}-btn:hover {
    background: #cc0000;
    box-shadow: 0 2px 0 #900, 0 6px 18px rgba(220,0,0,.45);
  }
  #${UI_ID}-btn:active {
    transform: translateY(2px);
    box-shadow: 0 0 0 #900, 0 2px 8px rgba(220,0,0,.3);
  }
  #${UI_ID}-btn:disabled {
    background: #2a2a2a;
    color: #555;
    box-shadow: 0 2px 0 #111;
    cursor: not-allowed;
    transform: none;
  }

  #${UI_ID}-toast {
    position: fixed;
    left: 50%;
    bottom: 60px;
    transform: translateX(-50%);
    z-index: 999999;
    min-width: 260px;
    max-width: 380px;
    background: rgba(10,10,10,.97);
    color: #e0e0e0;
    font-family: 'JetBrains Mono', monospace;
    padding: 14px 16px;
    border-radius: 4px;
    border: 1px solid #2a2a2a;
    border-left: 2px solid #e00;
    box-shadow: 0 8px 32px rgba(0,0,0,.6);
    pointer-events: none;
    opacity: 0;
    transition: opacity .2s;
  }
  #${UI_ID}-toast.visible { opacity: 1; }
  #${UI_ID}-toast-title {
    font-size: 13px;
    font-weight: 700;
    color: #fff;
    margin-bottom: 4px;
  }
  #${UI_ID}-toast-title::before {
    content: '> ';
    color: #e00;
  }
  #${UI_ID}-toast-msg {
    font-size: 12px;
    font-weight: 400;
    color: #666;
    padding-left: 14px;
  }
  #${UI_ID}-toast-bar-wrap {
    background: #1a1a1a;
    border-radius: 2px;
    height: 3px;
    margin-top: 10px;
    overflow: hidden;
    display: none;
  }
  #${UI_ID}-toast-bar {
    height: 100%;
    width: 0%;
    background: #e00;
    border-radius: 2px;
    transition: width .2s;
    box-shadow: 0 0 6px rgba(220,0,0,.6);
  }
`;

function injectStyles() {
  const style = document.createElement("style");
  style.textContent = css;
  document.head.appendChild(style);
}

function createUI() {
  const btn = document.createElement("button");
  btn.id = `${UI_ID}-btn`;
  btn.textContent = "⬇ Download";
  btn.addEventListener("click", downloadImages);
  document.body.appendChild(btn);

  const toast = document.createElement("div");
  toast.id = `${UI_ID}-toast`;
  toast.innerHTML = `
    <div id="${UI_ID}-toast-title"></div>
    <div id="${UI_ID}-toast-msg"></div>
    <div id="${UI_ID}-toast-bar-wrap"><div id="${UI_ID}-toast-bar"></div></div>
  `;
  document.body.appendChild(toast);
}

let toastTimeout;
const ui = {
  get btn() {
    return document.getElementById(`${UI_ID}-btn`);
  },
  get toast() {
    return document.getElementById(`${UI_ID}-toast`);
  },
  get title() {
    return document.getElementById(`${UI_ID}-toast-title`);
  },
  get msg() {
    return document.getElementById(`${UI_ID}-toast-msg`);
  },
  get barWrap() {
    return document.getElementById(`${UI_ID}-toast-bar-wrap`);
  },
  get bar() {
    return document.getElementById(`${UI_ID}-toast-bar`);
  },

  show(title, msg = "", { progress = null, autohide = 0 } = {}) {
    clearTimeout(toastTimeout);
    this.title.textContent = title;
    this.msg.textContent = msg;
    if (progress !== null) {
      this.barWrap.style.display = "block";
      this.bar.style.width = `${progress}%`;
    } else {
      this.barWrap.style.display = "none";
    }
    this.toast.classList.add("visible");
    if (autohide) toastTimeout = setTimeout(() => this.hide(), autohide);
  },

  hide() {
    this.toast.classList.remove("visible");
  },

  setProgress(done, total) {
    this.show(`Downloading… ${done}/${total}`, "", {
      progress: (done / total) * 100,
    });
  },

  setBusy(busy) {
    this.btn.disabled = busy;
    this.btn.textContent = busy ? "Downloading…" : "⬇ Download";
  },
};

// main shit

const downloadImages = async () => {
  ui.setBusy(true);
  ui.show("Starting…");
  try {
    const images = getImages();

    if (!images.length) {
      ui.show("Nothing to download", "No images found.", { autohide: 5000 });
      return;
    }

    ui.show(`Found ${images.length} images`, "Starting download…", {
      progress: 0,
    });

    const zipWriter = new zip.ZipWriter(new zip.BlobWriter("application/zip"), {
      bufferedWrite: true,
    });
    let done = 0;

    await Promise.all(
      images.map(async (url, i) => {
        const blob = await fetchImage(url);
        await zipWriter.add(`${i + 1}.jpg`, new zip.BlobReader(blob));
        ui.setProgress(++done, images.length);
      }),
    );

    ui.show("Generating zip…", "", { progress: 100 });
    const blobURL = URL.createObjectURL(await zipWriter.close());

    const link = document.createElement("a");
    link.href = blobURL;
    link.download = `${document.title}.zip`;
    link.click();

    ui.show("Done!", `${images.length} images ripped.`, { autohide: 5000 });
  } catch (e) {
    console.error(e);
    ui.show("Error", e.message, { autohide: 5000 });
  } finally {
    ui.setBusy(false);
  }
};

// init shit

injectStyles();
createUI();
GM_registerMenuCommand("Download Images (Ctrl/Cmd + S)", downloadImages);
VM.shortcut.register("cm-s", downloadImages);
VM.shortcut.enable();