// ==UserScript== // @name bangumi new wiki helper // @name:zh-CN bangumi 创建条目助手 // @namespace https://github.com/zhifengle // @description assist to create new subject // @description:zh-cn 辅助创建 bangumi.tv 上的条目 // @include http://www.getchu.com/soft.phtml?id=* // @include /^https?:\/\/www\.amazon\.co\.jp\/.*$/ // @include /^https?:\/\/(bangumi|bgm|chii)\.(tv|in)\/.*$/ // @match *://*/* // @author zhifengle // @homepage https://github.com/zhifengle/bangumi-new-wiki-helper // @version 0.4.35 // @note 0.4.27 支持音乐条目曲目列表 // @note 0.3.0 使用 typescript 重构,浏览器扩展和脚本使用公共代码 // @run-at document-end // @grant GM_addStyle // @grant GM_openInTab // @grant GM_registerMenuCommand // @grant GM_xmlhttpRequest // @grant GM_getValue // @grant GM_setValue // @grant GM_getResourceText // @resource NOTYF_CSS https://cdnjs.cloudflare.com/ajax/libs/notyf/3.10.0/notyf.min.css // @require https://cdnjs.cloudflare.com/ajax/libs/fuse.js/6.4.0/fuse.min.js // @require https://cdnjs.cloudflare.com/ajax/libs/notyf/3.10.0/notyf.min.js // @downloadURL https://update.greasyfork.icu/scripts/40041/bangumi%20new%20wiki%20helper.user.js // @updateURL https://update.greasyfork.icu/scripts/40041/bangumi%20new%20wiki%20helper.meta.js // ==/UserScript== let contextDom = null; function setCtxDom(dom) { contextDom = dom; } function getCtxDom() { return contextDom; } function clearCtxDom() { setCtxDom(undefined); } /** * 获取节点文本 * @param elem */ function getText(elem) { if (!elem) return ''; if (elem.tagName.toLowerCase() === 'meta') { return elem.content; } if (elem.tagName.toLowerCase() === 'input') { return elem.value; } return elem.textContent || elem.innerText || ''; } function getInnerText(elem) { if (!elem) return ''; return elem.innerText || elem.textContent || ''; } /** * dollar 选择单个 * @param {string} selector */ function $q(selector) { const ctxDom = getCtxDom(); if (ctxDom) { return ctxDom.querySelector(selector); } return document.querySelector(selector); } /** * dollar 选择所有元素 * @param {string} selector */ function $qa(selector) { const ctxDom = getCtxDom(); if (ctxDom) { return ctxDom.querySelectorAll(selector); } return document.querySelectorAll(selector); } /** * 查找包含文本的标签 * @param {string} selector * @param {string} text */ function contains(selector, text, $parent) { let elements; if ($parent) { elements = $parent.querySelectorAll(selector); } else { elements = $qa(selector); } let t; if (typeof text === 'string') { t = text; } else { t = text.join('|'); } return [].filter.call(elements, function (element) { return new RegExp(t, 'i').test(getText(element)); }); } function findElementByKeyWord(selector, $parent) { let res = null; if ($parent) { $parent = $parent.querySelector(selector.selector); } else { $parent = $q(selector.selector); } if (!$parent) return res; const targets = contains(selector.subSelector, selector.keyWord, $parent); if (targets && targets.length) { let $t = targets[targets.length - 1]; // 相邻节点 if (selector.sibling) { $t = targets[targets.length - 1].nextElementSibling; } return $t; } return res; } function findElement(selector, $parent) { var _a; let r = null; if (selector) { if (selector instanceof Array) { let i = 0; let targetSelector = selector[i]; while (targetSelector && !(r = findElement(targetSelector, $parent))) { targetSelector = selector[++i]; } } else { if (!selector.subSelector) { r = $parent ? $parent.querySelector(selector.selector) : $q(selector.selector); } else if (selector.isIframe) { // iframe 暂时不支持 parent const $iframeDoc = (_a = $q(selector.selector)) === null || _a === void 0 ? void 0 : _a.contentDocument; r = $iframeDoc === null || $iframeDoc === void 0 ? void 0 : $iframeDoc.querySelector(selector.subSelector); } else { r = findElementByKeyWord(selector, $parent); } if (selector.closest) { r = r.closest(selector.closest); } // recursive if (r && selector.nextSelector) { const nextSelector = selector.nextSelector; r = findElement(nextSelector, r); } } } return r; } function findAllElement(selector, $parent) { var _a, _b; let res = []; if (selector instanceof Array) { let i = 0; let targetSelector = selector[i]; while (targetSelector) { const arr = findAllElement(targetSelector, $parent); if (arr.length) { res.push(...arr); break; } targetSelector = selector[++i]; } } else { // 没有下一步的选择器 if (!selector.nextSelector) { // 没子选择器 if (!selector.subSelector) { res = Array.from($parent ? $parent.querySelectorAll(selector.selector) : $qa(selector.selector)); } else if (selector.isIframe) { const $iframeDoc = (_a = $q(selector.selector)) === null || _a === void 0 ? void 0 : _a.contentDocument; res = Array.from($iframeDoc === null || $iframeDoc === void 0 ? void 0 : $iframeDoc.querySelectorAll(selector.subSelector)); } else { if (selector.isIframe) { const $iframeDoc = (_b = $q(selector.selector)) === null || _b === void 0 ? void 0 : _b.contentDocument; // iframe 时不需要 keyWord $parent = $iframeDoc === null || $iframeDoc === void 0 ? void 0 : $iframeDoc.querySelector(selector.subSelector); } else { $parent = $parent ? $parent : $q(selector.selector); } if (!$parent) return res; res = contains(selector.subSelector, selector.keyWord, $parent); if (selector.sibling) { res = res.map(($t) => $t.nextElementSibling); } } // closest if (selector.closest) { res = res.map((r) => r.closest(selector.closest)); } } else { // 有下一步的选择器时,selector 是用来定位父节点的 const localSel = Object.assign({}, selector); delete localSel.nextSelector; const $p = findElement(localSel); if ($p) { res = findAllElement(selector.nextSelector, $p); } } } return res; } /** * @param {String} HTML 字符串 * @return {Element} */ function htmlToElement(html) { var template = document.createElement('template'); html = html.trim(); template.innerHTML = html; // template.content.childNodes; return template.content.firstChild; } /** * 载入 iframe * @param $iframe iframe DOM * @param src iframe URL * @param TIMEOUT time out */ function loadIframe($iframe, src, TIMEOUT = 10000) { return new Promise((resolve, reject) => { $iframe.src = src; let timer = setTimeout(() => { timer = null; $iframe.onload = undefined; reject('iframe timeout'); }, TIMEOUT); $iframe.onload = () => { clearTimeout(timer); $iframe.onload = null; resolve(null); }; }); } function genAnonymousLinkText(url, text) { return ` ${text} `; } var SubjectTypeId; (function (SubjectTypeId) { SubjectTypeId[SubjectTypeId["book"] = 1] = "book"; SubjectTypeId[SubjectTypeId["anime"] = 2] = "anime"; SubjectTypeId[SubjectTypeId["music"] = 3] = "music"; SubjectTypeId[SubjectTypeId["game"] = 4] = "game"; SubjectTypeId[SubjectTypeId["real"] = 6] = "real"; SubjectTypeId["all"] = "all"; })(SubjectTypeId || (SubjectTypeId = {})); const getchuGameModel = { key: 'getchu_game', description: 'Getchu游戏', host: ['getchu.com', 'www.getchu.com'], type: SubjectTypeId.game, pageSelectors: [ { selector: '.genretab.current', subSelector: 'a', keyWord: ['ゲーム', '同人'], }, ], controlSelector: [ { selector: '#soft-title', }, ], itemList: [], }; const commonSelector = { selector: '#soft_table table', subSelector: 'td', sibling: true, }; const dict = { 定価: '售价', 発売日: '发行日期', ジャンル: '游戏类型', ブランド: '开发', 原画: '原画', 音楽: '音乐', シナリオ: '剧本', アーティスト: '主题歌演出', 作詞: '主题歌作词', 作曲: '主题歌作曲', }; const configArr = Object.keys(dict).map((key) => { const r = { name: dict[key], selector: Object.assign({ // 匹配关键字开头 2020/03/18 keyWord: '^' + key }, commonSelector), }; if (key === '発売日') { r.category = 'date'; } return r; }); getchuGameModel.itemList.push({ name: '游戏名', selector: { selector: '#soft-title', }, category: 'subject_title', }, { name: 'cover', selector: [ { selector: '#soft_table .highslide', }, { selector: '#soft_table .highslide img', }, ], category: 'cover', }, ...configArr, { name: '游戏简介', selector: [ { selector: '#wrapper', subSelector: '.tabletitle', sibling: true, keyWord: 'ストーリー', }, { selector: '#wrapper', subSelector: '.tabletitle', sibling: true, keyWord: '作品紹介', }, { selector: '#wrapper', subSelector: '.tabletitle', sibling: true, keyWord: '商品紹介', }, ], category: 'subject_summary', }); getchuGameModel.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, { name: 'subject_nsfw', value: '1', category: 'checkbox', }, ]; // TODO: 区分 kindle 页面和 纸质书页面 const amazonSubjectModel = { key: 'amazon_jp_book', host: ['amazon.co.jp', 'www.amazon.co.jp'], description: '日亚图书', type: SubjectTypeId.book, pageSelectors: [ { selector: '#nav-subnav .nav-a:first-child', subSelector: '.nav-a-content', keyWord: ['本', '书', '漫画', 'マンガ', 'Audible'], }, { selector: '#wayfinding-breadcrumbs_container .a-unordered-list .a-list-item:first-child', subSelector: '.a-link-normal', keyWord: ['本', '书', '漫画', 'マンガ', 'Audible'], }, ], controlSelector: { selector: '#title', }, itemList: [], }; const commonSelectors = [ // 2021-05 日亚改版 { selector: '#richProductInformation_feature_div', subSelector: 'ol.a-carousel li', }, { selector: '#detailBullets_feature_div .detail-bullet-list', subSelector: 'li .a-list-item', }, { selector: '#detail_bullets_id .bucket .content', subSelector: 'li', }, ]; amazonSubjectModel.itemList.push({ name: '名称', selector: { selector: '#productTitle', }, category: 'subject_title', }, // 在 afterGetWikiData 获取封面 // { // name: 'cover', // selector: [ // { // selector: 'img#igImage', // }, // ], // category: 'cover', // }, { name: 'ASIN', selector: commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: ['ASIN', 'ISBN-10'] }); }), category: 'ASIN', }, { name: 'ISBN', selector: commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: 'ISBN-13' }); }), category: 'ISBN', pipes: ['k', 'ta'], }, { name: '发售日', selector: commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: ['発売日', '出版日期', '配信日'] }); }), category: 'date', pipes: ['ta', 'k', 'p', 'date'], }, { name: '出版社', selector: [ { selector: '#bylineInfo', subSelector: '.author', keyWord: '\\(出版社\\)', nextSelector: [ { selector: '.a-link-normal', }, { selector: 'a', }, ], }, ...commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: '出版社' }); }), ], }, { name: '页数', selector: commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: ['ページ', '页'] }); }), pipes: ['num'], }, // 有声书 { name: '播放时长', selector: commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: ['再生時間'] }); }), }, { name: '演播', selector: commonSelectors.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: ['ナレーター'] }); }), pipes: ['ta', 'k'], }, { name: '作者', selector: [ { selector: '#bylineInfo', subSelector: '.author', keyWord: '\\(著\\)', nextSelector: [ { selector: '.contributorNameID', }, { selector: 'a', }, ], }, { selector: '#byline .author span.a-size-medium', }, { selector: '#bylineInfo .author > a', }, { selector: '#bylineInfo .contributorNameID', }, ], category: 'creator', }, { name: '插图', selector: [ { selector: '#bylineInfo', subSelector: '.author', keyWord: 'イラスト', nextSelector: [ { selector: '.contributorNameID', }, { selector: 'a', }, ], }, ], category: 'creator', }, { name: '价格', selector: [ { selector: '#tmmSwatches .a-button-selected .slot-price', }, { selector: '#tmm-grid-swatch-OTHER .slot-price', }, { selector: '#tmm-grid-swatch-PAPERBACK .slot-price', }, { selector: '#tmmSwatches > div > div:last-child .slot-price', }, ], pipes: ['ta'], }, { name: '内容简介', selector: [ { selector: '#productDescription', subSelector: 'h3', sibling: true, keyWord: ['内容紹介', '内容'], }, { selector: '#bookDescription_feature_div .a-expander-content', }, { selector: '#bookDesc_iframe', subSelector: '#iframeContent', isIframe: true, }, ], category: 'subject_summary', }); const erogamescapeModel = { key: 'erogamescape', description: 'erogamescape', host: ['erogamescape.org', 'erogamescape.dyndns.org'], type: SubjectTypeId.game, pageSelectors: [ { selector: '#soft-title', }, ], controlSelector: { selector: '#soft-title', }, itemList: [], }; erogamescapeModel.itemList.push({ name: '游戏名', selector: { selector: '#soft-title > span', }, category: 'subject_title', }, { name: '开发', selector: { selector: '#brand a', }, }, { name: '发行日期', selector: { selector: '#sellday a', }, category: 'date', }, { name: 'cover', selector: { selector: '#image_and_basic_infomation img', }, category: 'cover', }, { name: 'website', selector: [ { selector: '#links', subSelector: 'a', keyWord: 'game_OHP', }, { selector: '#bottom_inter_links_main', subSelector: 'a', keyWord: 'game_OHP', }, ], category: 'website', }, { name: '原画', selector: { selector: '#genga > td:last-child', }, }, { name: '剧本', selector: { selector: '#shinario > td:last-child', }, }, { name: '歌手', selector: { selector: '#kasyu > td:last-child', }, }); erogamescapeModel.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, { name: 'subject_nsfw', value: '1', category: 'checkbox', }, ]; const steamdbModel = { key: 'steamdb_game', description: 'steamdb', host: ['steamdb.info'], type: SubjectTypeId.game, pageSelectors: [ { selector: '.pagehead h1', }, ], controlSelector: { selector: '.pagehead', }, itemList: [], }; const commonSelector$1 = { selector: '.scope-app .app-row table', subSelector: 'td', sibling: true, }; const dictArr = [ { name: '发行日期', keyWord: 'Release Date', }, { name: '开发', keyWord: 'Developer', }, { name: '发行', keyWord: 'Publisher', }, { name: '游戏引擎', keyWord: 'Technologies', } ]; const configArr$1 = dictArr.map((item) => { const r = { name: item.name, selector: Object.assign({ keyWord: item.keyWord }, commonSelector$1), }; if (item.name === '发行日期') { r.category = 'date'; } return r; }); const detailsTableSelector = { selector: '#info table', subSelector: 'td', sibling: true, }; const subTableSelector = { selector: 'table.web-assets', subSelector: 'td', sibling: true, }; const assetsTableSelector = { selector: '#js-assets-table', subSelector: 'td', sibling: true, }; steamdbModel.itemList.push({ name: '游戏名', selector: [ Object.assign(Object.assign({}, detailsTableSelector), { keyWord: 'name_localized', nextSelector: Object.assign(Object.assign({}, subTableSelector), { keyWord: 'japanese' }) }), { selector: '.pagehead h1', }, ], category: 'subject_title', }, { name: '中文名', selector: [ Object.assign(Object.assign({}, detailsTableSelector), { keyWord: 'name_localized', nextSelector: Object.assign(Object.assign({}, subTableSelector), { keyWord: 'schinese' }) }), Object.assign(Object.assign({}, detailsTableSelector), { keyWord: 'name_localized', nextSelector: Object.assign(Object.assign({}, subTableSelector), { keyWord: 'tchinese' }) }), ], category: 'alias', }, { name: '游戏类型', selector: [ Object.assign(Object.assign({}, detailsTableSelector), { keyWord: 'Primary Genre' }) ], pipes: ['ta', 'p'], }, { name: 'cover', selector: [ Object.assign(Object.assign({}, assetsTableSelector), { keyWord: 'library_assets', nextSelector: { selector: 'table.web-assets', subSelector: 'td', keyWord: 'library_capsule', sibling: true, nextSelector: { selector: 'a', }, } }), Object.assign(Object.assign({}, assetsTableSelector), { keyWord: 'Web Assets', nextSelector: { selector: 'table.web-assets', subSelector: 'td > a', keyWord: 'library_600x900', } }), ], category: 'cover', }, ...configArr$1, { name: '游戏简介', selector: [ { selector: '.scope-app .header-description', }, { selector: 'head meta[name="description"]', }, ], category: 'subject_summary', }); steamdbModel.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, ]; const steamModel = { key: 'steam_game', description: 'steam', host: ['store.steampowered.com'], type: SubjectTypeId.game, pageSelectors: [ { selector: '.apphub_AppName', }, ], controlSelector: { selector: '.apphub_AppName', }, itemList: [], }; steamModel.itemList.push({ name: '游戏名', selector: { selector: '.apphub_AppName', }, category: 'subject_title', }, { name: '发行日期', selector: { selector: '.release_date .date', }, category: 'date', }, { name: '开发', selector: { selector: '.glance_ctn_responsive_left .user_reviews', subSelector: '.dev_row .subtitle', keyWord: ['开发商', 'DEVELOPER'], sibling: true, }, }, { name: '发行', selector: { selector: '.glance_ctn_responsive_left .user_reviews', subSelector: '.dev_row .subtitle', keyWord: ['发行商', 'PUBLISHER'], sibling: true, }, }, { name: 'website', selector: { selector: '.responsive_apppage_details_left.game_details', subSelector: '.details_block > .linkbar', keyWord: ['访问网站', 'Visit the website'], }, category: 'website', }, { name: '游戏简介', selector: [ { selector: '.game_description_snippet', }, { selector: 'head meta[name="description"]', }, { selector: '#game_area_description', }, ], category: 'subject_summary', }); steamModel.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, ]; const dangdangBookModel = { key: 'dangdang_book', host: ['product.dangdang.com'], description: '当当图书', type: SubjectTypeId.book, pageSelectors: [ { selector: '#breadcrumb', subSelector: 'a', keyWord: '图书', }, ], controlSelector: { selector: '.name_info h1', }, itemList: [], }; const infoSelector = { selector: '.messbox_info', subSelector: 'span', }; const descSelector = { selector: '#detail_describe', subSelector: 'li', }; dangdangBookModel.itemList.push({ name: '名称', selector: { selector: '.name_info h1', }, category: 'subject_title', }, // { // name: 'cover', // selector: { // selector: 'img#largePic', // }, // category: 'cover', // }, { name: 'ISBN', selector: Object.assign(Object.assign({}, descSelector), { keyWord: '国际标准书号ISBN' }), category: 'ISBN', }, { name: '发售日', selector: Object.assign(Object.assign({}, infoSelector), { keyWord: '出版时间' }), category: 'date', }, { name: '作者', selector: [ Object.assign(Object.assign({}, infoSelector), { keyWord: '作者' }), ], }, { name: '出版社', selector: Object.assign(Object.assign({}, infoSelector), { keyWord: '出版社' }), }, { name: '内容简介', selector: [ { selector: '#content .descrip', }, ], category: 'subject_summary', }); const jdBookModel = { key: 'jd_book', host: ['item.jd.com'], description: '京东图书', type: SubjectTypeId.book, pageSelectors: [ { selector: '#crumb-wrap', subSelector: '.item > a', keyWord: '图书', }, ], controlSelector: { selector: '#name .sku-name', }, itemList: [], }; const descSelector$1 = { selector: '#parameter2', subSelector: 'li', }; jdBookModel.itemList.push({ name: '名称', selector: { selector: '#name .sku-name', }, category: 'subject_title', }, // { // name: 'cover', // selector: { // selector: '#preview img', // }, // category: 'cover', // }, { name: 'ISBN', selector: Object.assign(Object.assign({}, descSelector$1), { keyWord: 'ISBN' }), category: 'ISBN', }, { name: '发售日', selector: Object.assign(Object.assign({}, descSelector$1), { keyWord: '出版时间' }), category: 'date', }, { name: '作者', selector: [ { selector: '#p-author', keyWord: '著', }, ], }, { name: '出版社', selector: Object.assign(Object.assign({}, descSelector$1), { keyWord: '出版社' }), }, { name: '内容简介', selector: [ { selector: '.book-detail-item', subSelector: '.item-mt', keyWord: '内容简介', sibling: true, }, ], category: 'subject_summary', }); const doubanGameModel = { key: 'douban_game', description: 'douban game', host: ['douban.com', 'www.douban.com'], type: SubjectTypeId.game, pageSelectors: [ { selector: '#content h1', }, ], controlSelector: { selector: '#content h1', }, itemList: [], }; const gameAttr = { selector: '#content .thing-attr', subSelector: 'dt', sibling: true, }; doubanGameModel.itemList.push({ name: '游戏名', selector: { selector: '#content h1', }, category: 'subject_title', }, { name: '发行日期', selector: [ Object.assign(Object.assign({}, gameAttr), { keyWord: '发行日期' }), Object.assign(Object.assign({}, gameAttr), { keyWord: '预计上市时间' }), ], category: 'date', }, // 平台特殊处理 // { // name: '平台', // selector: { // ...gameAttr, // keyWord: '平台', // }, // category: 'platform', // }, { name: '别名', selector: Object.assign(Object.assign({}, gameAttr), { keyWord: '别名' }), category: 'alias', }, { name: '游戏类型', selector: Object.assign(Object.assign({}, gameAttr), { keyWord: '类型' }), }, { name: '开发', selector: Object.assign(Object.assign({}, gameAttr), { keyWord: '开发商' }), }, { name: '发行', selector: Object.assign(Object.assign({}, gameAttr), { keyWord: '发行商' }), }, // { // name: 'website', // selector: { // selector: '.responsive_apppage_details_left.game_details', // }, // category: 'website', // }, { name: '游戏简介', selector: [ { selector: '.mod.item-desc', subSelector: 'h2', keyWord: '简介', sibling: true, }, ], category: 'subject_summary', }, { name: 'cover', selector: { selector: '#content .item-subject-info .pic > a', }, category: 'cover', }); const doubanGameEditModel = { key: 'douban_game_edit', description: 'douban game edit', host: ['douban.com', 'www.douban.com'], urlRules: [/\/game\/\d+\/edit/], type: SubjectTypeId.game, pageSelectors: [ { selector: '#content h1', }, ], controlSelector: { selector: '#content h1', }, itemList: [], }; const gameAttr$1 = { selector: '#thing-modify', subSelector: '.thing-item .desc-item .label', sibling: true, }; doubanGameEditModel.itemList.push({ name: '游戏名', selector: [ Object.assign(Object.assign({}, gameAttr$1), { keyWord: '原名' }), Object.assign(Object.assign({}, gameAttr$1), { keyWord: '中文名' }), ], category: 'subject_title', }, { name: '发行日期', selector: [ Object.assign(Object.assign({}, gameAttr$1), { keyWord: '发行日期' }), Object.assign(Object.assign({}, gameAttr$1), { keyWord: '预计上市时间' }), ], category: 'date', }, { name: '平台', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '平台' }), category: 'platform', }, { name: '中文名', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '中文名' }), category: 'alias', }, { name: '别名', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '别名' }), category: 'alias', }, { name: '游戏类型', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '类型' }), }, { name: '开发', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '开发商' }), }, { name: '发行', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '发行商' }), }, // { // name: '游戏简介', // selector: [ // { // selector: '#thing_desc_options_0', // }, // ], // category: 'subject_summary', // }, { name: 'cover', selector: Object.assign(Object.assign({}, gameAttr$1), { keyWord: '图标', nextSelector: { selector: 'img', } }), category: 'cover', }); const dlsiteGameModel = { key: 'dlsite_game', description: 'dlsite游戏', host: ['dlsite.com', 'www.dlsite.com'], type: SubjectTypeId.game, pageSelectors: [ { selector: '.floorTab-item.type-doujin.is-active', }, { selector: '.floorTab-item.type-com.is-active', }, ], controlSelector: [ { selector: '#work_name', }, ], itemList: [], }; const commonSelector$2 = { selector: '#work_outline', subSelector: 'th', sibling: true, }; const arrDict = [ { name: '发行日期', key: ['販売日', '贩卖日', '販賣日'], categrory: 'date', }, // { // name: '游戏类型', // key: ['ジャンル', '分类'], // }, { name: '作者', key: ['作者'], }, { name: '原画', key: ['イラスト', '插画'], }, { name: '剧本', key: ['シナリオ', '剧情'], }, { name: '声优', key: ['声優', '声优'], }, { name: '音乐', key: ['音乐', '音楽'], }, ]; const configArr$2 = arrDict.map((obj) => { const r = { name: obj.name, selector: Object.assign({ keyWord: obj.key }, commonSelector$2), }; if (obj.categrory) { r.category = obj.categrory; } return r; }); dlsiteGameModel.itemList.push({ name: '游戏名', selector: { selector: '#work_name', }, category: 'subject_title', }, { // name: '社团名', name: '开发', selector: [ { selector: '#work_maker .maker_name a', }, ], }, ...configArr$2, { name: 'cover', selector: [ { selector: '#work_left div.slider_body_inner.swiper-container-horizontal > ul > li.slider_item:first-child > picture > img', }, ], category: 'cover', }, { name: '游戏简介', selector: [ { selector: '.work_parts_container', subSelector: '.work_parts_heading', keyWord: 'あらすじ', sibling: true, }, { selector: '#intro-title + div', }, ], category: 'subject_summary', }, { name: 'website', selector: [ { selector: '#work_name > a', }, ], category: 'website', }); dlsiteGameModel.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, { name: 'subject_nsfw', value: '1', category: 'checkbox', }, ]; const dmmGameModel = { key: 'dmm_game', description: 'dmm游戏', host: ['dlsoft.dmm.co.jp'], type: SubjectTypeId.game, pageSelectors: [ { selector: '.ntgnav-mainItem.is-active', subSelector: 'span', keyWord: 'ゲーム', }, ], controlSelector: [ { selector: 'h1#title', }, ], itemList: [], }; const commonSelector$3 = { selector: '.main-area-center .container02 table', subSelector: 'tr', nextSelector: { selector: '.type-right', }, }; const contentIframe = { selector: '#if_view', isIframe: true, subSelector: 'body', }; const arrDict$1 = [ { name: '发行日期', key: ['配信開始日'], category: 'date', }, { name: '游戏类型', key: ['ゲームジャンル'], }, { name: '原画', key: ['原画'], }, { name: '剧本', key: ['シナリオ', '剧情'], }, ]; const configArr$3 = arrDict$1.map((obj) => { const r = { name: obj.name, selector: Object.assign({ keyWord: obj.key }, commonSelector$3), }; if (obj.category) { r.category = obj.category; } return r; }); dmmGameModel.itemList.push({ name: '游戏名', selector: { selector: '#title', }, category: 'subject_title', }, { name: '开发', selector: [ { selector: '.ranking-and-brand .brand', subSelector: 'td', keyWord: 'ブランド', sibling: true, }, ], }, ...configArr$3, // 部分页面的图片是预览图,不少封面。所以改在 hook 里面,提取图片。 // { // name: 'cover', // selector: [ // { // ...contentIframe, // nextSelector: { // selector: '#guide-head > img', // }, // }, // ], // category: 'cover', // }, { name: '游戏简介', selector: [ Object.assign(Object.assign({}, contentIframe), { nextSelector: { selector: '#guide-content', subSelector: '.guide-capt', keyWord: '作品紹介', sibling: true, } }), { selector: '.read-text-area .text-overflow', }, ], category: 'subject_summary', }); dmmGameModel.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, { name: 'subject_nsfw', value: '1', category: 'checkbox', }, ]; const dlsiteGameCharaModel = { key: 'dlsite_game_chara', siteKey: 'dlsite_game', description: 'dlsite游戏角色', host: ['dlsite.com', 'www.dlsite.com'], type: SubjectTypeId.game, itemSelector: { selector: '.work_parts_multiimage_item', }, controlSelector: [ { selector: '.work_parts.type_multiimages *:first-child', }, { selector: '#work_name', }, ], itemList: [], }; // 限定父节点 dlsiteGameCharaModel.itemList.push({ name: 'cover', selector: { selector: '.image img', }, category: 'crt_cover', }); const dmmGameCharaModel = { key: 'dmm_game_chara', siteKey: 'dmm_game', description: 'dmm 游戏角色', type: SubjectTypeId.game, itemSelector: { selector: '#if_view', isIframe: true, subSelector: 'body', nextSelector: { selector: '.guide-sect .guide-box-chr', }, }, controlSelector: [ { selector: '#title', }, ], itemList: [], }; // 限定父节点 dmmGameCharaModel.itemList.push({ name: '姓名', selector: { selector: '.guide-tx16.guide-bold.guide-lin-hgt', }, category: 'crt_name', pipes: ['p', 'ta'], }, { name: 'cover', selector: { selector: 'img', }, category: 'crt_cover', }); const adultComicModel = { key: 'adultcomic', description: 'adultcomic', host: ['adultcomic.dbsearch.net'], type: SubjectTypeId.book, pageSelectors: [ { selector: '#pankuz > ol > li:nth-child(1) > a[href*="adultcomic.dbsearch.net"]', }, ], controlSelector: [ { selector: '#h2-icon-bk', }, { selector: 'h2-icon-wk', }, ], itemList: [], }; const commonSelectors$1 = [ { selector: '#info-table > div.info-box > dl', subSelector: 'dt', sibling: true, }, ]; const genSelectors = (keyWord) => commonSelectors$1.map((s) => { return Object.assign(Object.assign({}, s), { keyWord }); }); adultComicModel.itemList.push({ name: '名称', selector: { selector: '#h2-icon-bk', }, category: 'subject_title', }, // 图片使用的懒加载. 在 hook 里面读取 data-src { name: 'cover', selector: [ { selector: '#sample-image > figure > a', }, { selector: '#info-table > .img-box > img', }, ], category: 'cover', }, { name: 'ISBN', selector: genSelectors('ISBN'), category: 'ISBN', }, { name: '发售日', selector: genSelectors('発売日'), category: 'date', pipes: ['k', 'date'], }, { name: '出版社', selector: genSelectors('出版社'), }, { name: '书系', selector: genSelectors(['レーベル']), }, { name: '页数', selector: genSelectors(['ページ']), pipes: ['num'], }, { name: '作者', selector: [ { selector: '#info-table > div.info-box .author-list > li', }, ...genSelectors('漫画家'), ], category: 'creator', }, { name: '价格', selector: genSelectors('本体価格'), }, { name: '内容简介', selector: [ { selector: '#comment-clist > .iteminfo-box', subSelector: 'h4', sibling: true, keyWord: ['内容紹介'], }, ], category: 'subject_summary', }); const moepedia = { key: 'moepedia', description: 'moepedia.net', host: ['moepedia.net'], type: SubjectTypeId.game, pageSelectors: [ { selector: '.gme-Contents > .gme-Body', }, ], controlSelector: [ { selector: '.body-top_info_title > h2', }, ], itemList: [], }; const topTableSelector = { selector: 'body > div.st-Container.visible > div.gme-Contents > div > div > div.body-top > div.body-top_table.body-table > table', subSelector: 'tr > th', sibling: true, }; const middleTableSelector = { selector: 'body > div.st-Container.visible > div.gme-Contents > div > div > div.body-middle', subSelector: 'tr > th', sibling: true, }; moepedia.itemList.push({ name: '游戏名', selector: { selector: 'div.gme-Contents h2', }, category: 'subject_title', }, { name: '发行日期', selector: [ Object.assign(Object.assign({}, topTableSelector), { keyWord: '発売日' }), ], pipes: ['date'], }, { name: '售价', selector: [ Object.assign(Object.assign({}, topTableSelector), { keyWord: '価格' }), ], pipes: ['p'], }, { name: 'website', selector: [ { selector: 'body > div.st-Container.visible > div.gme-Contents > div > div > div.body-top > div.body-top_table.body-table > div > a', }, ], category: 'website', }, { name: 'cover', selector: [ { selector: 'div.gme-Contents div.body-top > div.body-top_image img', }, ], category: 'cover', }, { name: '原画', selector: Object.assign(Object.assign({}, middleTableSelector), { keyWord: ['原画'] }), }, { name: '开发', selector: Object.assign(Object.assign({}, middleTableSelector), { keyWord: ['ブランド'] }), }, { name: '剧本', selector: Object.assign(Object.assign({}, middleTableSelector), { keyWord: ['シナリオ'] }), }, { name: '游戏类型', selector: Object.assign(Object.assign({}, middleTableSelector), { keyWord: ['ジャンル'] }), }, { name: '音乐', selector: Object.assign(Object.assign({}, middleTableSelector), { keyWord: ['音楽'] }), }, { name: '主题歌演唱', selector: Object.assign(Object.assign({}, middleTableSelector), { keyWord: ['歌手'] }), }); moepedia.defaultInfos = [ { name: '平台', value: 'PC', category: 'platform', }, { name: 'subject_nsfw', value: '1', category: 'checkbox', }, ]; // ref links // https://vgmdb.net/album/9683 // https://vgmdb.net/album/134285 // https://vgmdb.net/album/122607 // https://vgmdb.net/album/86808 const vgmdbModel = { key: 'vgmdb', description: 'vgmdb', host: ['vgmdb.net'], type: SubjectTypeId.music, pageSelectors: [ { selector: '#innermain > h1', }, ], controlSelector: { selector: '#innermain > h1', }, itemList: [], }; const commonSelectors$2 = { selector: '#album_infobit_large', subSelector: 'tr > td:first-child', sibling: true, }; const creditsSelectors = { selector: '#collapse_credits table', subSelector: 'tr > td:first-child', sibling: true, }; vgmdbModel.itemList.push( // afterGetWikiData 里面 // { // name: '唱片名', // selector: { // selector: '#innermain > h1 > [lang=ja]', // }, // category: 'subject_title', // }, { name: '录音', selector: [ Object.assign(Object.assign({}, commonSelectors$2), { keyWord: 'Organizations' }), ], }, /* { name: '目录编号', selector: [ { ...commonSelectors, keyWord: 'Catalog Number', }, ], pipes: ['t'] }, */ { name: '条形码', selector: [ Object.assign(Object.assign({}, commonSelectors$2), { keyWord: 'Barcode' }), ], pipes: ['t'] }, { name: '发售日期', selector: [ Object.assign(Object.assign({}, commonSelectors$2), { keyWord: 'Release Date', nextSelector: { selector: 'a', } }), ], pipes: ['date'] }, { name: '价格', selector: [ Object.assign(Object.assign({}, commonSelectors$2), { keyWord: 'Price' }), ], }, { name: '版本特性', selector: [ Object.assign(Object.assign({}, commonSelectors$2), { keyWord: 'Media Format' }), ], }, { name: '播放时长', selector: [ { selector: '#tracklist', subSelector: 'span.smallfont', sibling: true, keyWord: 'Total length', }, { selector: '#tracklist', subSelector: 'span.smallfont', sibling: true, keyWord: 'Disc length', }, ], }, { name: '艺术家', selector: [ Object.assign(Object.assign({}, creditsSelectors), { keyWord: ['Performer', 'Vocalist'] }), ], pipes: ['ti'], }, { name: '作曲', selector: [ Object.assign(Object.assign({}, creditsSelectors), { keyWord: 'Composer' }), ], pipes: ['ti'], }, { name: '作词', selector: [ Object.assign(Object.assign({}, creditsSelectors), { keyWord: ['Lyricist', 'Lyrics'] }), ], pipes: ['ti'], }, { name: '编曲', selector: [ Object.assign(Object.assign({}, creditsSelectors), { keyWord: 'Arranger' }), ], pipes: ['ti'], }); // ref links // https://www.amazon.co.jp/dp/B07FQ5WPM3/ // https://www.amazon.co.jp/dp/B0D456FXL4 // https://www.amazon.co.jp/dp/B07GQXDHLN const amazonJpMusicModel = { key: 'amazon_jp_music', description: 'amazon jp music', host: ['amazon.co.jp', 'www.amazon.co.jp'], type: SubjectTypeId.music, pageSelectors: [ { selector: '#wayfinding-breadcrumbs_container .a-unordered-list .a-list-item:first-child', subSelector: '.a-link-normal', keyWord: ['ミュージック', 'Music', 'MUSIC', '音楽'], }, { selector: '#nav-subnav .nav-a:first-child img[alt="デジタルミュージック"]', }, { selector: '#detailBullets_feature_div + .a-unordered-list', subSelector: '.a-list-item', keyWord: ['ミュージック', '音楽'], }, ], controlSelector: { selector: '#title', }, itemList: [], }; const commonSelectors$3 = [ // 2021-05 日亚改版 { selector: '#richProductInformation_feature_div', subSelector: 'ol.a-carousel li', }, { selector: '#detailBullets_feature_div .detail-bullet-list', subSelector: 'li .a-list-item', }, { selector: '#detail_bullets_id .bucket .content', subSelector: 'li', }, ]; amazonJpMusicModel.itemList.push({ name: '名称', selector: { selector: '#productTitle', }, category: 'subject_title', }, { name: '艺术家', selector: [ { selector: '#bylineInfo', subSelector: '.author', keyWord: '\\(アーティスト\\)', nextSelector: [ { selector: '.contributorNameID', }, { selector: 'a', }, ], }, { selector: '#byline .author span.a-size-medium', }, { selector: '#bylineInfo .author > a', }, { selector: '#bylineInfo .contributorNameID', }, ], category: 'creator', pipes: ['k'], }, { name: '碟片数量', selector: commonSelectors$3.map((s) => { return Object.assign(Object.assign({}, s), { keyWord: ['ディスク枚数'] }); }), }, { name: '内容简介', selector: [ { selector: '#productDescription', subSelector: 'h3', sibling: true, keyWord: ['内容紹介', '内容'], }, { selector: '#productDescription', }, ], category: 'subject_summary', }, { name: '价格', selector: [ { selector: '#corePrice_feature_div > div > div > span.a-price.aok-align-center > span.a-offscreen', }, { selector: '#corePriceDisplay_desktop_feature_div > div.a-section.a-spacing-none.aok-align-center.aok-relative > span.aok-offscreen', }, { selector: '#declarative_ > table > tbody > tr > td.a-text-right.dp-new-col > span > a > span', }, ], }); // ref links // https://music.douban.com/subject/36072428/ // https://music.douban.com/subject/34956124/ const doubanMusicModel = { key: 'douban_music', description: 'douban music', host: ['music.douban.com'], type: SubjectTypeId.music, pageSelectors: [ { selector: '#db-nav-music', }, ], controlSelector: { selector: '#wrapper h1', }, itemList: [], }; doubanMusicModel.itemList.push({ name: '音乐名', selector: { selector: '#wrapper h1', }, category: 'subject_title', }, // textNode silbing 暂时不支持 /* { name: '发售日期', selector: [ { ...attr, keyWord: '发行时间', }, ], category: 'date', }, { name: '艺术家', selector: [ { ...attr, keyWord: '表演者', } ] }, { name: '流派', selector: { ...attr, keyWord: '流派', }, }, { name: '别名', selector: { ...attr, keyWord: '又名', }, category: 'alias', }, { name: '版本特性', selector: { ...attr, keyWord: '介质', }, }, { name: '碟片数量', selector: { ...attr, keyWord: '唱片数', }, }, { name: '厂牌', selector: { ...attr, keyWord: '出版者', }, }, */ { name: '音乐简介', selector: [ { selector: '.related_info', subSelector: 'h2', keyWord: '简介', sibling: true, }, ], category: 'subject_summary', }, { name: 'cover', selector: { selector: '#mainpic > span > a > img', }, category: 'cover', }); // 新增的 site model 需要在这里配置 const configs = { [getchuGameModel.key]: getchuGameModel, [erogamescapeModel.key]: erogamescapeModel, [amazonSubjectModel.key]: amazonSubjectModel, [steamdbModel.key]: steamdbModel, [steamModel.key]: steamModel, [dangdangBookModel.key]: dangdangBookModel, [jdBookModel.key]: jdBookModel, [doubanGameModel.key]: doubanGameModel, [doubanGameEditModel.key]: doubanGameEditModel, [dlsiteGameModel.key]: dlsiteGameModel, [dmmGameModel.key]: dmmGameModel, [adultComicModel.key]: adultComicModel, [moepedia.key]: moepedia, [vgmdbModel.key]: vgmdbModel, [amazonJpMusicModel.key]: amazonJpMusicModel, [doubanMusicModel.key]: doubanMusicModel, }; const charaModelDict = { [dlsiteGameCharaModel.key]: dlsiteGameCharaModel, [dmmGameCharaModel.key]: dmmGameCharaModel, }; function findModelByHost(host) { const keys = Object.keys(configs); const models = []; for (let i = 0; i < keys.length; i++) { const hosts = configs[keys[i]].host; if (hosts.includes(host)) { models.push(configs[keys[i]]); // return configs[keys[i]]; } } return models; } function getCharaModel(key) { const keys = Object.keys(charaModelDict); const targetKey = keys.find((k) => { var _a; return ((_a = charaModelDict[k]) === null || _a === void 0 ? void 0 : _a.siteKey) == key; }); if (!targetKey) return null; return charaModelDict[targetKey]; } // support GM_XMLHttpRequest function fetchInfo(url, type, opts = {}, TIMEOUT = 10 * 1000) { var _a; const method = ((_a = opts === null || opts === void 0 ? void 0 : opts.method) === null || _a === void 0 ? void 0 : _a.toUpperCase()) || 'GET'; // @ts-ignore { const gmXhrOpts = Object.assign({}, opts); if (method === 'POST' && gmXhrOpts.body) { gmXhrOpts.data = gmXhrOpts.body; } if (opts.decode) { type = 'arraybuffer'; } return new Promise((resolve, reject) => { // @ts-ignore GM_xmlhttpRequest(Object.assign({ method, timeout: TIMEOUT, url, responseType: type, onload: function (res) { if (res.status === 404) { reject(404); } if (opts.decode && type === 'arraybuffer') { let decoder = new TextDecoder(opts.decode); resolve(decoder.decode(res.response)); } else { resolve(res.response); } }, onerror: reject }, gmXhrOpts)); }); } } function fetchBinary(url, opts = {}) { return fetchInfo(url, 'blob', opts); } function fetchText(url, opts = {}, TIMEOUT = 10 * 1000) { return fetchInfo(url, 'text', opts, TIMEOUT); } function fetchJson(url, opts = {}) { return fetchInfo(url, 'json', opts); } function genRandomStr(len) { return Array.apply(null, Array(len)) .map(function () { return (function (chars) { return chars.charAt(Math.floor(Math.random() * chars.length)); })('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'); }) .join(''); } function formatDate(time, fmt = 'yyyy-MM-dd') { const date = new Date(time); const components = { 'M+': date.getMonth() + 1, 'd+': date.getDate(), 'h+': date.getHours(), 'm+': date.getMinutes(), 's+': date.getSeconds(), 'q+': Math.floor((date.getMonth() + 3) / 3), S: date.getMilliseconds(), }; // Replace year fmt = fmt.replace(/(y+)/i, (_, yearMatch) => (date.getFullYear() + '').slice(4 - yearMatch.length)); // Replace other components for (const [key, value] of Object.entries(components)) { fmt = fmt.replace(new RegExp(`(${key})`, 'i'), (_, match) => match.length === 1 ? value.toString() : String(value).padStart(match.length, '0')); } return fmt; } function dealDate(input) { // Regular expressions to match various date formats const regexPatterns = [ { pattern: /(\d{4})年(\d{1,2})月(\d{1,2})日?/, format: '$1-$2-$3' }, { pattern: /(\d{4})年(\d{1,2})月/, format: '$1-$2' }, { pattern: /(\d{4})[/-](\d{1,2})$/, format: '$1-$2' }, { pattern: /.*?(\d{4})\/(\d{1,2})\/(\d{1,2}).*?/, format: '$1-$2-$3' }, ]; for (const { pattern, format } of regexPatterns) { const match = input.replace(/\s/g, '').match(pattern); if (match) { return format.replace(/\$(\d+)/g, (_, number) => String(match[number]).padStart(2, '0')); } } // input is not a valid date if (isNaN(Date.parse(input))) { return input; } return formatDate(input); } function isEqualDate(d1, d2) { const resultDate = new Date(d1); const originDate = new Date(d2); if (resultDate.getFullYear() === originDate.getFullYear() && resultDate.getMonth() === originDate.getMonth() && resultDate.getDate() === originDate.getDate()) { return true; } return false; } const pipeFnDict = { // t: 去除开头和结尾的空格 t: trimSpace, // ta: 去除所有空格 ta: trimAllSpace, // ti: 去除空格,在 getWikiItem 里面,使用 innerText 取文本 ti: trimSpace, // k: 去除关键字; k: trimKeywords, // p: 括号 p: trimParenthesis, // pn: 括号不含数字 pn: trimParenthesisN, // num: 提取数字 num: getNum, date: getDate, }; function getStr(pipe) { return (pipe.out || pipe.rawInfo).trim(); } function trim(pipe, textList) { let str = getStr(pipe); return Object.assign(Object.assign({}, pipe), { out: str.replace(new RegExp(textList.join('|'), 'g'), '') }); } function trimAllSpace(pipe) { let str = getStr(pipe); return Object.assign(Object.assign({}, pipe), { out: str.replace(/\s/g, '') }); } function trimSpace(pipe) { let str = getStr(pipe); return Object.assign(Object.assign({}, pipe), { out: str.trim() }); } function trimParenthesis(pipe) { const textList = ['\\(.*?\\)', '(.*?)']; // const textList = ['\\([^d]*?\\)', '([^d]*?)']; // 去掉多余的括号信息 return trim(pipe, textList); } // 保留括号里面的数字. 比如一些图书的 1 2 3 function trimParenthesisN(pipe) { // const textList = ['\\(.*?\\)', '(.*?)']; const textList = ['\\([^d]*?\\)', '([^d]*?)']; // 去掉多余的括号信息 return trim(pipe, textList); } function trimKeywords(pipe, keyWords) { return trim(pipe, keyWords.map((k) => `${k}\s*?(:|:)?`)); } function getNum(pipe) { let str = getStr(pipe); const m = str.match(/\d+/); return { rawInfo: pipe.rawInfo, out: m ? m[0] : '', }; } function getDate(pipe) { let dataStr = getStr(pipe); return { rawInfo: pipe.rawInfo, out: dealDate(dataStr), }; } /** * * @param str 原字符串 * @param pipes 管道 * @returns 处理后的字符串 */ function dealTextByPipe(str, pipes, argsDict = {}) { let current = { rawInfo: str }; pipes = pipes || []; for (const p of pipes) { if (p instanceof Function) { // @TODO 支持传递参数 current = p(current); } else { if (argsDict[p]) { current = pipeFnDict[p](current, ...argsDict[p]); } else { current = pipeFnDict[p](current); } } } return current.out || str; } const adultComicTools = { hooks: { async afterGetWikiData(infos) { const res = []; for (const info of infos) { let newInfo = Object.assign({}, info); if (info.name === '作者') { const lists = document.querySelectorAll('#info-table > div.info-box .author-list > li'); if (lists && lists.length > 1) { newInfo.value = Array.from(lists) .map((node) => node.textContent.trim()) .join(', '); } } if (newInfo) { res.push(Object.assign({}, newInfo)); } } // getCover 判断 data-src。这里就禁用了 // const $img = document.querySelector('#sample-image > figure > img'); // if ($img) { // const info: SingleInfo = { // category: 'cover', // name: 'cover', // value: { // url: $img.getAttribute('data-src'), // dataUrl: $img.getAttribute('data-src'), // }, // }; // res.push(info); // } return res; }, }, }; /** * convert base64/URLEncoded data component to raw binary data held in a string * https://stackoverflow.com/questions/4998908/convert-data-uri-to-file-then-append-to-formdata * @param dataURI */ function dataURItoBlob(dataURI) { var byteString; if (dataURI.split(',')[0].indexOf('base64') >= 0) byteString = atob(dataURI.split(',')[1]); else byteString = decodeURI(dataURI.split(',')[1]); // instead of unescape // separate out the mime component var mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0]; // write the bytes of the string to a typed array var ia = new Uint8Array(byteString.length); for (var i = 0; i < byteString.length; i++) { ia[i] = byteString.charCodeAt(i); } return new Blob([ia], { type: mimeString }); } function getImageDataByURL(url, opts = {}) { if (!url) return Promise.reject('invalid img url'); return new Promise(async (resolve, reject) => { try { const blob = await fetchBinary(url, opts); var reader = new FileReader(); reader.onloadend = function () { resolve(reader.result); }; reader.readAsDataURL(blob); reader.onerror = reject; } catch (e) { reject(e); } }); } /** * convert to img Element to base64 string * @param $img */ function convertImgToBase64($img) { const canvas = document.createElement('canvas'); canvas.width = $img.width; canvas.height = $img.height; const ctx = canvas.getContext('2d'); ctx.drawImage($img, 0, 0, $img.width, $img.height); const dataURL = canvas.toDataURL('image/png'); return dataURL; } const amazonUtils = { dealTitle(str) { str = str.trim().split('\n')[0].trim(); // str = str.split(/\s[((][^0-9))]+?[))]/)[0] // 去掉尾部括号的内容, (1) (1) 这类不处理 const textList = [ '\\([^0-9]+?\\)$', '([^0-9]+?)$', '\\(.+?\\d+.+?\\)$', '(.+?\\d+.+?)$', ]; // 去掉多余的括号信息 str = str.replace(new RegExp(textList.join('|'), 'g'), '').trim(); // return str.replace(/\s[((][^0-9))]+?[))]$/g, '').trim(); return str; }, // 获取 URL 的 dp getUrlDp(str) { const m = str.match(/\/dp\/(.*?)\//); if (m) { return m[1]; } return ''; }, }; const amazonJpBookTools = { filters: [ { category: 'subject_title', dealFunc: amazonUtils.dealTitle, }, ], hooks: { async beforeCreate() { const $t = document.querySelector('#title'); const bookTypeList = document.querySelectorAll('#tmmSwatches ul > li.swatchElement'); const books = document.querySelectorAll('#tmmSwatches > .a-row div'); if ($t && ((bookTypeList && bookTypeList.length > 1) || (books && books.length > 1))) { const $div = document.createElement('div'); const $s = document.createElement('span'); $s.style.color = 'red'; $s.style.fontWeight = '600'; $s.innerHTML = '注意: '; const $txt = document.createElement('span'); $txt.innerHTML = '书籍存在多种版本,请优先选择实体书创建。(辅助创建脚本)'; $div.appendChild($s); $div.appendChild($txt); $div.style.padding = '6px 0'; $t.insertAdjacentElement('afterend', $div); // 没有简介时,使用 kindle 版本的介绍 const $desc = document.querySelector('#bookDescription_feature_div .a-expander-content'); if (!$desc) { const btns = document.querySelectorAll('#tmmSwatches ul > li.swatchElement .a-button-text'); if (btns && btns.length) { const url = Array.from(btns) .map((a) => a.href) .filter((h) => h.match(/^http/))[0]; if (url) { return { payload: { auxSite: { url, prefs: { originNames: ['ISBN', '名称'], }, }, }, }; } } } } return true; }, async afterGetWikiData(infos) { const res = []; for (const info of infos) { let newInfo = Object.assign({}, info); if (info.name === '页数') { let val = (info.value || '').trim().replace(/ページ|页/, ''); if (val && val.length < 8 && val.indexOf('予約商品') === -1) { newInfo.value = val; } else { newInfo = null; } } else if (info.name === '播放时长') { newInfo.value = info.value.replace('時間', '小时').replace(/ /g, ''); } else if (info.name === '价格') { let val = (info.value || '').replace(/来自|より/, '').trim(); newInfo.value = val; } if (newInfo) { res.push(Object.assign({}, newInfo)); } } const $cover = document.querySelector('#imgTagWrapperId>img'); if ($cover && !res.find((obj) => obj.name === 'cover')) { let url = ''; if ($cover.hasAttribute('data-old-hires')) { url = $cover.getAttribute('data-old-hires'); } else if ($cover.hasAttribute('data-a-dynamic-image')) { try { const obj = JSON.parse($cover.getAttribute('data-a-dynamic-image')); const urlArr = Object.keys(obj).sort().reverse(); if (urlArr && urlArr.length > 0) { url = urlArr[0]; } } catch (error) { } } // 如果还是没有图片链接 if (!url) { url = $cover.src; } let dataUrl = url; try { if (url) { dataUrl = await getImageDataByURL(url); } } catch (error) { } const info = { category: 'cover', name: 'cover', value: { url, dataUrl, }, }; res.push(info); } return res; }, }, }; async function getCoverInfo(res) { const $cover = document.querySelector('#imgTagWrapperId>img'); if ($cover && !res.find((obj) => obj.name === 'cover')) { let url = ''; if ($cover.hasAttribute('data-old-hires')) { url = $cover.getAttribute('data-old-hires'); } else if ($cover.hasAttribute('data-a-dynamic-image')) { try { const obj = JSON.parse($cover.getAttribute('data-a-dynamic-image')); const urlArr = Object.keys(obj).sort().reverse(); if (urlArr && urlArr.length > 0) { url = urlArr[0]; } } catch (error) { } } // 如果还是没有图片链接 if (!url) { url = $cover.src; } let dataUrl = url; try { if (url) { dataUrl = await getImageDataByURL(url); } } catch (error) { } const info = { category: 'cover', name: 'cover', value: { url, dataUrl, }, }; return info; } } const amazonJpMusicTools = { hooks: { async afterGetWikiData(infos) { const res = []; for (const item of infos) { if (item.name === '艺术家') { item.value = item.value.replace(/\//g, '、'); } res.push(item); } const date = document.querySelector('#declarative_ .title-text > span'); if (date) { const m = date.innerHTML.trim().match(/(\d{4})\/(\d{1,2})\/(\d{1,2})/); if (m) { res.push({ name: '发售日期', value: `${m[1]}-${m[2]}-${m[3]}`, }); } } const coverInfo = await getCoverInfo(res); if (coverInfo) { res.push(coverInfo); } const $tracks = document.querySelector('#music-tracks'); if ($tracks) { const discArr = [...$tracks.querySelectorAll('h4 + .a-row table')].map((table) => { return [...table.querySelectorAll('tr > td:nth-child(2)')].map((td) => { return { title: td.innerHTML.trim(), }; }); }); res.push({ category: 'ep', // 名字留空 name: '', value: discArr, }); } return res; }, }, }; const dlsiteTools = { hooks: { async afterGetWikiData(infos) { var _a; const res = []; for (const info of infos) { let val = info.value; if (val && typeof val === 'string' && !/http/.test(val) && ['原画', '剧本', '音乐', '游戏类型', '声优', '作者'].includes(info.name)) { const v = info.value.split('/'); if (v && v.length > 1) { val = v.map((s) => s.trim()).join(', '); } } res.push(Object.assign(Object.assign({}, info), { value: val })); } if (location.hostname.includes('dlsite.com')) { res.push({ name: 'website', value: `DLsite|${location.origin + location.pathname}`, category: 'listItem', }); } const cover = infos.find((obj) => obj.name === 'cover'); if (!cover) { let url = (_a = document.querySelector('meta[property="og:image"]')) === null || _a === void 0 ? void 0 : _a.content; if (url) { let dataUrl = url; try { if (url) { dataUrl = await getImageDataByURL(url, { headers: { Referer: url, }, }); } } catch (error) { } res.push({ category: 'cover', name: 'cover', value: { url, dataUrl, }, }); } } return res; }, }, filters: [ { category: 'date', dealFunc(str) { if (/年/.test(str)) { return dealDate(str.replace(/日.+$/, '日')); } return str; }, }, ], }; const dlsiteCharaTools = { hooks: { async afterGetWikiData(infos, model, el) { var _a; const res = [...infos]; const txt = ((_a = el.querySelector('p')) === null || _a === void 0 ? void 0 : _a.textContent) || ''; res.push({ name: '姓名', value: txt.split('\n')[0], category: 'crt_name', }); res.push({ name: 'CV', value: (txt.split('\n').find((s) => s.includes('CV')) || '') .replace('CV:', '') .trim(), }); let idx = txt.indexOf('\n\n'); if (idx === -1) { idx = 0; } else { idx = idx + 2; } res.push({ name: '人物简介', value: txt.slice(idx), category: 'crt_summary', }); return res; }, }, }; async function getCover($d, site) { let url; let dataUrl = ''; if ($d.tagName.toLowerCase() === 'a') { url = $d.getAttribute('href'); } else if ($d.tagName.toLowerCase() === 'img') { url = $d.getAttribute('src'); const dataSrc = $d.getAttribute('data-src'); if (dataSrc) { url = dataSrc; } } if (!url) return; try { // 在其它网站上获取的相对路径的链接 // @TODO 这里临时使用的全局变量来处理 if (!/^https?:/.test(url)) { let baseUrl = window._fetch_url_bg || location.href; url = new URL(url, baseUrl).href; } // 跨域的图片不能用这种方式 // dataUrl = convertImgToBase64($d as any); let opts = {}; if (site.includes('getchu')) { opts.headers = { Referer: location.href, }; if (!location.href.includes('getchu.com') && window._fetch_url_bg) { opts.headers.Referer = window._fetch_url_bg; } } dataUrl = await getImageDataByURL(url, opts); if (dataUrl) { return { url, dataUrl, }; } } catch (error) { return { url, dataUrl: url, }; } } const charaInfoDict = { 趣味: '爱好', 誕生日: '生日', '3サイズ': 'BWH', スリーサイズ: 'BWH', 身長: '身高', 血液型: '血型', }; const dmmTools = { hooks: { async afterGetWikiData(infos) { var _a; const res = []; const hasCover = infos.some((info) => info.category == 'cover'); for (const info of infos) { let val = info.value; res.push(Object.assign(Object.assign({}, info), { value: val })); } if (!hasCover) { // 使用 slider 里面的第一个图片 const slides = $qa('.image-slider.slick-slider li.slick-slide'); if (slides) { let url; let dataUrl = ''; const targetSlide = Array.from(slides).find((slide) => slide.dataset.slickIndex === '0'); url = (_a = targetSlide.querySelector('img')) === null || _a === void 0 ? void 0 : _a.getAttribute('src'); if (url) { try { dataUrl = await getImageDataByURL(url); } catch (error) { dataUrl = url; } res.push({ name: 'cover', category: 'cover', value: { url, dataUrl, }, }); } } else { const coverInfo = await getWikiItem({ name: 'cover', selector: [ { selector: '#if_view', isIframe: true, subSelector: 'body', nextSelector: { selector: '#guide-head > img', }, }, ], category: 'cover', }, 'dmm_game'); coverInfo && res.push(coverInfo); } } return res; }, }, filters: [ { category: 'date', dealFunc(str) { const re = /\d{4}\/\d{1,2}(\/\d{1,2})?/; const m = str.match(re); if (m) { return dealDate(m[0]); } return str; }, }, ], }; const dmmCharaTools = { hooks: { async afterGetWikiData(infos, model, $el) { const res = [...infos]; const $nameTxt = $el.querySelector('.guide-tx16.guide-bold.guide-lin-hgt'); if ($nameTxt) { // (きのみや なのか) const nameTxt = $nameTxt.textContent; if (nameTxt.match(/((.*))/)) { res.push({ name: '纯假名', value: nameTxt.match(/((.*))/)[1], }); } const cvTxt = $nameTxt.nextSibling.textContent; if (/CV/.test(cvTxt)) { res.push({ name: 'CV', value: cvTxt.replace(/CV:/, '').replace(/\s/g, ''), }); } } const boxArr = Array.from($el.querySelectorAll('.box')); for (const $box of boxArr) { const txtArr = $box.textContent .trim() .split(/:|:/) .map((s) => s.trim()); if (charaInfoDict[txtArr[0]]) { res.push({ name: charaInfoDict[txtArr[0]], value: txtArr[1], }); } else { res.push({ name: txtArr[0], value: txtArr[1], }); } } const $summary = $nameTxt.closest('div').cloneNode(true); $summary.firstElementChild.remove(); res.push({ name: '人物简介', value: $summary.textContent, category: 'crt_summary', }); return res; }, }, }; const doubanTools = { hooks: { async beforeCreate() { const href = window.location.href; if (/\/game\//.test(href) && !/\/game\/\d+\/edit/.test(href)) { return { payload: { auxSite: { url: document.querySelector('.th-modify > a').href, prefs: { originNames: ['平台', '发行日期'], targetNames: 'all', }, }, }, }; } }, async afterGetWikiData(infos) { const res = []; for (const info of infos) { if (['平台', '别名'].includes(info.name)) { const pArr = info.value.split('/').map((i) => { return Object.assign(Object.assign({}, info), { value: i.trim() }); }); res.push(...pArr); } else if (info.category === 'cover') { res.push(Object.assign({}, info)); } else { let val = info.value; if (val && typeof val === 'string') { const v = info.value.split('/'); if (v && v.length > 1) { val = v.map((s) => s.trim()).join(', '); } } if (info.name === '游戏类型' && val) { val = val.replace('游戏, ', '').trim(); } res.push(Object.assign(Object.assign({}, info), { value: val })); } } // 特殊处理平台 const $plateform = findElement({ selector: '#content .game-attr', subSelector: 'dt', sibling: true, keyWord: '平台', }); if ($plateform) { const aList = $plateform.querySelectorAll('a') || []; for (const $a of aList) { res.push({ name: '平台', value: getText($a).replace(/\/.*/, '').trim(), category: 'platform', }); } } return res; }, }, filters: [], }; const doubanGameEditTools = { hooks: { async beforeCreate() { const href = window.location.href; return /\/game\/\d+\/edit/.test(href); }, async afterGetWikiData(infos) { const res = []; for (const info of infos) { const arr = Object.assign({}, info); if (['平台', '别名'].includes(info.name)) { const plateformDict = { ARC: 'Arcade', NES: 'FC', 红白机: 'FC', 街机: 'Arcade', }; const pArr = info.value.split(',').map((i) => { let v = i.trim(); if (plateformDict[v]) { v = plateformDict[v]; } return Object.assign(Object.assign({}, info), { value: v }); }); res.push(...pArr); } else if (arr.category === 'cover' && arr.value && arr.value.url) { try { const url = arr.value.url.replace('/spic/', '/lpic/'); const dataUrl = await getImageDataByURL(url); const coverItem = Object.assign(Object.assign({}, arr), { value: { dataUrl, url, } }); res.push(coverItem); } catch (error) { console.error(error); } } else if (arr.name === '游戏类型') { arr.value = arr.value.replace(/,(?!\s)/g, ', '); res.push(arr); } else if (arr.name === '开发') { arr.value = arr.value.replace(/,(?!\s)/g, ', '); res.push(arr); } else { res.push(arr); } } // 描述 const inputList = document.querySelectorAll('input[name="target"][type="hidden"]'); inputList.forEach(($input) => { const val = $input.value; if (val === 'description') { const $target = $input .closest('form') .querySelector('.desc-form-item #thing_desc_options_0'); if ($target) { res.push({ name: '游戏简介', value: $target.value, category: 'subject_summary', }); } } }); return res; }, }, filters: [], }; const doubanMusicTools = { hooks: { async afterGetWikiData(infos) { var _a; const res = []; for (const item of infos) { res.push(item); } const $info = document.querySelector('#info'); if ($info) { const nameDict = { 又名: { name: '别名', category: 'alias', }, 发行时间: { name: '发售日期', category: 'date', }, 介质: { name: '版本特性', }, 唱片数: { name: '碟片数量', }, 流派: { name: '流派', }, 出版者: { name: '厂牌', }, 表演者: { name: '艺术家', }, 条形码: { name: '条形码', } }; $info.querySelectorAll('.pl').forEach((pl) => { let val = ''; if (pl.nextSibling.TEXT_NODE === 3) { val = pl.nextSibling.textContent.trim(); } let key = pl.textContent.trim().split(':')[0]; const anchors = pl.querySelectorAll('a'); if (anchors && anchors.length) { val = [...anchors].map((a) => a.textContent.trim()).join('、'); } if (!val) { return; } if (key in nameDict) { const target = nameDict[key]; res.push(Object.assign(Object.assign({}, target), { value: val })); } }); } const discNum = ((_a = res.find((item) => item.name === '碟片数量')) === null || _a === void 0 ? void 0 : _a.value) || 1; const tracks = [ ...document.querySelectorAll('.track-list ul.track-items > li'), ].map((item) => { const order = item.getAttribute('data-track-order'); const orderNum = order ? parseInt(order) : 0; const titleRaw = item.textContent.trim(); const durationReg = /\s*\d{1,2}:\d{1,2}$/; if (durationReg.test(titleRaw)) { const m = titleRaw.match(durationReg); return { title: titleRaw.replace(durationReg, ''), duration: m[0].trim(), order: orderNum, }; } return { title: item.textContent.trim(), order: orderNum, }; }); const discArr = []; let curDisc = []; for (let i = 0; i < tracks.length; i++) { const track = tracks[i]; if (track.order === 0) { if (curDisc.length) { discArr.push(curDisc); curDisc = []; } continue; } curDisc.push(track); } if (curDisc.length) { discArr.push(curDisc); } if (discArr.length && discArr.length == discNum) { res.push({ category: 'ep', name: '', value: discArr, }); } else { console.warn('碟片数量不匹配', discNum, discArr); } return res; }, }, filters: [], }; var ErogamescapeCategory; (function (ErogamescapeCategory) { ErogamescapeCategory["game"] = "game"; ErogamescapeCategory["brand"] = "brand"; ErogamescapeCategory["creater"] = "creater"; ErogamescapeCategory["music"] = "music"; ErogamescapeCategory["pov"] = "pov"; ErogamescapeCategory["character"] = "character"; })(ErogamescapeCategory || (ErogamescapeCategory = {})); const erogamescapeTools = { hooks: { async beforeCreate() { var _a; const $el = findElement([ { selector: '#links', subSelector: 'a', keyWord: 'Getchu.com', }, { selector: '#bottom_inter_links_main', subSelector: 'a', keyWord: 'Getchu.com', }, ]); const softQuery = (_a = $el === null || $el === void 0 ? void 0 : $el.getAttribute('href')) === null || _a === void 0 ? void 0 : _a.match(/\?id=\d+$/); if (softQuery) { return { payload: { auxSite: { url: `http://www.getchu.com/soft.phtml${softQuery[0]}`, opts: { cookie: 'getchu_adalt_flag=getchu.com', decode: 'EUC-JP', }, prefs: { originNames: ['游戏名'], targetNames: ['cover'], }, }, }, }; } return true; }, }, filters: [], }; const getchuTools = { dealTitle(str) { str = str.trim().split('\n')[0]; str = str .split('+')[0] .replace(/(このタイトルの関連商品)/, '') .trim(); return str.replace(/\s[^ ]*?(スペシャルプライス版|限定版|通常版|廉価版|復刻版|初回.*?版|描き下ろし).*?$|<.*>$/g, ''); }, getExtraCharaInfo(txt) { const re = /[^\s]+?[::]/g; const matchedArr = txt.match(re); if (!matchedArr) return []; const infoArr = txt.split(re); const res = []; matchedArr.forEach((item, idx) => { const val = (infoArr[idx + 1] || '').trim(); if (val) { res.push({ name: item.replace(/:|:/, ''), value: val, }); } }); return res; }, getCharacterInfo($t) { const charaData = []; const $name = $t.closest('dt').querySelector('h2'); let name; if ($name.querySelector('charalist')) { const $charalist = $name.querySelector('charalist'); name = getText($charalist); } else { if ($name.classList.contains('chara-name') && $name.querySelector('br')) { name = $name .querySelector('br') .nextSibling.textContent.split(/(|\(|\sCV|新建角色/)[0]; } else { name = getText($name).split(/(|\(|\sCV|新建角色/)[0]; } } charaData.push({ name: '姓名', value: name.replace(/\s/g, ''), category: 'crt_name', }); charaData.push({ name: '日文名', value: name, }); const nameTxt = getText($name); if (nameTxt.match(/((.*))/)) { charaData.push({ name: '纯假名', value: nameTxt.match(/((.*))/)[1], }); } const cvMatch = nameTxt.match(/(?<=CV[::]).+/); if (cvMatch) { charaData.push({ name: 'CV', value: cvMatch[0].replace(/\s/g, ''), }); } const $img = $t.closest('tr').querySelector('td > img'); if ($img) { charaData.push({ name: 'cover', value: convertImgToBase64($img), category: 'crt_cover', }); } // 处理杂项 参考 id=1074002 id=735329 id=1080370 // id=1080431 // id=840936 // dd tag const $dd = $t.closest('dt').nextElementSibling; const $clonedDd = $dd.cloneNode(true); Array.prototype.forEach.call($clonedDd.querySelectorAll('span[style^="font-weight"]'), (node) => { const t = getText(node).trim(); t.split(/\n/g).forEach((el) => { const extraInfo = getchuTools.getExtraCharaInfo(el); if (extraInfo.length) { charaData.push(...extraInfo); } else { const c = el.match(/B.*W.*H\d+/); if (c) { charaData.push({ name: 'BWH', value: c[0], }); } } }); node.remove(); }); charaData.push({ name: '人物简介', value: getText($clonedDd).trim(), category: 'crt_summary', }); const dict = { 誕生日: '生日', '3サイズ': 'BWH', スリーサイズ: 'BWH', 身長: '身高', 血液型: '血型', }; charaData.forEach((item) => { if (dict[item.name]) { item.name = dict[item.name]; } }); return charaData; }, }; const getchuSiteTools = { hooks: { async beforeCreate() { const $t = document.querySelector('#soft-title'); if (!$t) return false; const rawTitle = $t.textContent.trim(); if (/[同人グッズ|同人誌|同人音楽]/.test(rawTitle)) return false; return true; }, }, filters: [ { category: 'subject_title', dealFunc: getchuTools.dealTitle, }, ], }; const getchuCharaTools = { hooks: { async afterGetWikiData(infos, model, $el) { const res = [...infos]; const $chara = $el.querySelector('h2.chara-name'); if ($chara) { res.push(...getchuTools.getCharacterInfo($chara)); } return res; }, }, }; function dealTitle(str) { str = str.trim().split('\n')[0]; return str.replace(/\s[^ ]*?(スペシャルプライス版|限定版|通常版|廉価版|復刻版|初回.*?版|描き下ろし|パッケージ版).*?$|<.*>$/g, ''); } const moepediaTools = { hooks: { async beforeCreate() { const $el = findElement([ { selector: '.body-shop_list > .body-shop_item > a[href*="www.getchu.com/soft.phtml?id="]', }, ]); const url = $el === null || $el === void 0 ? void 0 : $el.getAttribute('href'); if (url) { return { payload: { auxSite: { url, opts: { cookie: 'getchu_adalt_flag=getchu.com', decode: 'EUC-JP', }, prefs: { originNames: ['游戏名'], targetNames: ['游戏简介'], }, }, }, }; } return true; }, async afterGetWikiData(infos) { const res = []; for (const info of infos) { let val = info.value; if (info.name === '游戏名') { val = dealTitle(val); } else if (['原画', '剧本', '音乐', '主题歌演唱', '游戏类型'].includes(info.name)) { val = val.replace(/\n\s*/g, ', '); } else if (info.name === '售价') { val = val.replace(/.*¥/, '¥'); } res.push(Object.assign(Object.assign({}, info), { value: val })); } return res; }, }, }; const steamTools = { hooks: { async beforeCreate() { return { payload: { disableDate: true, }, }; }, async afterGetWikiData(infos) { const res = []; for (const info of infos) { let newInfo = Object.assign({}, info); if (info.name === 'website') { // https://steamcommunity.com/linkfilter/?url=https://www.koeitecmoamerica.com/ryza/ const arr = newInfo.value.split('?url='); newInfo.value = arr[1] || ''; newInfo.category = 'website,listItem'; } res.push(Object.assign({}, newInfo)); } if (location.hostname === 'store.steampowered.com') { res.push({ name: 'website', value: `Steam|${location.origin + location.pathname}`, category: 'website,listItem', }); } return res; } }, filters: [ { category: 'date', dealFunc(str) { if (/年/.test(str)) { return dealDate(str.replace(/\s/g, '')); } return formatDate(str); }, }, ], }; const steamdbTools = { hooks: { async beforeCreate() { return { payload: { disableDate: true, }, }; }, async afterGetWikiData(infos) { var _a; const res = []; for (const info of infos) { let newInfo = Object.assign({}, info); if (info.name === '游戏引擎') { newInfo.value = info.value.replace(/^Engine\./g, ''); } if (info.name === '游戏简介') { if (info.value.match(/\n.*?Steam charts, data, update history\.$/)) { newInfo.value = info.value.split('\n')[0]; } } // if (info.name === '游戏类型') { // newInfo.value = info.value.split(',').map((s) => s.trim()).join('、'); // } if (info.name === 'cover') { if (info.value.url) { const a = info.value.url; const h = a.lastIndexOf('?'); const m = a.substring((h === -1 ? a.length : h) - 4); const scaleUrl = a.substring(0, a.length - m.length) + '_2x' + m; let dataUrl = ''; try { dataUrl = await getImageDataByURL(scaleUrl); } catch (error) { } if (dataUrl) { newInfo.value = { url: scaleUrl, dataUrl, }; } } } if (newInfo) { res.push(Object.assign({}, newInfo)); } } const $appInstall = document.querySelector('#js-app-install'); const appId = (_a = $appInstall === null || $appInstall === void 0 ? void 0 : $appInstall.href.match(/steam:\/\/launch\/(\d+)/)) === null || _a === void 0 ? void 0 : _a[1]; if (appId) { res.push({ name: 'website', value: `Steam|https://store.steampowered.com/app/${appId}`, category: 'listItem', }); } // 额外信息 [...document.querySelectorAll('#info > table > tbody > tr > td.span3')].forEach(item => { const sibling = item.nextElementSibling; if (sibling.innerHTML.includes('General Mature Content')) { res.push({ name: 'subject_nsfw', value: '1', category: 'checkbox', }); return; } if (item.innerHTML.includes('name_localized')) { const names = [...sibling.querySelectorAll('table > tbody > tr')].map((tr) => { const name = tr.querySelector('td:nth-child(1)').textContent.trim(); const value = tr.querySelector('td:nth-child(2)').textContent.trim(); return { name, value, }; }); const gameName = res.find(info => info.name === '游戏名'); const enName = names.find(name => name.name === 'english'); const jpName = names.find(name => name.name === 'japanese'); if (enName && gameName) { if (gameName.value !== enName.value) { res.push({ name: '别名', value: `英文|${enName.value}`, }); } } if (jpName && gameName) { if (gameName.value !== jpName.value) { res.push({ name: '别名', value: `日文|${jpName.value}`, }); } } const tchName = names.find(name => name.name === 'tchinese'); if (tchName) { res.push({ name: '别名', value: `繁中|${tchName.value}`, }); } } }); return res; }, }, filters: [ { category: 'date', dealFunc(str) { const arr = str.split('–'); if (!arr[0]) return ''; return formatDate(arr[0].trim()); }, }, ], }; const vgmdbTools = { hooks: { async beforeCreate() { const $t = document.querySelector('#innermain h1 > .albumtitle[lang=ja]'); if ($t && $t.style.display === 'none') { const $div = document.createElement('div'); const $s = document.createElement('span'); $s.style.color = 'red'; $s.style.fontWeight = '600'; $s.innerHTML = '注意: '; const $txt = document.createElement('span'); $txt.innerHTML = '请设置 Title / Name Language 为 Original。(辅助创建脚本)'; $div.appendChild($s); $div.appendChild($txt); $div.style.padding = '6px 0'; $t.parentElement.insertAdjacentElement('afterend', $div); } return true; }, async afterGetWikiData(infos) { var _a, _b; const res = []; const $h1 = document.querySelector('#innermain > h1'); res.push({ name: '唱片名', value: $h1.innerText, category: 'subject_title', }); for (const item of infos) { if (item.name === '价格' && item.value.includes('Not for Sale')) { continue; } // 替换数字 if (item.name === '版本特性' && /\d+/.test(item.value)) { res.push(Object.assign(Object.assign({}, item), { value: item.value.replace(/\d+/, '').trim() })); continue; } if (item.name === '目录编号') { res.push(Object.assign(Object.assign({}, item), { value: item.value.trim().split(' ')[0].trim() })); continue; } res.push(item); } /* for (const $td of document.querySelectorAll( '#album_infobit_large td:first-child' )) { const label = ($td as HTMLElement).innerText; const links = $td.nextElementSibling.querySelectorAll('a'); let value = ''; if ($td.nextElementSibling.querySelector('.artistname[lang=ja]')) { value = [...links] .map( (node) => node.querySelector('.artistname[lang=ja]').textContent ) .join('、'); } else { value = [...links].map((node) => node.innerText).join('、'); } let name = ''; if (label.includes('Performer')) { name = '艺术家'; } else if (label.includes('Composer')) { name = '作曲'; } else if (label.includes('Arranger')) { name = '编曲'; } else if (label.includes('Lyricist')) { name = '作词'; } if (name) { res.push({ name, value, }); } } */ let url = (_a = document.querySelector('meta[property="og:image"]')) === null || _a === void 0 ? void 0 : _a.content; if (!url) { try { url = document.querySelector('#coverart').style.backgroundImage.match(/url\(["']?([^"']*)["']?\)/)[1]; } catch (error) { } } if (url) { let dataUrl = url; try { if (url) { dataUrl = await getImageDataByURL(url, { headers: { Referer: url, }, }); } } catch (error) { } res.push({ category: 'cover', name: 'cover', value: { url, dataUrl, }, }); } // 曲目列表 const tracklist = document.querySelector('#tracklist'); if (tracklist) { let tableList = tracklist.querySelectorAll('.tl > table'); (_b = document.querySelectorAll('#tlnav > li > a')) === null || _b === void 0 ? void 0 : _b.forEach((item) => { if (item.innerHTML.includes('Japanese')) { const rel = item.getAttribute('rel'); tableList = document.querySelectorAll(`#${rel} > table`); } }); const discArr = [...tableList].map((table) => { return [...table.querySelectorAll('tr')].map((item) => { const $tds = item.querySelectorAll('td'); return { title: $tds[1].innerText.trim(), duration: $tds[2].innerText.trim(), }; }); }); res.push({ category: 'ep', // 名字留空 name: '', value: discArr, }); } return res; }, }, }; function trimParenthesis$1(str) { const textList = ['\\([^d]*?\\)', '([^d]*?)']; // 去掉多余的括号信息 return str.replace(new RegExp(textList.join('|'), 'g'), '').trim(); } function identity(x) { return x; } const noOps = () => Promise.resolve(true); function getHooks(siteConfig, timing) { var _a; const hooks = ((_a = sitesFuncDict[siteConfig.key]) === null || _a === void 0 ? void 0 : _a.hooks) || {}; return hooks[timing] || noOps; } function getCharaHooks(config, timing) { var _a; const hooks = ((_a = charaFuncDict[config.key]) === null || _a === void 0 ? void 0 : _a.hooks) || {}; return hooks[timing] || noOps; } function dealFuncByCategory(key, category) { var _a; let fn; if ((_a = sitesFuncDict[key]) === null || _a === void 0 ? void 0 : _a.filters) { const obj = sitesFuncDict[key].filters.find((x) => x.category === category); fn = obj && obj.dealFunc; } if (fn) { return fn; } else { return (str = '') => identity(str.trim()); } } const sitesFuncDict = { amazon_jp_book: amazonJpBookTools, dangdang_book: { filters: [ { category: 'date', dealFunc(str) { return dealDate(str.replace(/出版时间[::]/, '').trim()); }, }, { category: 'subject_title', dealFunc(str) { return trimParenthesis$1(str); }, }, ], }, jd_book: { filters: [ { category: 'subject_title', dealFunc(str) { return trimParenthesis$1(str); }, }, ], }, getchu_game: getchuSiteTools, erogamescape: erogamescapeTools, steam_game: steamTools, steamdb_game: steamdbTools, douban_game: doubanTools, douban_game_edit: doubanGameEditTools, dlsite_game: dlsiteTools, dmm_game: dmmTools, adultcomic: adultComicTools, moepedia: moepediaTools, vgmdb: vgmdbTools, amazon_jp_music: amazonJpMusicTools, douban_music: doubanMusicTools, }; // 存储新建角色的钩子函数和 filters const charaFuncDict = { dlsite_game_chara: dlsiteCharaTools, dmm_game_chara: dmmCharaTools, getchu_chara: getchuCharaTools, }; /** * 处理单项 wiki 信息 * @param str * @param category * @param keyWords */ function dealItemText(str, category = '', keyWords = []) { if (['subject_summary', 'subject_title'].indexOf(category) !== -1) { return str; } const textList = ['\\(.*?\\)', '(.*?)']; // 去掉多余的括号信息 // const keyStr = keyWords.sort((a, b) => b.length - a.length).join('|') // `(${keyStr})(${separators.join('|')})?` return str .replace(new RegExp(textList.join('|'), 'g'), '') .replace(new RegExp(keyWords.map((k) => `${k}\s*?(:|:)?`).join('|'), 'g'), '') .replace(/[^\d:]+?(:|:)/, '') .trim(); } async function getWikiItem(infoConfig, site) { var _a; if (!infoConfig) return; const sl = infoConfig.selector; let $d; let targetSelector; if (sl instanceof Array) { let i = 0; targetSelector = sl[i]; while (!($d = findElement(targetSelector)) && i < sl.length) { targetSelector = sl[++i]; } } else { targetSelector = sl; $d = findElement(targetSelector); } if (!$d) return; let keyWords; if (targetSelector.keyWord instanceof Array) { keyWords = targetSelector.keyWord; } else { keyWords = [targetSelector.keyWord]; } let val; let txt = getText($d); if ((_a = infoConfig.pipes) === null || _a === void 0 ? void 0 : _a.includes('ti')) { txt = getInnerText($d); } const pipeArgsDict = { k: [keyWords], }; switch (infoConfig.category) { case 'cover': case 'crt_cover': val = await getCover($d, site); break; case 'subject_summary': // 优先使用 innerText const innerTxt = getInnerText($d); if (innerTxt) { txt = innerTxt; } case 'alias': case 'subject_title': // 有管道优先使用管道处理数据. 兼容之前使用写法 if (infoConfig.pipes) { val = dealTextByPipe(txt, infoConfig.pipes, pipeArgsDict); } else { val = dealFuncByCategory(site, infoConfig.category)(txt); } break; case 'website': val = dealFuncByCategory(site, 'website')($d.getAttribute('href')); break; case 'date': // 有管道优先使用管道处理数据. 兼容之前使用写法 if (infoConfig.pipes) { val = dealTextByPipe(txt, infoConfig.pipes, pipeArgsDict); } else { // 日期预处理,不能删除 val = dealItemText(txt, infoConfig.category, keyWords); val = dealFuncByCategory(site, infoConfig.category)(val); } break; default: // 有管道优先使用管道处理数据. 兼容之前使用写法 if (infoConfig.pipes) { val = dealTextByPipe(txt, infoConfig.pipes, pipeArgsDict); } else { val = dealItemText(txt, infoConfig.category, keyWords); } } // 信息后处理 if (infoConfig.category === 'creator') { val = val.replace(/\s/g, ''); } if (val) { return { name: infoConfig.name, value: val, category: infoConfig.category, }; } } async function getWikiData(siteConfig, el) { el ? setCtxDom(el) : clearCtxDom(); const r = await Promise.all(siteConfig.itemList.map((item) => getWikiItem(item, siteConfig.key))); clearCtxDom(); const defaultInfos = siteConfig.defaultInfos || []; let rawInfo = r.filter((i) => i); const hookRes = await getHooks(siteConfig, 'afterGetWikiData')(rawInfo, siteConfig); if (Array.isArray(hookRes) && hookRes.length) { rawInfo = hookRes; } return [...rawInfo, ...defaultInfos]; } async function getCharaData(model, el) { el ? setCtxDom(el) : clearCtxDom(); const r = await Promise.all(model.itemList.map((item) => getWikiItem(item, model.key))); clearCtxDom(); const defaultInfos = model.defaultInfos || []; let rawInfo = r.filter((i) => i); const hookRes = await getCharaHooks(model, 'afterGetWikiData')(rawInfo, model, el); if (Array.isArray(hookRes) && hookRes.length) { rawInfo = hookRes; } return [...rawInfo, ...defaultInfos]; } /** * 过滤搜索结果: 通过名称以及日期 * @param items * @param subjectInfo * @param opts */ function filterResults(items, subjectInfo, opts = {}, isSearch = true) { var _a; if (!items) return; // 只有一个结果时直接返回, 不再比较日期 if (items.length === 1 && isSearch) { const result = items[0]; return result; // if (isEqualDate(result.releaseDate, subjectInfo.releaseDate)) { // } } let results = new Fuse(items, Object.assign({}, opts)).search(subjectInfo.name); if (!results.length) return; // 有参考的发布时间 if (subjectInfo.releaseDate) { for (const item of results) { const result = item.item; if (result.releaseDate) { if (isEqualDate(result.releaseDate, subjectInfo.releaseDate)) { return result; } } } } // 比较名称 const nameRe = new RegExp(subjectInfo.name.trim()); for (const item of results) { const result = item.item; if (nameRe.test(result.name) || nameRe.test(result.greyName)) { return result; } } return (_a = results[0]) === null || _a === void 0 ? void 0 : _a.item; } function getQueryInfo(items) { let info = {}; items.forEach((item) => { if (item.category === 'subject_title') { info.name = item.value; } if (item.category === 'date') { info.releaseDate = item.value; } if (item.category === 'ASIN') { info.asin = item.value; } if (item.category === 'ISBN') { info.isbn = item.value; } }); return info; } /** * 插入控制的按钮 * @param $t 父节点 * @param cb 返回 Promise 的回调 */ function insertControlBtn($t, cb) { if (!$t) return; const $div = document.createElement('div'); const $s = document.createElement('span'); $s.classList.add('e-wiki-new-subject'); $s.innerHTML = '新建'; const $search = $s.cloneNode(); $search.innerHTML = '新建并查重'; $div.appendChild($s); $div.appendChild($search); $t.insertAdjacentElement('afterend', $div); $s.addEventListener('click', async (e) => { await cb(e); }); $search.addEventListener('click', async (e) => { if ($search.innerHTML !== '新建并查重') return; $search.innerHTML = '查重中...'; try { await cb(e, true); $search.innerHTML = '新建并查重'; } catch (e) { if (e === 'notmatched') { $search.innerHTML = '未查到条目'; } console.error(e); } }); } /** * 插入新建角色控制的按钮 * @param $t 父节点 * @param cb 返回 Promise 的回调 */ function insertControlBtnChara($t, cb) { if (!$t) return; const $div = document.createElement('div'); const $s = document.createElement('a'); $s.classList.add('e-wiki-new-character'); // $s.setAttribute('target', '_blank') $s.innerHTML = '添加新虚拟角色'; $div.appendChild($s); $t.insertAdjacentElement('afterend', $div); $s.addEventListener('click', async (e) => { await cb(e); }); } function addCharaUI($t, names, cb) { if (!$t) return; if (!names.length) { console.warn('没有虚拟角色可用'); return; } // @TODO 增加全部 // const btn = `添加新虚拟角色`; const $div = htmlToElement(`