Jellyfin备份/还原最爱演员和影片

备份/还原最爱演员和影片

// ==UserScript==
// @name         Jellyfin备份/还原最爱演员和影片
// @namespace    http://tampermonkey.net/
// @version      0.0.2
// @description  备份/还原最爱演员和影片
// @author       Squirtle
// @license      MIT
// @match        *://*/web/index.html*
// @match        *://*/*/web/index.html*
// @icon         https://www.google.com/s2/favicons?sz=64&domain=soundiiz.com
// @grant        GM_registerMenuCommand
// @grant        GM_xmlhttpRequest
// @grant        GM_addStyle
// @grant        GM_getValue
// @grant        GM_setValue

// ==/UserScript==

;(function () {
    'use strict'
    let settings, ITEMS_URL, POST_FAV_ITEMS_URL, FAV_PERSONS_URL
    initSettings()
    const modal = createModal()
    const uploadPersonBtn = modal.querySelector('#jv-uploadPersonBtn')
    const uploadVideoBtn = modal.querySelector('#jv-uploadVideoBtn')
    const fileInput = modal.querySelector('#jv-fileInput')
    const submitBtn = modal.querySelector('#jv-submit')
    const resetBtn = modal.querySelector('#jv-reset')
    const downloadPersonBtn = modal.querySelector('#jv-downloadPersonBtn')
    const downloadVideoBtn = modal.querySelector('#jv-downloadVideoBtn')
    const form = modal.querySelector('#jv-form')
    const closeIcon = modal.querySelector('.jv-close-icon')
    const logText = modal.querySelector('#jv-logText')
    const logBtn = modal.querySelector('#jv-logBtn')
    const logListElement = modal.querySelector('#jv-logList')
    const logList = []
    let showLog = false

    function initSettings() {
        settings = GM_getValue('settings', {})
        ITEMS_URL = `${settings.serverUrl}/Users/${settings.userId}/Items`
        POST_FAV_ITEMS_URL = `${settings.serverUrl}/Users/${settings.userId}/FavoriteItems`
        FAV_PERSONS_URL = `${settings.serverUrl}/Persons`
    }

    function addQuery(base, obj) {
        if (!obj) {
            return base
        }
        const query = Object.entries(obj)
            .filter(([_, value]) => value != null)
            .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(value)}`)
            .join('&')
        if (!query) {
            return base
        }
        return base.endsWith('?') ? base + query : `${base}?${query}`
    }

    async function request(url, method = 'GET') {
        return new Promise((resolve, reject) => {
            GM_xmlhttpRequest({
                method,
                url,
                headers: { 'X-Emby-Token': settings.apiKey },
                onload(response) {
                    if (response.status === 200) {
                        try {
                            resolve(JSON.parse(response.responseText))
                        } catch (error) {
                            reject(error)
                        }
                    } else {
                        reject(response)
                    }
                },
                onerror(error) {
                    reject(error)
                }
            })
        }).catch(error => log(`${error.statusText}: 检查参数是否设置正确`))
    }

    async function commonFetch(url, params) {
        const finalParams = {
            startIndex: 0,
            fields: 'PrimaryImageAspectRatio,SortName,PrimaryImageAspectRatio',
            imageTypeLimit: 1,
            includeItemTypes: 'Movie,Person',
            recursive: true,
            sortBy: 'SortName',
            sortOrder: 'Ascending',
            api_key: settings.apiKey,
            ...params
        }
        const result = await request(addQuery(url, finalParams))
        return result.Items
    }

    async function fetchItems(params) {
        return commonFetch(ITEMS_URL, params)
    }

    async function fetchPersons(params) {
        return commonFetch(FAV_PERSONS_URL, params)
    }

    function getFileName(type) {
        const date = new Date()
        const names = ['backup', type, date.getFullYear(), date.getMonth() + 1, date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds()]
        return `${names.join('-')}.json`
    }

    function downloadFile(content, fileName) {
        const blob = new Blob([content], { type: 'application/json' })
        const url = URL.createObjectURL(blob)
        const a = document.createElement('a')
        a.href = url
        a.download = fileName
        a.click()
        URL.revokeObjectURL(url)
    }

    function getListNames(list) {
        return list.map(item => item.Name)
    }

    async function downloadPersons() {
        const persons = await fetchPersons({ userId: settings.userId, isFavorite: true })
        const content = JSON.stringify(getListNames(persons), null, 2)
        const fileName = getFileName('persons')
        downloadFile(content, fileName)
    }
    async function downloadVideos() {
        const videos = await fetchItems({ isFavorite: true })
        const content = JSON.stringify(getListNames(videos), null, 2)
        const fileName = getFileName('videos')
        downloadFile(content, fileName)
    }

    function buildFormItems() {
        const formKeys = ['apiKey', 'userId', 'serverUrl']
        return formKeys
            .map(key => {
                return `
                <div class='jv-form-item'>
                    <label for='${key}'>${key}: </lable>
                    <input type='text' name='${key}' value='${settings[key] || ''}' />
                </div>
            `
            })
            .join('\n')
    }

    function createModal() {
        const modal = document.createElement('div')
        modal.id = 'jv-modal'
        modal.innerHTML = `
            <div class='jv-close-icon'>X</div>
            <div class='jv-section'>
                <h2>设置参数</h2>
                <form id='jv-form'>
                    ${buildFormItems()}
                </form>
                <div class='jv-btn-group'>
                    <button id='jv-submit'>确定</button>
                    <button id='jv-reset'>重置</button>
                </div>
            </div>

            <div class='jv-section'>
                <h2>下载文件</h2>
                <div class='jv-btn-group'>
                    <button id='jv-downloadPersonBtn'>下载演员</button>
                    <button id='jv-downloadVideoBtn'>下载影片</button>
                </div>
            </div>

            <div class='jv-section'>
                <h2>上传文件</h2>
                <input type='file' id='jv-fileInput' />
                <div class='jv-btn-group'>
                    <button id='jv-uploadPersonBtn'>上传演员</button>
                    <button id='jv-uploadVideoBtn'>上传影片</button>
                </div>
            </div>
            <div>
                <div id='jv-logText'></div>
                <button id='jv-logBtn'>查看完整日志</button>
                <div id='jv-logList'><div>
            </div>

       `
        document.body.appendChild(modal)
        return modal
    }

    function showModal() {
        modal.style.display = 'block'
    }
    function hideModal() {
        modal.style.display = 'none'
    }

    async function getUploadContent() {
        return new Promise((resolve, reject) => {
            const file = fileInput.files[0]
            if (!file) {
                return reject('请先上传一个文件')
            }
            const reader = new FileReader()
            reader.onload = async function (e) {
                let data
                try {
                    const fileContent = e.target.result
                    data = JSON.parse(fileContent)
                } catch (error) {
                    console.error(error)
                    reject('请上传合法的json文件')
                }
                if (data?.length > 0) {
                    resolve(data)
                } else {
                    reject('上传的文件为空,请重新上传')
                }
            }
            reader.readAsText(file)
        }).catch(alert)
    }

    async function uploadPersons() {
        const persons = await getUploadContent()
        for (const person of persons) {
            const items = await fetchPersons({ limit: 5, searchTerm: person, includeItemTypes: 'Person', IncludePeople: true, userId: settings.userId })
            const targetItem = items.find(item => item.Name === person)
            if (items.length === 0 || !targetItem) {
                log(`未搜索到: ${person}`)
            } else {
                const favoriteURL = `${POST_FAV_ITEMS_URL}/${items[0].Id}`
                try {
                    await request(favoriteURL, 'POST')
                    log(`成功:${person}`)
                } catch (error) {
                    log(`失败: ${person}`)
                }
            }
        }
    }

    async function uploadVideos() {
        const videos = await getUploadContent()
        for (const video of videos) {
            const items = await fetchItems({ limit: 5, searchTerm: video.slice(0, 35), includeItemTypes: 'Movie' })
            if (items.length === 1) {
                const favoriteURL = `${POST_FAV_ITEMS_URL}/${items[0].Id}`
                try {
                    await request(favoriteURL, 'POST')
                    log(`成功: ${video}`)
                } catch (error) {
                    log(`失败: ${video}`)
                }
            } else {
                log(`搜索出${items.length}个结果: ${video}\n \t${getListNames(items).join('\n\t')} `)
            }
        }
    }

    function handleSubmit() {
        const formData = new FormData(form)
        const data = Object.fromEntries(formData)
        GM_setValue('settings', data)
        initSettings()
        log('设置成功')
    }

    function handleReset() {
        initSettings()
        form.innerHTML = buildFormItems()
    }

    function log(text) {
        logText.textContent = text
        logList.push(text)
        console.log(text)
    }

    function toggleLog() {
        if (showLog) {
            showLog = false
            logListElement.style.display = 'none'
        } else {
            showLog = true
            logListElement.innerHTML = logList.join('<br />')
            logListElement.style.display = 'block'
        }
    }

    function registerEventListeners() {
        uploadPersonBtn.addEventListener('click', uploadPersons)
        uploadVideoBtn.addEventListener('click', uploadVideos)
        submitBtn.addEventListener('click', handleSubmit)
        resetBtn.addEventListener('click', handleReset)
        downloadPersonBtn.addEventListener('click', downloadPersons)
        downloadVideoBtn.addEventListener('click', downloadVideos)
        closeIcon.addEventListener('click', hideModal)
        logBtn.addEventListener('click', toggleLog)
    }

    function registerMenuListeners() {
        GM_registerMenuCommand('打开设置', showModal)
    }

    function start() {
        registerEventListeners()
        registerMenuListeners()
    }

    start()

    const css = `
        #jv-modal {
            position: fixed;
            left: 50%;
            top: 50%;
            transform: translate(-50%, -50%);
            background: #fff;
            z-index: 1100;
            overflow: auto;
            display: none;
            padding: 0 50px 50px;
        }

        .jv-section {
            width: 500px;
            margin-bottom: 50px;
        }

        .jv-form-item {
            margin-bottom: 5px;
        }

        .jv-btn-group {
            margin-top: 20px;
        }

        .jv-btn-group button {
            margin-right: 5px;
            cursor: pointer;
        }

        .jv-close-icon {
            position: absolute;
            right: 10px;
            top: 10px;
            cursor: pointer;
            font-size: 20px;
            font-weight: bold;
        }

        #jv-logText {
            color: red;
            margin-bottom: 10px;
        }

        #jv-logList {
            margin-top: 10px;
            max-height: 400px;
            overflow: auto;
        }

    `

    GM_addStyle(css)
})()