Sonarr-Title-i18n

利用 TMDB 接口把 Sonarr 中的标题替换成其他语言标题

< Opiniones de Sonarr-Title-i18n

Puntuación: Bueno; el script funciona tal y como promete

§
Publicado: 28/06/2025

为什么安装后右上角没有这个按钮

§
Publicado: 29/06/2025

// ==UserScript==
// @name Sonarr-Title-i18n
// @name:zh Sonarr 标题国际化
// @description 利用 TMDB 接口把 Sonarr 中的标题替换成其他语言标题
// @namespace https://github.com/LuckyPuppy514
// @version 1.0.5
// @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
// @downloadURL https://update.greasyforks.org/scripts/450716/Sonarr-Title-i18n.user.js
// @updateURL https://update.greasyforks.org/scripts/450716/Sonarr-Title-i18n.meta.js
// ==/UserScript==

'use strict';

// 添加调试信息
console.log('Sonarr-Title-i18n script loaded on:', window.location.href);

// 默认语言代码
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 = `

`
const SETTING_DIV = `


🔑 TMDB API Key
✨ Language Code
😰 Error Message
Save
🔑 Get TMDB API Key 🔑



`

// 添加按钮和设置组件
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();

// 改进的按钮添加逻辑,支持多种可能的右导航栏类名
function addButtonToHeader() {
// 尝试多种可能的右导航栏选择器
const possibleSelectors = [
'PageHeader-right-e8LU4', // 原始类名
'[class*="PageHeader-right"]', // 包含 PageHeader-right 的类名
'.page-header .toolbar', // 通用工具栏
'header .toolbar', // 头部工具栏
'[class*="toolbar"]', // 任何包含 toolbar 的类名
'nav .navbar-nav', // 导航栏
'.navbar-right', // 右侧导航栏
'header > div:last-child' // 头部最后一个div
];

let rightHeader = null;

// 尝试每个选择器
for (let selector of possibleSelectors) {
if (selector.startsWith('.') || selector.startsWith('[')) {
rightHeader = document.querySelector(selector);
} else {
rightHeader = document.getElementsByClassName(selector)[0];
}

if (rightHeader) {
console.log('Found header element using selector:', selector);
break;
}
}

// 如果还是找不到,尝试在页面顶部添加
if (!rightHeader) {
console.log('Could not find header element, trying to add to body');
rightHeader = document.body;
// 为按钮添加固定定位样式
i18nButton.style.position = 'fixed';
i18nButton.style.top = '10px';
i18nButton.style.right = '10px';
i18nButton.style.zIndex = '9999';
}

if (rightHeader) {
rightHeader.appendChild(i18nButton);
console.log('i18n button added successfully');
} else {
console.log('Failed to add i18n button');
}
}

// 延时等待页面加载,并尝试多次添加按钮
let attempts = 0;
const maxAttempts = 10;

function tryAddButton() {
attempts++;
if (document.getElementById(i18n_BUTTON_ID)) {
console.log('i18n button already exists');
return;
}

addButtonToHeader();

if (!document.getElementById(i18n_BUTTON_ID) && attempts < maxAttempts) {
setTimeout(tryAddButton, 1000);
}
}

setTimeout(tryAddButton, 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_results && res.tv_results.length > 0) {
GM_setValue(KEY_TITLE_PREFIX + title, res.tv_results[0].name);
if (saveOverview) {
let overview = res.tv_results[0].overview;
if (overview) {
GM_setValue(KEY_OVERVIEW_PREFIX + title, overview);
let overviewDiv = document.getElementsByClassName(DETAILS_OVERVIEW_CLASS_NAME)[0];
if (overviewDiv) {
overviewDiv.innerHTML = '

';
}
}
}
}
},
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);
}
}
};
}

// 主初始化函数
function initScript() {
console.log('Initializing Sonarr-Title-i18n script...');

// 确保jQuery已加载
if (typeof $ === 'undefined') {
console.log('jQuery not loaded yet, retrying...');
setTimeout(initScript, 500);
return;
}

console.log('jQuery loaded, starting script initialization');

try {
initSeriesData();
addi18nButtonAndSettingDiv();
setInterval(replaceTitle, 500);
console.log('Script initialization completed successfully');
} catch (error) {
console.error('Error during script initialization:', error);
}
}

// 启动脚本
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initScript);
} else {
initScript();
}

我尝试改了下代码,现在可以显示了

Publicar respuesta

Inicia sesión para responder.

长期地址
遇到问题?请前往 GitHub 提 Issues。