利用 TMDB 接口把 Sonarr 中的标题替换成其他语言标题
// ==UserScript== // @name Sonarr-Title-i18n // @name:zh Sonarr 标题国际化 // @description 利用 TMDB 接口把 Sonarr 中的标题替换成其他语言标题 // @namespace https://github.com/LuckyPuppy514 // @version 1.0.4 // @homepage https://github.com/LuckyPuppy514/Sonarr-Title-i18n // @author LuckyPuppy514 // @copyright 2022, Grant LuckyPuppy514 (https://github.com/LuckyPuppy514) // @license MIT // @icon https://github.rn.lckp.top/LuckyPuppy514/dashboard-icons/master/png/sonarr.png // @include *://*sonarr* // @include *://*:8989/* // @run-at document-end // @require https://unpkg.com/[email protected]/dist/jquery.min.js // @grant GM_setValue // @grant GM_getValue // @grant GM_deleteValue // @grant GM_listValues // ==/UserScript== 'use strict'; // 默认语言代码 const DEFAULT_LANGUAGE_CODE = "zh-CN"; // GM_setValue key const KEY_TMDB_API_KEY = "KEY_TMDB_API_KEY"; const KEY_LANGUAGE_CODE = "KEY_LANGUAGE_CODE"; const KEY_ERROR_MESSAGE = "KEY_ERROR_MESSAGE"; const KEY_TITLE_PREFIX = "TITLE_"; const KEY_TVDBID_PREFIX = "TVDBID_"; const KEY_OVERVIEW_PREFIX = "OVERVIEW_"; // className const RIGHT_HEADERF_CLASS_NAME = "PageHeader-right-e8LU4"; const POSTER_TITLE_CLASS_NAME = "SeriesIndexPoster-title-rhAQh"; const OVERVIEW_TITLE_CLASS_NAME = "SeriesIndexOverview-title-LQthD SeriesIndexOverview-link-ltHLM Link-link-RInnp Link-link-RInnp Link-to-kylTi"; const DETAILS_TITLE_CLASS_NAME = "SeriesDetails-title-pJv1g"; const CALENDAR_TITLE_CLASS_NAME = "CalendarEvent-seriesTitle-QSWzp"; const CALENDAR_TITLE_AGENDA_CLASS_NAME = "AgendaEvent-seriesTitle-uBPt0"; const DETAILS_OVERVIEW_CLASS_NAME = "SeriesDetails-overview-cQJdA"; const SERIES_TITLE_CLASS_NAME = "Link-link-RInnp Link-to-kylTi"; // url path const DETAILS_TITLE_PATH = "/series/"; const CALENDAR_TITLE_PATH = "/calendar"; const SERIES_TITLE_PATH = "/serieseditor, /seasonpass, /queue, /history, /blocklist, /missing, /cutoffunmet"; // element id const i18n_BUTTON_ID = "i18n-button"; const SETTING_HIDDEN_DIV_ID = "setting-hidden-div"; const SETTING_SHOW_DIV_ID = "setting-show-div"; const CLOSE_BUTTON_ID = "close-button"; const SAVE_BUTTON_ID = "save-button"; const CLEAR_CACHE_BUTTON_ID = "clear-cache-button"; const TMDB_API_KEY_INPUT_ID = "tmdb-api-key"; const LANGUAGE_CODE_INPUT_ID = "language-code"; const ERROR_MESSAGE_TEXTAREA_ID = "error-message"; // css const CSS = ` #i18n-button { width: 25px; height: 25px; margin-top: 17px; margin-left: -15px; margin-right: 17px; border: 0px; border-radius: 50%; background: rgba(255,255,255, 0); background-repeat: no-repeat; cursor: pointer; z-index: 999 } #i18n-button-svg { width: 23px; height: 23px; } #setting-div { display: flex; justify-content: center; } #setting-hidden-div { width: 100%; height: 100%; position: fixed; top: 0; left: 0; background-color: #000000; opacity: 0.3; display: none; } #setting-show-div { width: 500px; height: 260px; background-color: rgba(64, 68, 84, 0.9); display: none; flex-direction: column; border-radius: 5px; align-items: center; padding-top: 40px; box-sizing: border-box; position: absolute; top: 200px; } #close-button { position: absolute; top: 7px; right: 7px; width: 28px; height: 28px; border-radius: 50%; background-size: cover; background-image: url(https://cdn.jsdelivr.net/gh/LuckyPuppy514/pic-bed/common/icons8-close-48.png); background-repeat: no-repeat; background-color: rgba(91, 137, 254, 0); color: rgba(255, 255, 255, 0); font-weight: normal; } #close-button:hover { background-color: rgba(255, 255, 255, 0.5); cursor: pointer; } #setting-show-div input { width: 280px; height: 25px; border-radius: 5px; border: none; outline: none; padding-left: 5px; background-color: rgba(0, 0, 0, 1); color: rgba(255, 255, 255, 1); } #setting-show-div input::-webkit-input-placeholder { color: rgb(255, 255, 255); opacity: 0.4; } #setting-show-div input:first-child { margin-top: 5px; margin-bottom: 5px; } #save-button { cursor: pointer; width: 300px; height: 30px; border-radius: 5px; border: none; outline: none; margin-left: 5px; padding-left: 5px; background-color: rgba(0, 255, 0, 0.8); color: rgba(255, 255, 255, 1); } #clear-cache-button:hover { background-color: rgba(255, 255, 255, 0.5); cursor: pointer; } #clear-cache-button { position: absolute; bottom: 8px; right: 8px; width: 30px; height: 30px; border-radius: 50%; background-size: cover; background-image: url(https://cdn.jsdelivr.net/gh/LuckyPuppy514/pic-bed/common/icons8-broom-64.png); background-repeat: no-repeat; background-color: rgba(91, 137, 254, 0); color: rgba(255, 255, 255, 0); font-weight: normal; } strong:hover:after { position: absolute; left: 30px; top: -25px; padding: 0px; border: 1px solid rgb(255, 255, 255); background-color: rgba(0,0,0,0.8); border-radius: 3px; color: rgba(255, 255, 255, 1); content: attr(data-tips); text-align: center; z-index: 2; width: 90px; height: 30px; } #error-message { width: 280px; height: 50px; border-radius: 5px; border: none; outline: none; padding-left: 5px; margin-top: 5px; margin-bottom: 5px; background-color: rgba(0, 0, 0, 1); color: rgba(255, 255, 255, 1); } #setting-table { width: 420px; height: 100px; border: none; } ` // html const i18n_BUTTON = ` <svg id="i18n-button-svg" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48px" height="48px"><path fill="#CFD8DC" d="M15,13h25c1.104,0,2,0.896,2,2v25c0,1.104-0.896,2-2,2H26L15,13z"/><path fill="#546E7A" d="M26.832,34.854l-0.916-1.776l0.889-0.459c0.061-0.031,6.101-3.208,9.043-9.104l0.446-0.895l1.79,0.893l-0.447,0.895c-3.241,6.496-9.645,9.85-9.916,9.989L26.832,34.854z"/><path fill="#546E7A" d="M38.019 34l-.87-.49c-.207-.116-5.092-2.901-8.496-7.667l1.627-1.162c3.139 4.394 7.805 7.061 7.851 7.087L39 32.26 38.019 34zM26 22H40V24H26z"/><path fill="#546E7A" d="M32 20H34V24H32z"/><path fill="#2196F3" d="M33,35H8c-1.104,0-2-0.896-2-2V8c0-1.104,0.896-2,2-2h14L33,35z"/><path fill="#3F51B5" d="M26 42L23 35 33 35z"/><path fill="#FFF" d="M19.172,24h-4.36l-1.008,3H11l4.764-13h2.444L23,27h-2.805L19.172,24z M15.444,22h3.101l-1.559-4.714L15.444,22z"/></svg> ` const SETTING_DIV = ` <div id="setting-div"> <div id="setting-hidden-div"></div> <div id="setting-show-div"> <strong id="close-button" data-tips="Close"></strong> <table id="setting-table"> <tr> <td>🔑 TMDB API Key</td> <td><input type="text" id="tmdb-api-key" placeholder="Please input TMDB API Key"></td> </tr> <tr> <td>✨ Language Code</td> <td><input type="text" id="language-code" placeholder="Please input Language Code"></td> </tr> <tr> <td>😰 Error Message</td> <td><textarea type="text" id="error-message" readonly></textarea></td> </tr> </table> <button type="button" id="save-button">Save</button> <a href="https://www.themoviedb.org/settings/api" target="_blank" style="text-decoration: none; color: rgba(0, 255, 0, 0.8); margin-top: 10px">🔑 Get TMDB API Key 🔑<a> <strong id="clear-cache-button" data-tips="Clear Cache"></strong> </div> </div> ` // 添加按钮和设置组件 function addi18nButtonAndSettingDiv() { // 添加 CSS var css = document.createElement("style"); css.innerHTML = CSS.trim(); document.head.appendChild(css); // 添加 i18n 按钮 var i18nButton = document.createElement("button"); i18nButton.id = i18n_BUTTON_ID; i18nButton.innerHTML = i18n_BUTTON.trim(); // 延时等待右导航栏加载 setTimeout(function () { let rightHeader = document.getElementsByClassName(RIGHT_HEADERF_CLASS_NAME)[0]; if (rightHeader) { rightHeader.appendChild(i18nButton); } }, 1000); // 添加设置组件 var div = document.createElement("div"); div.innerHTML = SETTING_DIV.trim(); document.body.appendChild(div); // 添加事件 var closeButton = document.getElementById(CLOSE_BUTTON_ID); var saveButton = document.getElementById(SAVE_BUTTON_ID); var clearCacheButton = document.getElementById(CLEAR_CACHE_BUTTON_ID); var tmdbApiKeyInput = document.getElementById(TMDB_API_KEY_INPUT_ID); var languageCodeInput = document.getElementById(LANGUAGE_CODE_INPUT_ID); var errorMessageTextarea = document.getElementById(ERROR_MESSAGE_TEXTAREA_ID); // 打开设置界面 i18nButton.onclick = function () { let tmdbApiKey = GM_getValue(KEY_TMDB_API_KEY); let languageCode = GM_getValue(KEY_LANGUAGE_CODE); if (tmdbApiKey) { tmdbApiKeyInput.value = tmdbApiKey; } if (languageCode) { languageCodeInput.value = languageCode; } else { languageCodeInput.value = DEFAULT_LANGUAGE_CODE; } document.getElementById(SETTING_SHOW_DIV_ID).style.display = "flex"; document.getElementById(SETTING_HIDDEN_DIV_ID).style.display = "block"; }; // 关闭设置界面 closeButton.onclick = function () { let settingShowDiv = document.getElementById(SETTING_SHOW_DIV_ID); let settingHiddenDiv = document.getElementById(SETTING_HIDDEN_DIV_ID); settingShowDiv.style.display = 'none'; settingHiddenDiv.style.display = 'none'; settingShowDiv.style.top = '200px'; settingShowDiv.style.left = ''; } // 保存设置 saveButton.onclick = function () { GM_setValue(KEY_TMDB_API_KEY, tmdbApiKeyInput.value); GM_setValue(KEY_LANGUAGE_CODE, languageCodeInput.value); clearCache(); initSeriesData(); closeButton.click(); } // 清除缓存 clearCacheButton.onclick = function () { clearCache(); Toast("Clear Cache Success", 2500); } } // 清除缓存 function clearCache() { let keys = GM_listValues(); for (let key of keys) { if (key.indexOf(KEY_TITLE_PREFIX) != -1 || key.indexOf(KEY_OVERVIEW_PREFIX) != -1 || key == KEY_ERROR_MESSAGE) { GM_deleteValue(key); } } document.getElementById(ERROR_MESSAGE_TEXTAREA_ID).value = ""; } // 保存错误信息 function saveErrorMessage(errorMessage) { var errorMessageTextarea = document.getElementById(ERROR_MESSAGE_TEXTAREA_ID); errorMessageTextarea.value = errorMessage; GM_setValue(KEY_ERROR_MESSAGE, errorMessage); } // 初始化数据 function initSeriesData() { var tmdbApiKey = GM_getValue(KEY_TMDB_API_KEY); var languageCode = GM_getValue(KEY_LANGUAGE_CODE); if (!tmdbApiKey || !languageCode) { return; } // 获取 Sonarr apiKey $.ajax({ type: "GET", url: "/initialize.json", xhrFields: { withCredentials: true }, success: function (res) { let apiKey = res.apiKey; // 获取所有剧集的标题和 tvdbId $.ajax({ type: "GET", url: "/api/v3/series", xhrFields: { withCredentials: true }, headers: { "X-Api-Key": apiKey }, success: function (res) { for (let tv of res) { // 没有数据则请求 TMDB 接口 if (!GM_getValue(KEY_TITLE_PREFIX + tv.title)) { GM_setValue(KEY_TVDBID_PREFIX + tv.title, tv.tvdbId); translate(tv.title, false); } if (GM_getValue(KEY_ERROR_MESSAGE)) { break; } } }, error: function (err) { let errorMessage = "获取 Sonarr 剧集列表出错: " + JSON.stringify(err); saveErrorMessage(errorMessage) console.log(errorMessage); } }); }, error: function (err) { let errorMessage = "获取 Sonarr apiKey 出错: " + JSON.stringify(err); saveErrorMessage(errorMessage) console.log(errorMessage); } }); } // 显示消息 function Toast(msg, duration) { duration = isNaN(duration) ? 3000 : duration; var m = document.createElement('div'); m.innerHTML = msg; m.style.cssText = "max-width:60%;min-width: 150px;padding:0 14px;height: 40px;color: rgb(255, 255, 255);line-height: 40px;text-align: center;border-radius: 4px;position: fixed;top: 50%;left: 50%;transform: translate(-50%, -50%);z-index: 999999;background: rgba(0, 0, 0,.7);font-size: 16px;"; document.body.appendChild(m); setTimeout(function () { var d = 0.5; m.style.opacity = '0'; setTimeout(function () { document.body.removeChild(m) }, d * 1000); }, duration); } // 通过 TMDB 接口获取对应语言的标题和简介 function translate(title, saveOverview) { var tmdbApiKey = GM_getValue(KEY_TMDB_API_KEY); var languageCode = GM_getValue(KEY_LANGUAGE_CODE); if (!tmdbApiKey || !languageCode || GM_getValue(KEY_ERROR_MESSAGE)) { return; } GM_setValue(KEY_TITLE_PREFIX + title, title); let tvdbId = GM_getValue(KEY_TVDBID_PREFIX + title); $.ajax({ type: "GET", url: "https://api.themoviedb.org/3/find/" + tvdbId + "?api_key=" + tmdbApiKey + "&language=" + languageCode + "&external_source=tvdb_id", success: function (res) { if (res && res.tv_r###lts && res.tv_r###lts.length > 0) { GM_setValue(KEY_TITLE_PREFIX + title, res.tv_r###lts[0].name); if (saveOverview) { let overview = res.tv_r###lts[0].overview; if (overview) { GM_setValue(KEY_OVERVIEW_PREFIX + title, overview); let overviewDiv = document.getElementsByClassName(DETAILS_OVERVIEW_CLASS_NAME)[0]; if (overviewDiv) { overviewDiv.innerHTML = '<div style="overflow: hidden;"><span>' + overview + '</span></div>'; } } } } }, error: function (err) { let errorMessage = "请求 TMDB 接口出错: " + JSON.stringify(err); saveErrorMessage(errorMessage) console.log(errorMessage); } }); } // 替换网页中的标题 function replaceTitle() { var tmdbApiKey = GM_getValue(KEY_TMDB_API_KEY); var languageCode = GM_getValue(KEY_LANGUAGE_CODE); if (!tmdbApiKey || !languageCode) { return; } var url = window.location.href; var className = POSTER_TITLE_CLASS_NAME; var endPath = url.substring(url.lastIndexOf("/")); var saveOverview = false; if (endPath != "/") { if (CALENDAR_TITLE_PATH.indexOf(endPath) != -1) { className = CALENDAR_TITLE_CLASS_NAME; } else if (SERIES_TITLE_PATH.indexOf(endPath) != -1) { className = SERIES_TITLE_CLASS_NAME; } else if (url.indexOf(DETAILS_TITLE_PATH) != -1) { className = DETAILS_TITLE_CLASS_NAME; saveOverview = true; } } var titleDivs = document.getElementsByClassName(className); var errorMessageTextarea = document.getElementById(ERROR_MESSAGE_TEXTAREA_ID); if (className == POSTER_TITLE_CLASS_NAME && (!titleDivs || titleDivs.length == 0)) { titleDivs = document.getElementsByClassName(SERIES_TITLE_CLASS_NAME); if (!titleDivs || titleDivs.length == 0) { titleDivs = document.getElementsByClassName(OVERVIEW_TITLE_CLASS_NAME); } } if (className == CALENDAR_TITLE_CLASS_NAME && (!titleDivs || titleDivs.length == 0)) { titleDivs = document.getElementsByClassName(CALENDAR_TITLE_AGENDA_CLASS_NAME); } for (let titleDiv of titleDivs) { let title = titleDiv.innerHTML; if (!title || title.indexOf("<") != -1) { continue; } var translatedTitle = GM_getValue(KEY_TITLE_PREFIX + title); if (translatedTitle && title != translatedTitle) { titleDiv.innerHTML = translatedTitle; if (saveOverview) { // 详情页重新加载最新数据 translate(title, saveOverview); } } }; } initSeriesData(); addi18nButtonAndSettingDiv(); setInterval(replaceTitle, 500);