您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
备份/还原最爱演员和影片
// ==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) })()