Greasy Fork

Greasy Fork is available in English.

迁移豆瓣收藏到 Bangumi

migrate douban collection to bangumi and export douban collection

当前为 2022-06-27 提交的版本,查看 最新版本

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        migrate douban collection to bangumi
// @name:zh-CN  迁移豆瓣收藏到 Bangumi
// @namespace   https://github.com/22earth
// @description migrate douban collection to bangumi and export douban collection
// @description:zh-cn 迁移豆瓣动画收藏到 Bangumi.
// @include     /^https?:\/\/(bangumi|bgm|chii)\.(tv|in)\/?$/
// @include     https://movie.douban.com/mine
// @include     https://search.douban.com/movie/subject_search*
// @author      22earth
// @homepage    https://github.com/22earth/gm_scripts
// @version     0.0.6
// @run-at      document-end
// @grant       GM_registerMenuCommand
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_getResourceText
// @require     https://cdn.staticfile.org/fuse.js/6.4.0/fuse.min.js
// @resource    bangumiDataURL https://unpkg.com/[email protected]/dist/data.json
// ==/UserScript==


function sleep(num) {
    return new Promise((resolve) => {
        setTimeout(resolve, num);
    });
}
function randomSleep(max = 400, min = 200) {
    return sleep(randomNum(max, min));
}
function randomNum(max, min) {
    return Math.floor(Math.random() * (max - min + 1)) + min;
}

// support GM_XMLHttpRequest
let retryCounter = 0;
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) {
                        retryCounter = 0;
                        reject(404);
                    }
                    else if (res.status === 302 && retryCounter < 5) {
                        retryCounter++;
                        resolve(fetchInfo(res.finalUrl, type, opts, TIMEOUT));
                    }
                    if (opts.decode && type === 'arraybuffer') {
                        retryCounter = 0;
                        let decoder = new TextDecoder(opts.decode);
                        resolve(decoder.decode(res.response));
                    }
                    else {
                        retryCounter = 0;
                        resolve(res.response);
                    }
                }, onerror: (e) => {
                    retryCounter = 0;
                    reject(e);
                } }, gmXhrOpts));
        });
    }
}
function fetchText(url, opts = {}, TIMEOUT = 10 * 1000) {
    return fetchInfo(url, 'text', opts, TIMEOUT);
}
function fetchJson(url, opts = {}) {
    return fetchInfo(url, 'json', opts);
}

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 = {}));

function formatDate(time, fmt = 'yyyy-MM-dd') {
    const date = new Date(time);
    var o = {
        '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(), //毫秒
    };
    if (/(y+)/i.test(fmt)) {
        fmt = fmt.replace(RegExp.$1, (date.getFullYear() + '').substr(4 - RegExp.$1.length));
    }
    for (var k in o) {
        if (new RegExp('(' + k + ')', 'i').test(fmt)) {
            fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? o[k] : ('00' + o[k]).substr(('' + o[k]).length));
        }
    }
    return fmt;
}
function dealDate(dataStr) {
    // 2019年12月19
    let l = [];
    if (/\d{4}年\d{1,2}月(\d{1,2}日?)?/.test(dataStr)) {
        l = dataStr
            .replace('日', '')
            .split(/年|月/)
            .filter((i) => i);
    }
    else if (/\d{4}\/\d{1,2}(\/\d{1,2})?/.test(dataStr)) {
        l = dataStr.split('/');
    }
    else if (/\d{4}-\d{1,2}(-\d{1,2})?/.test(dataStr)) {
        return dataStr;
    }
    else {
        return dataStr;
    }
    return l
        .map((i) => {
        if (i.length === 1) {
            return `0${i}`;
        }
        return i;
    })
        .join('-');
}
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 SEARCH_RESULT = 'search_result';

/**
 * 过滤搜索结果: 通过名称以及日期
 * @param items
 * @param subjectInfo
 * @param opts
 */
function filterResults(items, subjectInfo, opts = {}, isSearch = true) {
    var _a;
    if (!items)
        return;
    // 只有一个结果时直接返回, 不再比较日期
    if (items.length === 1 && isSearch) {
        return items[0];
    }
    var results = new Fuse(items, Object.assign({}, opts)).search(subjectInfo.name);
    if (!results.length)
        return;
    // 有参考的发布时间
    const tempResults = [];
    if (subjectInfo.releaseDate) {
        for (const obj of results) {
            const result = obj.item;
            if (result.releaseDate) {
                // 只有年的时候
                if (result.releaseDate.length === '4') {
                    if (result.releaseDate === subjectInfo.releaseDate.slice(0, 4)) {
                        return result;
                    }
                }
                else {
                    if (isEqualDate(result.releaseDate, subjectInfo.releaseDate)) {
                        return result;
                    }
                }
                // 过滤年份不一致的数据
                if (result.releaseDate.slice(0, 4) === subjectInfo.releaseDate.slice(0, 4)) {
                    tempResults.push(obj);
                }
            }
        }
    }
    // 比较名称
    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) ||
            nameRe.test(result.rawName)) {
            return result;
        }
    }
    results = tempResults;
    return (_a = results[0]) === null || _a === void 0 ? void 0 : _a.item;
}
const typeIdDict = {
    dropped: {
        name: '抛弃',
        id: '5',
    },
    on_hold: {
        name: '搁置',
        id: '4',
    },
    do: {
        name: '在看',
        id: '3',
    },
    collect: {
        name: '看过',
        id: '2',
    },
    wish: {
        name: '想看',
        id: '1',
    },
};
function findInterestStatusById(id) {
    for (let key in typeIdDict) {
        const obj = typeIdDict[key];
        if (obj.id === id) {
            return Object.assign({ key: key }, obj);
        }
    }
}
async function getSearchResultByGM() {
    return new Promise((resolve, reject) => {
        const listenId = window.gm_val_listen_id;
        if (listenId) {
            GM_removeValueChangeListener(listenId);
        }
        window.gm_val_listen_id = GM_addValueChangeListener(
        // const listenId = GM_addValueChangeListener(
        SEARCH_RESULT, (n, oldValue, newValue) => {
            console.log('enter promise');
            const now = +new Date();
            if (newValue.type === SEARCH_RESULT &&
                newValue.timestamp &&
                newValue.timestamp < now) {
                // GM_removeValueChangeListener(listenId);
                resolve(newValue.data);
            }
            reject('mismatch timestamp');
        });
    });
}
async function setSearchResultByGM(data) {
    const res = {
        type: SEARCH_RESULT,
        timestamp: +new Date(),
        data,
    };
    GM_setValue(SEARCH_RESULT, res);
}

function getBgmHost() {
    return `${location.protocol}//${location.host}`;
}
function getSubjectId(url) {
    const m = url.match(/(?:subject|character)\/(\d+)/);
    if (!m)
        return '';
    return m[1];
}
function getUserId(url) {
    // https://bgm.tv/user/a_little
    const m = url.match(/user\/(.*)/);
    if (!m)
        return '';
    return m[1];
}
function insertLogInfo($sibling, txt) {
    const $log = document.createElement('div');
    $log.classList.add('e-wiki-log-info');
    // $log.setAttribute('style', 'color: tomato;');
    $log.innerHTML = txt;
    $sibling.parentElement.insertBefore($log, $sibling);
    $sibling.insertAdjacentElement('afterend', $log);
    return $log;
}
function convertItemInfo($item) {
    let $subjectTitle = $item.querySelector('h3>a.l');
    let itemSubject = {
        name: $subjectTitle.textContent.trim(),
        rawInfos: $item.querySelector('.info').textContent.trim(),
        // url 没有协议和域名
        url: $subjectTitle.getAttribute('href'),
        greyName: $item.querySelector('h3>.grey')
            ? $item.querySelector('h3>.grey').textContent.trim()
            : '',
    };
    let matchDate = $item
        .querySelector('.info')
        .textContent.match(/\d{4}[\-\/\年]\d{1,2}[\-\/\月]\d{1,2}/);
    if (matchDate) {
        itemSubject.releaseDate = dealDate(matchDate[0]);
    }
    const $rateInfo = $item.querySelector('.rateInfo');
    if ($rateInfo) {
        const rateInfo = {};
        if ($rateInfo.querySelector('.fade')) {
            rateInfo.score = $rateInfo.querySelector('.fade').textContent;
            rateInfo.count = $rateInfo
                .querySelector('.tip_j')
                .textContent.replace(/[^0-9]/g, '');
        }
        else {
            rateInfo.score = '0';
            rateInfo.count = '少于10';
        }
        itemSubject.rateInfo = rateInfo;
    }
    const $rank = $item.querySelector('.rank');
    if ($rank) {
        itemSubject.rank = $rank.textContent.replace('Rank', '').trim();
    }
    const $collectInfo = $item.querySelector('.collectInfo');
    const collectInfo = {};
    const $comment = $item.querySelector('#comment_box');
    if ($comment) {
        collectInfo.comment = $comment.textContent.trim();
    }
    if ($collectInfo) {
        const textArr = $collectInfo.textContent.split('/');
        collectInfo.date = textArr[0].trim();
        textArr.forEach((str) => {
            if (str.match('标签')) {
                collectInfo.tags = str.replace(/标签:/, '').trim();
            }
        });
        const $starlight = $collectInfo.querySelector('.starlight');
        if ($starlight) {
            $starlight.classList.forEach((s) => {
                if (/stars\d/.test(s)) {
                    collectInfo.score = s.replace('stars', '');
                }
            });
        }
    }
    if (Object.keys(collectInfo).length) {
        itemSubject.collectInfo = collectInfo;
    }
    const $cover = $item.querySelector('.subjectCover img');
    if ($cover && $cover.tagName.toLowerCase() === 'img') {
        // 替换 cover/s --->  cover/l 是大图
        const src = $cover.getAttribute('src') || $cover.getAttribute('data-cfsrc');
        if (src) {
            itemSubject.cover = src.replace('pic/cover/s', 'pic/cover/l');
        }
    }
    return itemSubject;
}
function getItemInfos($doc = document) {
    const items = $doc.querySelectorAll('#browserItemList>li');
    const res = [];
    for (const item of Array.from(items)) {
        res.push(convertItemInfo(item));
    }
    return res;
}
function getTotalPageNum($doc = document) {
    const $multipage = $doc.querySelector('#multipage');
    let totalPageNum = 1;
    const pList = $multipage === null || $multipage === void 0 ? void 0 : $multipage.querySelectorAll('.page_inner>.p');
    if (pList && pList.length) {
        let tempNum = parseInt(pList[pList.length - 2].getAttribute('href').match(/page=(\d*)/)[1]);
        totalPageNum = parseInt(pList[pList.length - 1].getAttribute('href').match(/page=(\d*)/)[1]);
        totalPageNum = totalPageNum > tempNum ? totalPageNum : tempNum;
    }
    return totalPageNum;
}
function genCollectionURL(userId, subjectType, interestType) {
    const dict = {
        movie: 'anime',
        music: 'music',
        book: 'book',
    };
    return `https://bgm.tv/${dict[subjectType]}/list/${userId}/${interestType}`;
}
async function getAllPageInfo(userId, subjectType, interestType) {
    const url = genCollectionURL(userId, subjectType, interestType);
    console.info('bgm collection page: ', url);
    const rawText = await fetchText(url);
    const $doc = new DOMParser().parseFromString(rawText, 'text/html');
    const totalPageNum = getTotalPageNum($doc);
    const res = [...getItemInfos($doc)];
    let page = 2;
    while (page <= totalPageNum) {
        let reqUrl = url;
        const m = url.match(/page=(\d*)/);
        if (m) {
            reqUrl = reqUrl.replace(m[0], `page=${page}`);
        }
        else {
            reqUrl = `${reqUrl}?page=${page}`;
        }
        await sleep(500);
        console.info('fetch info: ', reqUrl);
        const rawText = await fetchText(reqUrl);
        const $doc = new DOMParser().parseFromString(rawText, 'text/html');
        res.push(...getItemInfos($doc));
        page += 1;
    }
    return res;
}
function loadIframe($iframe, subjectId) {
    return new Promise((resolve, reject) => {
        $iframe.src = `/update/${subjectId}`;
        let timer = setTimeout(() => {
            timer = null;
            reject('bangumi iframe timeout');
        }, 5000);
        $iframe.onload = () => {
            clearTimeout(timer);
            $iframe.onload = null;
            resolve(null);
        };
    });
}
async function getUpdateForm(subjectId) {
    const iframeId = 'e-userjs-update-interest';
    let $iframe = document.querySelector(`#${iframeId}`);
    if (!$iframe) {
        $iframe = document.createElement('iframe');
        $iframe.style.display = 'none';
        $iframe.id = iframeId;
        document.body.appendChild($iframe);
    }
    await loadIframe($iframe, subjectId);
    const $form = $iframe.contentDocument.querySelector('#collectBoxForm');
    return $form;
    // return $form.action;
}
/**
 * 更新用户收藏
 * @param subjectId 条目 id
 * @param data 更新数据
 */
async function updateInterest(subjectId, data) {
    // gh 暂时不知道如何获取,直接拿 action 了
    const $form = await getUpdateForm(subjectId);
    const formData = new FormData($form);
    const obj = Object.assign({ referer: 'ajax', tags: '', comment: '', update: '保存' }, data);
    for (let [key, val] of Object.entries(obj)) {
        if (!formData.has(key)) {
            formData.append(key, val);
        }
        else {
            // 标签和吐槽可以直接清空
            if (['tags', 'comment', 'rating'].includes(key)) {
                formData.set(key, val);
            }
            else if (!formData.get(key) && val) {
                formData.set(key, val);
            }
        }
    }
    await fetch($form.action, {
        method: 'POST',
        body: formData,
    });
}

var BangumiDomain;
(function (BangumiDomain) {
    BangumiDomain["chii"] = "chii.in";
    BangumiDomain["bgm"] = "bgm.tv";
    BangumiDomain["bangumi"] = "bangumi.tv";
})(BangumiDomain || (BangumiDomain = {}));
var Protocol;
(function (Protocol) {
    Protocol["http"] = "http";
    Protocol["https"] = "https";
})(Protocol || (Protocol = {}));
/**
 * 处理搜索页面的 html
 * @param info 字符串 html
 */
function dealSearchResults(info) {
    const results = [];
    let $doc = new DOMParser().parseFromString(info, 'text/html');
    let items = $doc.querySelectorAll('#browserItemList>li>div.inner');
    // get number of page
    let numOfPage = 1;
    let pList = $doc.querySelectorAll('.page_inner>.p');
    if (pList && pList.length) {
        let tempNum = parseInt(pList[pList.length - 2].getAttribute('href').match(/page=(\d*)/)[1]);
        numOfPage = parseInt(pList[pList.length - 1].getAttribute('href').match(/page=(\d*)/)[1]);
        numOfPage = numOfPage > tempNum ? numOfPage : tempNum;
    }
    if (items && items.length) {
        for (const item of Array.prototype.slice.call(items)) {
            let $subjectTitle = item.querySelector('h3>a.l');
            let itemSubject = {
                name: $subjectTitle.textContent.trim(),
                // url 没有协议和域名
                url: $subjectTitle.getAttribute('href'),
                greyName: item.querySelector('h3>.grey')
                    ? item.querySelector('h3>.grey').textContent.trim()
                    : '',
            };
            let matchDate = item
                .querySelector('.info')
                .textContent.match(/\d{4}[\-\/\年]\d{1,2}[\-\/\月]\d{1,2}/);
            if (matchDate) {
                itemSubject.releaseDate = dealDate(matchDate[0]);
            }
            let $rateInfo = item.querySelector('.rateInfo');
            if ($rateInfo) {
                if ($rateInfo.querySelector('.fade')) {
                    itemSubject.score = $rateInfo.querySelector('.fade').textContent;
                    itemSubject.count = $rateInfo
                        .querySelector('.tip_j')
                        .textContent.replace(/[^0-9]/g, '');
                }
                else {
                    itemSubject.score = '0';
                    itemSubject.count = '少于10';
                }
            }
            else {
                itemSubject.score = '0';
                itemSubject.count = '0';
            }
            results.push(itemSubject);
        }
    }
    else {
        return [];
    }
    return [results, numOfPage];
}
/**
 * 搜索条目
 * @param subjectInfo
 * @param type
 * @param uniqueQueryStr
 */
async function searchSubject(subjectInfo, bgmHost = 'https://bgm.tv', type = SubjectTypeId.all, uniqueQueryStr = '') {
    let releaseDate;
    if (subjectInfo && subjectInfo.releaseDate) {
        releaseDate = subjectInfo.releaseDate;
    }
    let query = (subjectInfo.name || '').trim();
    if (type === SubjectTypeId.book) {
        // 去掉末尾的括号并加上引号
        query = query.replace(/([^0-9]+?)|\([^0-9]+?\)$/, '');
        query = `"${query}"`;
    }
    if (uniqueQueryStr) {
        query = `"${uniqueQueryStr || ''}"`;
    }
    if (!query || query === '""') {
        console.info('Query string is empty');
        return;
    }
    const url = `${bgmHost}/subject_search/${encodeURIComponent(query)}?cat=${type}`;
    console.info('search bangumi subject URL: ', url);
    const rawText = await fetchText(url);
    const rawInfoList = dealSearchResults(rawText)[0] || [];
    // 使用指定搜索字符串如 ISBN 搜索时, 并且结果只有一条时,不再使用名称过滤
    if (uniqueQueryStr && rawInfoList && rawInfoList.length === 1) {
        return rawInfoList[0];
    }
    const options = {
        keys: ['name', 'greyName'],
    };
    return filterResults(rawInfoList, subjectInfo, options);
}
/**
 * 通过时间查找条目
 * @param subjectInfo 条目信息
 * @param pageNumber 页码
 * @param type 条目类型
 */
async function findSubjectByDate(subjectInfo, bgmHost = 'https://bgm.tv', pageNumber = 1, type) {
    if (!subjectInfo || !subjectInfo.releaseDate || !subjectInfo.name) {
        throw new Error('invalid subject info');
    }
    const releaseDate = new Date(subjectInfo.releaseDate);
    if (isNaN(releaseDate.getTime())) {
        throw `invalid releasedate: ${subjectInfo.releaseDate}`;
    }
    const sort = releaseDate.getDate() > 15 ? 'sort=date' : '';
    const page = pageNumber ? `page=${pageNumber}` : '';
    let query = '';
    if (sort && page) {
        query = '?' + sort + '&' + page;
    }
    else if (sort) {
        query = '?' + sort;
    }
    else if (page) {
        query = '?' + page;
    }
    const url = `${bgmHost}/${type}/browser/airtime/${releaseDate.getFullYear()}-${releaseDate.getMonth() + 1}${query}`;
    console.info('find subject by date: ', url);
    const rawText = await fetchText(url);
    let [rawInfoList, numOfPage] = dealSearchResults(rawText);
    const options = {
        threshold: 0.3,
        keys: ['name', 'greyName'],
    };
    let result = filterResults(rawInfoList, subjectInfo, options, false);
    if (!result) {
        if (pageNumber < numOfPage) {
            await sleep(300);
            return await findSubjectByDate(subjectInfo, bgmHost, pageNumber + 1, type);
        }
        else {
            throw 'notmatched';
        }
    }
    return result;
}
/**
 * 查找条目是否存在: 通过名称搜索或者日期加上名称的过滤查询
 * @param subjectInfo 条目基本信息
 * @param bgmHost bangumi 域名
 * @param type 条目类型
 */
async function checkExist(subjectInfo, bgmHost = 'https://bgm.tv', type, disabelDate) {
    const subjectTypeDict = {
        [SubjectTypeId.game]: 'game',
        [SubjectTypeId.anime]: 'anime',
        [SubjectTypeId.music]: 'music',
        [SubjectTypeId.book]: 'book',
        [SubjectTypeId.real]: 'real',
        [SubjectTypeId.all]: 'all',
    };
    let searchResult = await searchSubject(subjectInfo, bgmHost, type);
    console.info(`First: search result of bangumi: `, searchResult);
    if (searchResult && searchResult.url) {
        return searchResult;
    }
    if (disabelDate) {
        return;
    }
    searchResult = await findSubjectByDate(subjectInfo, bgmHost, 1, subjectTypeDict[type]);
    console.info(`Second: search result by date: `, searchResult);
    return searchResult;
}
async function checkAnimeSubjectExist(subjectInfo) {
    const result = await checkExist(subjectInfo, getBgmHost(), SubjectTypeId.anime, true);
    return result;
}
const siteUtils = {
    name: 'Bangumi',
    contanerSelector: '#columnHomeB',
    getUserId: getUserId,
    getSubjectId: getSubjectId,
    updateInterest: updateInterest,
    checkSubjectExist: checkAnimeSubjectExist,
    getAllPageInfo: getAllPageInfo,
};

/**
 * 为页面添加样式
 * @param style
 */
/**
 * 下载内容
 * https://stackoverflow.com/questions/14964035/how-to-export-javascript-array-info-to-csv-on-client-side
 * @example
 * download(csvContent, 'dowload.csv', 'text/csv;encoding:utf-8');
 * BOM: data:text/csv;charset=utf-8,\uFEFF
 * @param content 内容
 * @param fileName 文件名
 * @param mimeType 文件类型
 */
function downloadFile(content, fileName, mimeType = 'application/octet-stream') {
    var a = document.createElement('a');
    a.href = URL.createObjectURL(new Blob([content], {
        type: mimeType,
    }));
    a.style.display = 'none';
    a.setAttribute('download', fileName);
    document.body.appendChild(a);
    a.click();
    document.body.removeChild(a);
}
/**
 * @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$1($iframe, src, TIMEOUT = 5000) {
    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 genCollectionURL$1(userId, interestType, subjectType = 'movie', start = 1) {
    const baseURL = `https://${subjectType}.douban.com/people/${userId}/${interestType}`;
    if (start === 1) {
        return baseURL;
    }
    else {
        return `${baseURL}?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
    }
}
function convertBangumiScore(num) {
    return Math.ceil(num / 2);
    // if (num < 4) {
    //   return 1;
    // }
    // if (num < 6) {
    //   return 2;
    // }
    // if (num < 8) return 3;
    // if (num < 9) return 4;
    // if (num === 10) return 5;
    // return 0;
}
function getSubjectId$1(url) {
    const m = url.match(/movie\.douban\.com\/subject\/(\d+)/);
    if (m) {
        return m[1];
    }
    return '';
}
function getUserId$1(homeURL) {
    let m = homeURL.match(/douban.com\/people\/([^\/]*)\//);
    if (m) {
        return m[1];
    }
    return '';
}
function convertItemInfo$1($item) {
    var _a, _b, _c;
    let $subjectTitle = $item.querySelector('.info .title a');
    // 默认第二项为日文名
    const titleArr = $subjectTitle.textContent
        .trim()
        .split('/')
        .map((str) => str.trim());
    const rawInfos = $item.querySelector('.info .intro').textContent.trim();
    let itemSubject = {
        name: titleArr[1],
        rawInfos,
        url: $subjectTitle.getAttribute('href'),
        greyName: titleArr[0],
    };
    const $cover = $item.querySelector('.pic img');
    if ($cover && $cover.tagName.toLowerCase() === 'img') {
        const src = $cover.getAttribute('src');
        if (src) {
            itemSubject.cover = src;
        }
    }
    const jpDateReg = /(\d+-\d\d\-\d\d)(?:\(日本\))/;
    const dateReg = /\d+-\d\d\-\d\d/;
    let m;
    if ((m = rawInfos.match(jpDateReg))) {
        itemSubject.releaseDate = m[1];
    }
    else if ((m = rawInfos.match(dateReg))) {
        itemSubject.releaseDate = m[0];
    }
    const $collectInfo = $item.querySelector('.info');
    if ($collectInfo) {
        const collectInfo = {};
        collectInfo.date = (_a = $collectInfo
            .querySelector('li .date')) === null || _a === void 0 ? void 0 : _a.textContent.trim();
        collectInfo.tags = (_b = $collectInfo
            .querySelector('li .tags')) === null || _b === void 0 ? void 0 : _b.textContent.replace('标签: ', '').trim();
        collectInfo.comment = (_c = $collectInfo
            .querySelector('li .comment')) === null || _c === void 0 ? void 0 : _c.textContent.trim();
        const $rating = $collectInfo.querySelector('[class^=rating]');
        if ($rating) {
            const m = $rating.getAttribute('class').match(/\d/);
            if (m) {
                // 十分制
                collectInfo.score = +m[0] * 2;
            }
        }
        itemSubject.collectInfo = collectInfo;
    }
    return itemSubject;
}
function getTotalPageNum$1($doc = document) {
    const numStr = $doc.querySelector('.mode > .subject-num').textContent.trim();
    return Number(numStr.split('/')[1].trim());
}
/**
 * 拿到当前页面豆瓣用户收藏信息列表
 * @param $doc DOM
 */
function getItemInfos$1($doc = document) {
    const items = $doc.querySelectorAll('#content .grid-view > .item');
    const res = [];
    for (const item of Array.from(items)) {
        res.push(convertItemInfo$1(item));
    }
    return res;
}
/**
 * 获取所有分页的条目数据
 * @param userId 用户id
 * @param subjectType 条目类型
 * @param interestType 条目状态
 */
async function getAllPageInfo$1(userId, subjectType = 'movie', interestType) {
    let res = [];
    const url = genCollectionURL$1(userId, interestType, subjectType);
    const rawText = await fetchText(url);
    const $doc = new DOMParser().parseFromString(rawText, 'text/html');
    const totalPageNum = getTotalPageNum$1($doc);
    res = [...getItemInfos$1($doc)];
    // 16 分割
    let page = 16;
    while (page <= totalPageNum) {
        let reqUrl = genCollectionURL$1(userId, interestType, subjectType, page);
        await sleep(500);
        console.info('fetch info: ', reqUrl);
        const rawText = await fetchText(reqUrl);
        const $doc = new DOMParser().parseFromString(rawText, 'text/html');
        res.push(...getItemInfos$1($doc));
        page += 15;
    }
    return res;
}
function convertHomeSearchItem($item) {
    const dealHref = (href) => {
        if (/^https:\/\/movie\.douban\.com\/subject\/\d+\/$/.test(href)) {
            return href;
        }
        const urlParam = href.split('?url=')[1];
        if (urlParam) {
            return decodeURIComponent(urlParam.split('&')[0]);
        }
        else {
            throw 'invalid href';
        }
    };
    const $title = $item.querySelector('.title h3 > a');
    const href = dealHref($title.getAttribute('href'));
    const $ratingNums = $item.querySelector('.rating-info > .rating_nums');
    let ratingsCount = '';
    let averageScore = '';
    if ($ratingNums) {
        const $count = $ratingNums.nextElementSibling;
        const m = $count.innerText.match(/\d+/);
        if (m) {
            ratingsCount = m[0];
        }
        averageScore = $ratingNums.innerText;
    }
    let greyName = '';
    const $greyName = $item.querySelector('.subject-cast');
    if ($greyName) {
        greyName = $greyName.innerText;
    }
    return {
        name: $title.textContent.trim(),
        greyName: greyName.split('/')[0].replace('原名:', '').trim(),
        releaseDate: (greyName.match(/\d{4}$/) || [])[0],
        url: href,
        score: averageScore,
        count: ratingsCount,
    };
}
/**
 * 通过首页搜索的结果
 * @param query 搜索字符串
 */
async function getHomeSearchResults(query, cat = '1002') {
    const url = `https://www.douban.com/search?cat=${cat}&q=${encodeURIComponent(query)}`;
    console.info('Douban search URL: ', url);
    const rawText = await fetchText(url);
    const $doc = new DOMParser().parseFromString(rawText, 'text/html');
    const items = $doc.querySelectorAll('.search-result > .result-list > .result > .content');
    return Array.prototype.slice
        .call(items)
        .map(($item) => convertHomeSearchItem($item));
}
/**
 * 提取所有 search.douban.com 的条目信息
 * @param $doc 页面容器
 */
function getAllSearchResult($doc = document) {
    let items = $doc.querySelectorAll('#root .item-root');
    return Array.prototype.slice
        .call(items)
        .map(($item) => convertSubjectSearchItem($item));
}
/**
 * 提取 search.douban.com 的条目信息
 * @param $item 单项搜索结果容器 DOM
 */
function convertSubjectSearchItem($item) {
    // item-root
    const $title = $item.querySelector('.title a');
    let name = '';
    let greyName = '';
    let releaseDate = '';
    let rawName = '';
    if ($title) {
        const rawText = $title.textContent.trim();
        rawName = rawText;
        const yearRe = /\((\d{4})\)$/;
        releaseDate = (rawText.match(yearRe) || ['', ''])[1];
        let arr = rawText.split(/ (?!-)/);
        if (arr && arr.length === 2) {
            name = arr[0];
            greyName = arr[1].replace(yearRe, '');
        }
        else {
            arr = rawText.split(/ (?!(-|\w))/);
            name = arr[0];
            greyName = rawText.replace(name, '').trim().replace(yearRe, '').trim();
        }
    }
    let ratingsCount = '';
    let averageScore = '';
    const $ratingNums = $item.querySelector('.rating_nums');
    if ($ratingNums) {
        const $count = $ratingNums.nextElementSibling;
        const m = $count.textContent.match(/\d+/);
        if (m) {
            ratingsCount = m[0];
        }
        averageScore = $ratingNums.textContent;
    }
    return {
        name,
        rawName,
        url: $title.getAttribute('href'),
        score: averageScore,
        count: ratingsCount,
        releaseDate,
    };
}
/**
 * 单独类型搜索入口
 * @param query 搜索字符串
 * @param cat 搜索类型
 * @param type 获取传递数据的类型: gm 通过 GM_setValue, message 通过 postMessage
 */
async function getSubjectSearchResults(query, cat = '1002') {
    const url = `https://search.douban.com/movie/subject_search?search_text=${encodeURIComponent(query)}&cat=${cat}`;
    console.info('Douban search URL: ', url);
    const iframeId = 'e-userjs-search-subject';
    let $iframe = document.querySelector(`#${iframeId}`);
    if (!$iframe) {
        $iframe = document.createElement('iframe');
        $iframe.setAttribute('sandbox', 'allow-forms allow-same-origin allow-scripts');
        $iframe.style.display = 'none';
        $iframe.id = iframeId;
        document.body.appendChild($iframe);
    }
    // 这里不能使用 await 否则数据加载完毕了监听器还没有初始化
    loadIframe$1($iframe, url, 1000 * 10);
    return await getSearchResultByGM();
}
async function sendSearchResults() {
    const searchItems = getAllSearchResult();
    setSearchResultByGM(searchItems);
}
async function updateInterest$1(subjectId, data) {
    const interestObj = findInterestStatusById(data.interest);
    let query = '';
    if (data.interest !== undefined) {
        query = 'interest=' + interestObj.key;
    }
    let url = `https://movie.douban.com/j/subject/${subjectId}/interest?${query}`;
    const collectInfo = await fetchJson(url);
    const interestStatus = collectInfo.interest_status;
    const tags = collectInfo.tags;
    const $doc = new DOMParser().parseFromString(collectInfo.html, 'text/html');
    const $form = $doc.querySelector('form');
    const formData = new FormData($form);
    const sendData = {
        interest: interestObj.key,
        tags: data.tags,
        comment: data.comment,
        rating: convertBangumiScore(+data.rating) + '',
    };
    if (tags && tags.length) {
        sendData.tags = tags.join(' ');
    }
    if (interestStatus) {
        sendData.interest = interestStatus;
    }
    if (data.privacy === '1') {
        // @ts-ignore
        sendData.privacy = 'on';
    }
    for (let [key, val] of Object.entries(sendData)) {
        if (!formData.has(key)) {
            formData.append(key, val);
        }
        else if (formData.has(key) && !formData.get(key) && val) {
            formData.set(key, val);
        }
    }
    // share-shuo: douban  删除分享广播
    if (formData.has('share-shuo')) {
        formData.delete('share-shuo');
    }
    await fetch($form.action, {
        method: 'POST',
        headers: {
            'X-Requested-With': 'XMLHttpRequest',
        },
        body: formData,
    });
}
/**
 *
 * @param subjectInfo 条目信息
 * @param type 默认使用主页搜索
 * @returns 搜索结果
 */
async function checkAnimeSubjectExist$1(subjectInfo, type = 'home_search') {
    let query = (subjectInfo.name || '').trim();
    if (!query) {
        console.info('Query string is empty');
        return Promise.reject();
    }
    let rawInfoList;
    let searchResult;
    const options = {
        keys: ['name', 'greyName'],
    };
    if (type === 'home_search') {
        rawInfoList = await getHomeSearchResults(query);
    }
    else {
        rawInfoList = await getSubjectSearchResults(query);
    }
    searchResult = filterResults(rawInfoList, subjectInfo, options, true);
    console.info(`Search result of ${query} on Douban: `, searchResult);
    if (searchResult && searchResult.url) {
        return searchResult;
    }
}
const siteUtils$1 = {
    name: '豆瓣',
    contanerSelector: '#content .aside',
    getUserId: getUserId$1,
    getSubjectId: getSubjectId$1,
    getAllPageInfo: getAllPageInfo$1,
    updateInterest: updateInterest$1,
    checkSubjectExist: checkAnimeSubjectExist$1,
};

function insertControl(contanerSelector, name) {
    GM_addStyle(`
  .e-userjs-export-tool-container input {
    margin-bottom: 12px;
  }
  .e-userjs-export-tool-container .title {
    color: #F09199;
    font-weight: bold;
    font-size: 14px;
    margin: 12px 0;
    display: inline-block;
  }
  .e-userjs-export-tool-container .import-btn{
    margin-top: 12px;
  }
  .e-userjs-export-tool-container .export-btn {
    display: none;
  }
  .e-userjs-export-tool-container .retry-btn {
    display: none;
  }
  .ui-button {
    display: inline-block;
    line-height: 20px;
    font-size: 14px;
    text-align: center;
    color: #4c5161;
    border-radius: 4px;
    border: 1px solid #d0d0d5;
    padding: 9px 15px;
    min-width: 80px;
    background-color: #fff;
    background-repeat: no-repeat;
    background-position: center;
    text-decoration: none;
    box-sizing: border-box;
    transition: border-color .15s, box-shadow .15s, opacity .15s;
    font-family: inherit;
    cursor: pointer;
    overflow: visible;

    background-color: #2a80eb;
    color: #fff;
  }
`);
    const $parent = document.querySelector(contanerSelector);
    const $container = htmlToElement(`
<div class="e-userjs-export-tool-container">
<div>
  <span class="title">${name}主页 URL: </span><br/>
  <input placeholder="输入${name}主页的 URL" class="inputtext" autocomplete="off" type="text" size="30" name="tags" value="">
</div>
  <div>
<label for="movie-type-select">选择同步类型:</label>
<select name="movieType" id="movie-type-select">
    <option value="">所有</option>
    <option value="do">在看</option>
    <option value="wish">想看</option>
    <option value="collect">看过</option>
</select>
  </div>
  <button class="ui-button import-btn" type="submit">
导入${name}动画收藏
  </button>
  <br/>
  <button class="ui-button export-btn" type="submit">
导出${name}动画的收藏同步信息
  </button>
  <button class="ui-button retry-btn" type="submit">
重新同步失败的条目
  </button>
</div>
  `);
    $parent.appendChild($container);
    return $container;
}

let bangumiData = null;
const typeIdDict$1 = {
    dropped: {
        name: '抛弃',
        id: '5',
    },
    on_hold: {
        name: '搁置',
        id: '4',
    },
    do: {
        name: '在看',
        id: '3',
    },
    collect: {
        name: '看过',
        id: '2',
    },
    wish: {
        name: '想看',
        id: '1',
    },
};
function getBangumiSubjectId(name = '', greyName = '') {
    var _a;
    if (!bangumiData)
        return;
    const obj = bangumiData.items.find((item) => {
        let cnNames = [];
        if (item.titleTranslate && item.titleTranslate['zh-Hans']) {
            cnNames = item.titleTranslate['zh-Hans'];
        }
        return (item.title === name ||
            item.title === greyName ||
            cnNames.includes(greyName));
    });
    return (_a = obj === null || obj === void 0 ? void 0 : obj.sites) === null || _a === void 0 ? void 0 : _a.find((item) => item.site === 'bangumi').id;
}
function genCSVContent(infos) {
    const header = '\ufeff名称,别名,发行日期,地址,封面地址,收藏日期,我的评分,标签,吐槽,其它信息,类别,同步情况,搜索结果信息';
    let csvContent = '';
    const keys = Object.keys(infos);
    keys.forEach((key) => {
        infos[key].forEach((item) => {
            csvContent += `\r\n${item.name || ''},${item.greyName || ''},${item.releaseDate || ''}`;
            const subjectUrl = item.url;
            csvContent += `,${subjectUrl}`;
            const cover = item.cover || '';
            csvContent += `,${cover}`;
            const collectInfo = item.collectInfo || {};
            const collectDate = collectInfo.date || '';
            csvContent += `,${collectDate}`;
            const score = collectInfo.score || '';
            csvContent += `,${score}`;
            const tag = collectInfo.tags || '';
            csvContent += `,${tag}`;
            const comment = collectInfo.comment || '';
            csvContent += `,"${comment}"`;
            const rawInfos = item.rawInfos || '';
            csvContent += `,"${rawInfos}"`;
            csvContent += `,"${typeIdDict$1[key].name}"`;
            csvContent += `,${item.syncStatus || ''}`;
            // 新增搜索结果信息
            let searchResultStr = '';
            if (item.syncSubject) {
                const obj = item.syncSubject;
                searchResultStr = `${obj.name};${obj.greyName || ''};${obj.url || ''};${obj.rawName || ''}`;
            }
            // 同步信息
            csvContent += `,"${searchResultStr}"`;
        });
    });
    return header + csvContent;
}
// 区分是否为动画
function isJpMovie(item) {
    return item.rawInfos.indexOf('日本') !== -1;
}
function clearLogInfo($container) {
    $container
        .querySelectorAll('.e-wiki-log-info')
        .forEach((node) => node.remove());
}
function init(site) {
    let targetUtils;
    let originUtils;
    if (site === 'bangumi') {
        targetUtils = siteUtils$1;
        originUtils = siteUtils;
    }
    else {
        targetUtils = siteUtils;
        originUtils = siteUtils$1;
    }
    const $container = insertControl(originUtils.contanerSelector, targetUtils.name);
    const $input = $container.querySelector('input');
    const $importBtn = $container.querySelector('.import-btn');
    const $exportBtn = $container.querySelector('.export-btn');
    const $retryBtn = $container.querySelector('.retry-btn');
    const interestInfos = {
        do: [],
        collect: [],
        wish: [],
        dropped: [],
        on_hold: [],
    };
    $exportBtn.addEventListener('click', async (e) => {
        const $text = e.target;
        $text.value = '导出中...';
        let name = 'Bangumi';
        if (site === 'bangumi') {
            name = '豆瓣';
        }
        let strName = `${name}动画的收藏`;
        const csv = genCSVContent(interestInfos);
        // $text.value = '导出完成';
        $text.style.display = 'none';
        downloadFile(csv, `${strName}-${formatDate(new Date())}.csv`);
    });
    $retryBtn.addEventListener('click', async (e) => {
        try {
            bangumiData = JSON.parse(GM_getResourceText('bangumiDataURL'));
        }
        catch (e) {
            console.log('parse JSON:', e);
        }
        const userId = getUserIdFromInput($input.value, targetUtils.getUserId);
        if (!userId)
            return;
        const arr = getInterestTypeArr();
        for (let type of arr) {
            const res = interestInfos[type];
            for (let i = 0; i < res.length; i++) {
                let item = res[i];
                if (!item.syncStatus) {
                    item = await migrateCollection(originUtils, item, site, type);
                }
                res[i] = item;
            }
        }
        clearLogInfo($container);
        $exportBtn.style.display = 'inline-block';
        $retryBtn.style.display = 'inline-block';
    });
    $importBtn.addEventListener('click', async (e) => {
        try {
            bangumiData = JSON.parse(GM_getResourceText('bangumiDataURL'));
        }
        catch (e) {
            console.log('parse JSON:', e);
        }
        const userId = getUserIdFromInput($input.value, targetUtils.getUserId);
        if (!userId)
            return;
        const arr = getInterestTypeArr();
        for (let type of arr) {
            try {
                const res = (await targetUtils.getAllPageInfo(userId, 'movie', type));
                for (let i = 0; i < res.length; i++) {
                    let item = res[i];
                    item = await migrateCollection(originUtils, item, site, type);
                    res[i] = item;
                }
                interestInfos[type] = [...res];
            }
            catch (error) {
                console.error(error);
            }
        }
        clearLogInfo($container);
        $exportBtn.style.display = 'inline-block';
        $retryBtn.style.display = 'inline-block';
    });
}
function getUserIdFromInput(val, fn) {
    if (!val) {
        alert(`请输入${name}主页地址`);
        return '';
    }
    const userId = fn(val);
    if (!userId) {
        alert(`无效${name}主页地址`);
        return '';
    }
    return userId;
}
function getInterestTypeArr() {
    const $container = document.querySelector('.e-userjs-export-tool-container');
    const $select = $container.querySelector('#movie-type-select');
    // const arr: InterestType[] = ['wish'];
    let arr = ['do', 'collect', 'wish'];
    if ($select && $select.value) {
        arr = [$select.value];
    }
    return arr;
}
async function migrateCollection(siteUtils, item, site, type) {
    const subjectItem = Object.assign({}, item);
    const $container = document.querySelector('.e-userjs-export-tool-container');
    const $btn = $container.querySelector('.import-btn');
    // 在 Bangumi 上 非日语的条目跳过
    if (site === 'bangumi' && !isJpMovie(subjectItem)) {
        return subjectItem;
    }
    let subjectId = '';
    // 使用 bangumi data
    if (site === 'bangumi') {
        subjectId = getBangumiSubjectId(subjectItem.name, subjectItem.greyName);
    }
    if (!subjectId) {
        try {
            await randomSleep(1000, 400);
            const result = await siteUtils.checkSubjectExist({
                name: subjectItem.name,
                releaseDate: subjectItem.releaseDate,
            });
            if (result && result.url) {
                subjectId = siteUtils.getSubjectId(result.url);
                subjectItem.syncSubject = result;
            }
        }
        catch (error) {
            console.error(error);
        }
    }
    if (subjectId) {
        clearLogInfo($container);
        const nameStr = `<span style="color:tomato">《${subjectItem.name}》</span>`;
        insertLogInfo($btn, `更新收藏 ${nameStr} 中...`);
        await siteUtils.updateInterest(subjectId, Object.assign(Object.assign({ interest: typeIdDict$1[type].id }, subjectItem.collectInfo), { rating: subjectItem.collectInfo.score || '' }));
        subjectItem.syncStatus = '成功';
        await randomSleep(2000, 1000);
        insertLogInfo($btn, `更新收藏 ${nameStr} 成功`);
    }
    return subjectItem;
}
if (location.href.match(/bgm.tv|bangumi.tv|chii.in/)) {
    init('bangumi');
}
if (location.href.match(/movie.douban.com/)) {
    init('douban');
}
if (location.href.match(/search\.douban\.com\/movie\/subject_search/)) {
    if (window.top !== window.self) {
        sendSearchResults();
    }
}