// ==UserScript==
// @name Poipiku Downloader
// @name:zh-CN Poipiku下载器
// @name:ja ポイピク ダウンローダー
// @description Download images or text from Poipiku
// @description:zh-CN 从Poipiku下载图片或文字
// @description:ja Download images or text from Poipiku
// @author calary
// @namespace http://tampermonkey.net/
// @version 0.4.4
// @license GPL-3.0
// @include http*://poipiku.com*
// @match https://poipiku.com/
// @connect img.poipiku.com
// @connect img-org.poipiku.com
// @icon https://poipiku.com/favicon.ico
// @require https://cdn.bootcdn.net/ajax/libs/jquery/2.2.4/jquery.min.js
// @require https://cdn.bootcdn.net/ajax/libs/jszip/3.7.1/jszip.min.js
// @require https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @run-at document-end
// ==/UserScript==
jQuery(function ($) {
const lang = (
window.navigator.language ||
window.navigator.browserLanguage ||
"en-us"
).toLowerCase();
const i18nMap = {
"en-us": {
ui_logined: "Logined",
ui_password: "Password",
ui_qualitytrue: "You can download high quality images.",
ui_qualityfalse: "You cannot download high quality images.",
ui_mode: "Rename image with page id",
btn_downloadimages: "Save images (.zip)",
btn_downloadimageseperately: "Save images Seperately",
btn_downloadtext: "Save text (.txt)",
error_default: "Something went wrong",
error_fetch: "Fetch content error. Entered wrong password?",
error_noimage: "No Images",
error_zip: "Failed to create zip. Please try to save images seperagtely.",
txt_title: "Title: ",
txt_author: "Author: ",
txt_twitter: "Twitter: ",
txt_link: "Link: ",
},
"zh-cn": {
ui_logined: "登录状态",
ui_password: "密码",
ui_qualitytrue: "可以下载高质量图片。",
ui_qualityfalse: "不能下载高质量图片。",
ui_mode: "图片命名包含当页ID",
btn_downloadimages: "图片打包为(.zip)",
btn_downloadimageseperately: "独立保存图片",
btn_downloadtext: "保存文字为(.txt)",
error_default: "出错了",
error_fetch: "请求失败。是否输入密码有误?",
error_noimage: "没有图片",
error_zip: "打包失败,请尝试独立保存",
txt_title: "标题:",
txt_author: "作者:",
txt_twitter: "推特:",
txt_link: "地址:",
},
ja: {
ui_logined: "ログイン",
ui_password: "パスワード",
ui_qualitytrue: "高品質の画像を保存できます。",
ui_qualityfalse: "高品質の画像を保存することはできません。",
ui_mode: "IDをファイル名に入れます",
btn_downloadimages: "画像を保存(.zip)",
btn_downloadimageseperately: "画像を個別に保存",
btn_downloadtext: "テキストを保存(.txt)",
error_default: "問題が発生しました",
error_fetch:
"コンテンツの取得エラー。間違ったパスワードを入力しましたか?",
error_zip: "ZIPの作成に失敗しました。 画像を個別に保存してみてください。",
error_noimage: "画像なし",
txt_title: "タイトル:",
txt_author: "ユーザー:",
txt_twitter: "Twitter:",
txt_link: "URL:",
},
};
const i18n = (key) =>
(i18nMap[lang] && i18nMap[lang][key]) || i18nMap["en-us"][key];
const website = "poipiku";
const logined = $(".LoginButton").length === 0;
const fontFamily = "Arial, 'Microsoft Yahei', Helvetica, sans-serif";
class PageInfo {
authorId = "";
workId = "";
title = "";
author = "";
twiter = "";
saveFilename = "";
isText = false;
hasPassword = false;
constructor(url) {
this.url = url;
this.saveImages = this.saveImages.bind(this);
this.saveText = this.saveText.bind(this);
this.downloadImages = this.downloadImages.bind(this);
this.downloadImagesAsZip = this.downloadImagesAsZip.bind(this);
this.downloadImagesSeperately = this.downloadImagesSeperately.bind(this);
this.downloadText = this.downloadText.bind(this);
this.downloadAppendPage = this.downloadAppendPage.bind(this);
this.init();
}
init() {
if (this.initPromise) {
return this.initPromise;
}
const url = this.url;
const execResult = /\/(\d+)\/(\d+)/.exec(url);
const authorId = execResult && execResult[1];
const workId = execResult && execResult[2];
this.authorId = authorId;
this.workId = workId;
let promise;
if (this.url === window.location.href) {
promise = Promise.resolve(document.body.innerHTML);
} else {
promise = request({ url: url });
}
this.initPromise = promise.then((payload) => this.load(payload));
return this.initPromise;
}
load(payload) {
let html = payload;
html = html.replace(/^.+<body>/, "");
html = html.replace(/<\/body>.+$/, "");
const $html = $(`<div>${html}</div>`);
const twitter = $html.find(".UserInfoProgile a").html();
const username = $html.find(".UserInfoUserName a").html();
const username2 =
(twitter &&
(twitter.charAt(0) === "@"
? twitter.substring(1)
: twitter.split("/").slice(-1).join(""))) ||
username;
const desc = $html.find(".IllustItemDesc").text().substring(0, 20);
this.saveFilename = filterFilename(
`[${username2}][${website}][${this.authorId}_${this.workId}]${desc}`
);
this.title = $html.find(".IllustItemDesc").text();
this.author = $html.find(".UserInfoUserName a").html();
this.twitter = $html.find(".UserInfoProgile a").prop("href");
this.isText = $html.find(".IllustItem").hasClass("Text");
this.hasPassword = $html.find(".IllustItem").hasClass("Password");
this.existingHtml =
$html.find(".IllustItemThumb").eq(0).prop("outerHTML") +
$html.find(".IllustItemText").eq(0).prop("outerHTML");
}
// 生成保存文件名
getSaveFilename() {
return this.saveFilename;
}
// 生成保存图片文件名
// 默认:序号.后缀名
// 选中:网站_作品id_序号.后缀名
getSaveImageFilename(src, index) {
let suffix = src.split(".").splice(-1);
const mode = $saveFileMode.is(":checked");
if (mode) {
return `${website}_${this.workId}_${index + 1}.${suffix}`;
}
return `${index + 1}.${suffix}`;
}
// 批量下载图片的默认方法
saveImages(list, saveAsZip, $status) {
let finishehCount = 0;
let zip;
let folder;
if (saveAsZip) {
try {
zip = new JSZip();
folder = zip.folder(this.saveFilename);
} catch (e) {
alert(e);
}
}
$status = $status || $("<div></div>");
$status.text(`0/${list.length}`);
let promises = list.map((src, index) => {
return getBlob(src).then((blob) => {
finishehCount++;
$status.text(`${finishehCount}/${list.length}`);
if (zip) {
folder.file(this.getSaveImageFilename(src, index), blob, {
binary: true,
});
} else {
let suffix = src.split(".").splice(-1);
saveAs(
new Blob([blob]),
`${this.saveFilename}_${index + 1}.${suffix}`
);
}
});
});
Promise.all(promises)
.then(() => {
if (zip) {
return zip
.generateAsync({ type: "blob", base64: true })
.then((content) => saveAs(content, this.saveFilename + ".zip"));
}
})
.catch((e) => {
alert(i18n("error_zip"));
});
}
// 保存文字的默认方法
saveText(option) {
let str = "";
if (option.title) {
str += `${i18n("txt_title")}${option.title}\n`;
}
if (option.author) {
str += `${i18n("txt_author")}${option.author}\n`;
}
if (option.twitter) {
str += `${i18n("txt_twitter")}${option.twitter}\n`;
}
str += `${i18n("txt_link")}${window.location.href}\n`;
str += `\n\n`;
str += option.content;
saveAs(
new Blob([str], { type: "text/plain;charset=UTF-8" }),
this.saveFilename + ".txt"
);
}
// 下载图片
downloadImages(saveAsZip, $status) {
this.init()
.then(this.downloadAppendPage)
.then(($page) => {
if (logined) {
return request({
url: "/f/ShowIllustDetailF.jsp",
type: "POST",
data: {
ID: this.authorId,
TD: this.workId,
AD: "-1",
PAS: $password.val(),
},
dataType: "json",
}).then((payload) => {
if (!payload.html) {
throw new Error(i18n("error_fetch"));
}
return $(payload.html);
});
}
return $page;
})
.then(($page) => {
let list = [];
$page
.find(logined ? ".DetailIllustItemImage" : ".IllustItemThumbImg")
.each(function () {
const src = $(this).attr("src");
if (src && !/^\/img/.test(src)) {
list.push(window.location.protocol + src);
}
});
if (list.length) {
this.saveImages(list, saveAsZip, $status);
} else {
throw new Error(i18n("error_noimage"));
}
})
.catch((e) => {
alert(e.message || i18n("error_default"));
});
}
// 打包图片
downloadImagesAsZip($btn) {
this.downloadImages(true, $btn && $btn.find(".status"));
}
// 独立下载图片
downloadImagesSeperately($btn) {
this.downloadImages(false, $btn && $btn.find(".status"));
}
// 下载文字
downloadText() {
this.init()
.then(this.downloadAppendPage)
.then(($page) => {
this.saveText({
title: this.title,
author: this.author,
twitter: this.twitter,
content: $page.find(".NovelSection").text(),
});
})
.catch((e) => {
alert(e.message || i18n("error_default"));
});
}
downloadAppendPage() {
return request({
url: "/f/ShowAppendFileF.jsp",
type: "POST",
data: {
UID: this.authorId,
IID: this.workId,
PAS: $password.val(),
MD: 0,
TWF: -1,
},
dataType: "json",
}).then((payload) => {
if (payload.result_num < 0) {
throw new Error(payload.html);
}
return $(`<div>${this.existingHtml}${payload.html}</div>`);
});
}
}
const pageInfo = new PageInfo(window.location.href);
$(".IllustThumb").each(function () {
const $this = $(this);
const isText = /文字/.test($this.find(".Num").text());
const hasPassword =
/pass\.png/.test($this.find(".IllustThumbImg").css("background-image")) ||
/pass\.png/.test($this.find(".Publish").css("background-image"));
if (hasPassword) {
return;
}
if (isText) {
$(`<button>${i18n("btn_downloadtext")}</button>`)
.on("click", downloadTextFromList)
.css({
position: "absolute",
left: 4,
top: 110,
zIndex: 1,
fontFamily: fontFamily,
})
.appendTo($this);
} else {
$(
`<button>${i18n(
"btn_downloadimageseperately"
)} <b class='status'></b></button>`
)
.on("click", downloadImagesSeperatelyFromList)
.css({
position: "absolute",
left: 4,
top: 110,
zIndex: 1,
fontFamily: fontFamily,
})
.appendTo($this);
$(
`<button>${i18n("btn_downloadimages")} <b class='status'></b></button>`
)
.on("click", downloadImagesAsZipFromList)
.css({
position: "absolute",
left: 4,
top: 140,
zIndex: 1,
fontFamily: fontFamily,
})
.appendTo($this);
}
});
const $panel = $(`<div>
<div>${i18n("ui_logined")}: <b style="color:red">${logined}</b>.</div>
<div class="line-qualitytip" >${
logined ? i18n("ui_qualitytrue") : i18n("ui_qualityfalse")
}</div>
<div class="line-password">${i18n(
"ui_password"
)} <input type='text' class="password"></div>
<div class="line-mode" >${i18n(
"ui_mode"
)} <input type='checkbox' class="saveFileMode"></div>
<div class="line-images">
<button class="btn-downloadImagesSeperately" style="font-size:20px">${i18n(
"btn_downloadimageseperately"
)} <b class='status'></b></button></button><br>
<button class="btn-downloadImages" style="font-size:20px">${i18n(
"btn_downloadimages"
)} <b class='status'></b></button>
</div>
<div class="line-text"><button class="btn-downloadText" style="font-size:20px">${i18n(
"btn_downloadtext"
)}</button></div>
</div>`)
.css({
position: "fixed",
left: 0,
bottom: 50,
zIndex: 999999,
background: "#fff",
color: "#333",
fontSize: 18,
fontFamily: fontFamily,
padding: 10,
})
.appendTo($("body"));
const $password = $panel.find(".password");
const $saveFileMode = $panel.find(".saveFileMode");
$panel.find("button").css({
fontFamily: fontFamily,
});
pageInfo.init().then(function () {
if (!pageInfo.workId) {
$panel.find(".line-password").hide();
$panel.find(".line-images").hide();
$panel.find(".line-mode").hide();
$panel.find(".line-text").hide();
return;
}
if (!pageInfo.hasPassword) {
$panel.find(".line-password").hide();
}
if (pageInfo.isText) {
$panel.find(".line-images").hide();
$panel.find(".line-qualitytip").hide();
$panel.find(".line-mode").hide();
} else {
$panel.find(".line-text").hide();
}
$panel.find(".btn-downloadImages").on("click", function () {
pageInfo.downloadImagesAsZip($(this));
});
$panel.find(".btn-downloadImagesSeperately").on("click", function () {
pageInfo.downloadImagesSeperately($(this));
});
$panel.find(".btn-downloadText").on("click", function () {
pageInfo.downloadText($(this));
});
});
function request(config) {
return new Promise((resolve, reject) => {
$.ajax({
...config,
success: (response) => {
resolve(response);
},
error: () => {
reject(new Error(i18n("error_default")));
},
});
});
}
function getMimeType(suffix) {
let map = {
png: "image/png",
jpg: "image/jpeg",
jpeg: "image/jpeg",
gif: "image/gif",
};
return map[suffix] || "text/plain";
}
function getBlob(url) {
// return fetch(url).then((response) => response.blob());
return new Promise((resolve, reject) => {
GM.xmlHttpRequest({
method: "GET",
url: url,
responseType: "blob",
headers: { referer: window.location.href },
onload: (payload) => {
resolve(payload.response);
},
onerror: () => {
reject(new Error(i18n("error_default")));
},
});
});
// return new Promise((resolve, reject) => {
// GM.xmlHttpRequest({
// method: "GET",
// url: url,
// overrideMimeType: "text/plain; charset=x-user-defined",
// headers: { referer: window.location.href },
// onload: (xhr) => {
// let r = xhr.responseText;
// let data = new Uint8Array(r.length);
// let i = 0;
// while (i < r.length) {
// data[i] = r.charCodeAt(i);
// i++;
// }
// let suffix = url.split(".").splice(-1);
// let blob = new Blob([data], { type: getMimeType(suffix) });
// resolve(blob);
// },
// onerror: () => {
// reject(new Error(i18n("error_default")));
// },
// });
// });
}
// 过滤文件名非法字符
function filterFilename(filename) {
return filename.replace(/\?|\*|\:|\"|\<|\>|\\|\/|\|/g, "");
}
function getPageInfo($btn) {
const url = $btn.siblings(".IllustThumbImg").prop("href");
return new PageInfo(url);
}
function downloadImagesAsZipFromList() {
const $this = $(this);
const pageInfo = getPageInfo($this);
pageInfo.init().then(() => {
pageInfo.downloadImagesAsZip($this);
});
}
function downloadImagesSeperatelyFromList() {
const $this = $(this);
const pageInfo = getPageInfo($this);
pageInfo.init().then(() => {
pageInfo.downloadImagesSeperately($this);
});
}
function downloadTextFromList() {
const $this = $(this);
const pageInfo = getPageInfo($this);
pageInfo.init().then(() => {
pageInfo.downloadText($this);
});
}
});