// ==UserScript== // @name MyAnimeList(MAL) — русское описание аниме // @namespace https://github.com/njko39/MAL-RU-Enhancements // @match https://myanimelist.net/anime/* // @match https://myanimelist.net/anime.php?id=* // @icon https://t3.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&url=http://myanimelist.net&size=64 // @version 1.3 // @author njko39 // @copyright 2025, njko39 (https://github.com/njko39/MAL-RU-Enhancements) // @description Добавляет русское описание аниме с Шикимори или GitHub на страницу аниме на MAL // @downloadURL https://github.com/njko39/MAL-RU-Enhancements/raw/refs/heads/main/MAL-RU-Enhancements.js // @homepageURL https://github.com/njko39/MAL-RU-Enhancements // @updateURL https://github.com/njko39/MAL-RU-Enhancements/raw/refs/heads/main/MAL-RU-Enhancements.js // @license MIT // @grant none // ==/UserScript== // ==OpenUserJS== // @author mwehehe // ==/OpenUserJS== /* Эта часть скрипта заменяет описание и название через API. */ // Function to create the tab HTML function createTabHtml(newDescription, originalDescription, showSource, shikimoriUrl) { const sourceHtml = showSource ? ` <div class="source"> Источник: <a href="https://shikimori.one${shikimoriUrl}" target="_blank">Шикимори</a> </div> ` : ''; return ` <div class="block"> <div class="tab-button-container"> <button class="tablinks" onclick="openTab(event, 'newDescription')">RUS</button> <button class="tablinks" onclick="openTab(event, 'originalDescription')">ENG</button> </div> <div id="newDescription" class="tabcontent"> ${newDescription || ''} ${sourceHtml} </div> <div id="originalDescription" class="tabcontent" style="display:none;"> ${originalDescription} </div> </div> `; } // Function to replace special tags with links function processContentLinks(description) { if (typeof description !== 'string' || description === '') { return ''; } // Process [character] tags const characterRegex = /\[character=(\d+)\](.*?)\[\/character\]/g; let processedDescription = description.replace(characterRegex, (_, id, text) => { return `<a href="https://myanimelist.net/character/${id}" target="_blank">${text}</a>`; }); // Process [anime] tags const animeRegex = /\[anime=(\d+)\](.*?)\[\/anime\]/g; processedDescription = processedDescription.replace(animeRegex, (_, id, text) => { return `<a href="https://myanimelist.net/anime/${id}" target="_blank">${text}</a>`; }); // Process [person] tags const personRegex = /\[person=(\d+)\](.*?)\[\/person\]/g; processedDescription = processedDescription.replace(personRegex, (_, id, text) => { return `<a href="https://myanimelist.net/people/${id}" target="_blank">${text}</a>`; }); return processedDescription; } // Function to fetch from GitHub async function fetchFromGithub(url) { try { const response = await fetch(url); if (!response.ok) { return null; } const text = await response.text(); return text.trim(); } catch (error) { console.log('Error fetching from GitHub:', error); return null; } } // Function to fetch title from GitHub async function fetchGithubTitle(animeId) { const titleUrl = `https://raw.githubusercontent.com/njko39/MAL-RU-Enhancements/main/assets/anime/${animeId}/title.md`; return await fetchFromGithub(titleUrl); } // Function to fetch description from GitHub async function fetchGithubDescription(animeId) { const descriptionUrl = `https://raw.githubusercontent.com/njko39/MAL-RU-Enhancements/main/assets/anime/${animeId}/description.md`; return await fetchFromGithub(descriptionUrl); } // Main function async function replaceDescriptionTextAndTitle() { const currentPageUrl = window.location.href; const urlParts = currentPageUrl.split('/'); // Find the index of "anime" in the URL parts const animeIndex = urlParts.indexOf('anime'); let animeId = null; if (animeIndex === -1) { // "anime" not found in path, check if the URL contains "/anime.php?" const parser = document.createElement('a'); parser.href = currentPageUrl; // Check if the URL path includes "/anime.php" if (parser.pathname.includes('/anime.php')) { const searchParams = new URLSearchParams(parser.search); if (searchParams.has('id')) { animeId = searchParams.get('id'); // Verify if it's a valid number if (isNaN(animeId)) { console.log('Anime ID not found'); return; } } else { console.log('Anime section not found in URL'); return; } } else { console.log('Anime section not found in URL'); return; } } else { // "anime" found in path, extract ID as before const animeIdPart = urlParts[animeIndex + 1]; animeId = animeIdPart.replace(/\D/g, ''); if (!animeId) { console.log('Anime ID not found'); return; } } console.log('Anime ID:', animeId); const shikimoriApiUrl = `https://shikimori.one/api/animes/${animeId}`; try { const response = await fetch(shikimoriApiUrl); // Fetch title and description separately let shikimoriTitle = null; let shikimoriDescription = null; let shikimoriUrl = ''; if (response.ok) { const dataResponse = await response.json(); shikimoriTitle = dataResponse.russian; shikimoriDescription = dataResponse.description; shikimoriUrl = dataResponse.url || `/${animeId}`; // Determine if the description came from Shikimori (not null) const cameFromShikimoriDescription = shikimoriDescription !== null; // Try GitHub if needed const githubTitle = shikimoriTitle ? null : await fetchGithubTitle(animeId); const githubDescription = cameFromShikimoriDescription ? null : await fetchGithubDescription(animeId); // Combine data const data = { russian: shikimoriTitle || githubTitle, description: shikimoriDescription || githubDescription, url: shikimoriUrl }; // Pass the source visibility flag (only when Shikimori provided a description) processAndDisplayData(data, animeId, cameFromShikimoriDescription, shikimoriUrl); } else { // If Shikimori API returns 404, try GitHub for title and description const githubTitle = await fetchGithubTitle(animeId); const githubDescription = await fetchGithubDescription(animeId); // Combine data const data = { russian: githubTitle, description: githubDescription, url: `/${animeId}` }; // Pass the source visibility flag (false since we didn't use Shikimori) processAndDisplayData(data, animeId, false, `/${animeId}`); } } catch (error) { console.log('Error:', error); const descElement = document.querySelector('p[itemprop="description"]'); if (descElement) { descElement.textContent = 'Error fetching description: ' + error; } } } // Function to add specific CSS when title is replaced function addTitleCSS() { // Remove existing styles with the same ID to prevent duplication const existingStyle = document.getElementById('titleCSS'); if (existingStyle) { existingStyle.remove(); } // Create new style element const style = document.createElement('style'); style.id = 'titleCSS'; style.textContent = ` .h1.edit-info div.h1-title h1 { display: inline-block !important; } `; document.head.appendChild(style); } // Helper function to process and display data function processAndDisplayData(data, animeId, cameFromShikimori, shikimoriUrl) { let description = data.description; const russianName = data.russian; const shikimoriPageUrl = shikimoriUrl || `/${animeId}`; // Handle title update separately if (russianName) { const titleElement = document.querySelector('h1.title-name.h1_bold_none strong'); const englishTitleElement = document.querySelector('p.title-english.title-inherit'); if (titleElement) { const originalTitle = titleElement.textContent; titleElement.textContent = russianName; if (englishTitleElement) { englishTitleElement.textContent = originalTitle; } else { const newEnglishTitleElement = document.createElement('p'); newEnglishTitleElement.className = 'title-english title-inherit'; newEnglishTitleElement.textContent = originalTitle; titleElement.parentNode.appendChild(newEnglishTitleElement); } } // Add the CSS after title replacement addTitleCSS(); } // Process description let processedDescription = ''; if (typeof description === 'string' && description !== '') { processedDescription = processContentLinks(description); } else { processedDescription = `У аниме пока что нет описания на русском. Вы можете предложить его на <a href="https://shikimori.one${shikimoriPageUrl}" target="_blank">Шикимори</a> или <a href="https://github.com/njko39/MAL-RU-Enhancements/" target="_blank">GitHub странице скрипта</a>, и оно появится здесь.`; } // Add tab CSS const style = document.createElement('style'); style.textContent = ` .block { padding-bottom: 1.5em; } .tab-button-container { margin-bottom: 1.5em; } .source { text-align: right; font-size: 0.8em; color: #666; margin-top: 1em; } .source a { color: #999; text-decoration: none; } .source a:hover { text-decoration: underline; } `; document.documentElement.appendChild(style); // Add tab JavaScript const script = document.createElement('script'); script.textContent = ` function openTab(evt, tabName) { const tabcontent = document.getElementsByClassName("tabcontent"); for (let i = 0; i < tabcontent.length; i++) { tabcontent[i].style.display = "none"; } const tablinks = document.getElementsByClassName("tablinks"); for (let i = 0; i < tablinks.length; i++) { tablinks[i].className = tablinks[i].className.replace(" active", ""); } document.getElementById(tabName).style.display = "block"; evt.currentTarget.className += " active"; } `; document.body.appendChild(script); // Create tab for descriptions const descElement = document.querySelector('p[itemprop="description"]'); if (!descElement) { console.log('Description element not found'); return; } const originalDescription = descElement.innerHTML; // Create tab HTML with appropriate source visibility const tabHtml = createTabHtml(processedDescription, originalDescription, cameFromShikimori, shikimoriPageUrl); descElement.outerHTML = tabHtml; document.getElementsByClassName("tablinks")[0].click(); } // Run the function when the page loads window.addEventListener('load', replaceDescriptionTextAndTitle); /* В этой части статичный перевод, который не зависит от API. */ function replaceText(node) { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent .replace(/Favorites/g, "В избранном") .replace(/Members/g, "В списках") .replace(/Popularity/g, "Популярность") .replace(/Ranked/g, "Топ") .replace(/Score/g, "Оценка") .replace(/Rating/g, "Рейтинг") .replace(/Duration/g, "Длительность") .replace(/Source/g, "Первоисточник") .replace(/Studios/g, "Студии") .replace(/Licensors/g, "Лицензировано") .replace(/Producers/g, "Продюсеры") .replace(/Broadcast/g, "Показ") .replace(/Premiered/g, "Премьера") .replace(/Aired/g, "Дата") .replace(/Status/g, "Статус") .replace(/Episodes/g, "Эпизоды") .replace(/Type/g, "Тип") .replace(/Genres/g, "Жанры") .replace(/Themes/g, "Темы"); } else if (node.nodeType === Node.ELEMENT_NODE) { for (let child of node.childNodes) { replaceText(child); } } } // Process .spaceit_pad elements for original translations const spaceitElements = document.querySelectorAll('.spaceit_pad'); spaceitElements.forEach(element => { replaceText(element); }); // Process #menu_left div for "Anime" and "Manga" translations function replaceMenuText(node) { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent .replace(/Anime/g, "Аниме") .replace(/Manga/g, "Манга"); } else if (node.nodeType === Node.ELEMENT_NODE) { for (let child of node.childNodes) { replaceMenuText(child); } } } const menuLeft = document.querySelector('#menu_left'); if (menuLeft) { replaceMenuText(menuLeft); } // Process .stats-block divs for specific translations function replaceStatsText(node) { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent .replace(/Spring/g, "Весна") .replace(/Winter/g, "Зима") .replace(/Fall/g, "Осень") .replace(/Summer/g, "Лето") .replace(/Ranked/g, "Топ") .replace(/Popularity/g, "Популярность") .replace(/Members/g, "В списках"); } else if (node.nodeType === Node.ELEMENT_NODE) { for (let child of node.childNodes) { replaceStatsText(child); } } } const statsBlocks = document.querySelectorAll('.stats-block.po-r.clearfix'); statsBlocks.forEach(block => { replaceStatsText(block); }); // Update the .fl-l.score div's attributes function updateScoreAttributes() { const scoreDivs = document.querySelectorAll('.fl-l.score'); scoreDivs.forEach(div => { div.setAttribute('data-title', 'Рейтинг'); const userValue = div.getAttribute('data-user'); if (userValue) { if (userValue === '- user') { div.setAttribute('data-user', 'Нет оценок'); } else { div.setAttribute('data-user', userValue.replace(/users$/, 'оценок')); } } }); } updateScoreAttributes(); // Process .user-status-block div for specific translations and adjust select width function replaceUserStatusText(node) { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent .replace(/Appalling/g, "Провал") .replace(/Horrible/g, "Ужасно") .replace(/Very Bad/g, "Очень плохо") .replace(/Bad/g, "Плохо") .replace(/Average/g, "Нормально") .replace(/Fine/g, "Неплохо") .replace(/Very Good/g, "Очень хорошо") .replace(/Good/g, "Хорошо") .replace(/Great/g, "Отлично") .replace(/Masterpiece/g, "Шедевр") .replace(/Select/g, "Оценить") .replace(/Dropped/g, "Брошено") .replace(/On-Hold/g, "Отложено") .replace(/Watching/g, "Смотрю") .replace(/Plan to Watch/g, "Запланировано") .replace(/Completed/g, "Просмотрено") .replace(/Episodes:/g, "Эпизоды:"); } else if (node.nodeType === Node.ELEMENT_NODE) { for (let child of node.childNodes) { replaceUserStatusText(child); } const selectElement = node.querySelector('select#myinfo_status'); if (selectElement) { selectElement.style.width = '111px'; // Adjusted width for better text fit } } } const userStatusBlocks = document.querySelectorAll( '.user-status-block.js-user-status-block.fn-grey6.clearfix.al.mt8.po-r' ); userStatusBlocks.forEach(block => { replaceUserStatusText(block); }); // New function to handle translations in #addtolist element function replaceAddToListText(node) { if (node.nodeType === Node.TEXT_NODE) { node.textContent = node.textContent .replace(/Appalling/g, "Провал") .replace(/Horrible/g, "Ужасно") .replace(/Very Bad/g, "Очень плохо") .replace(/Bad/g, "Плохо") .replace(/Average/g, "Нормально") .replace(/Fine/g, "Неплохо") .replace(/Very Good/g, "Очень хорошо") .replace(/Good/g, "Хорошо") .replace(/Great/g, "Отлично") .replace(/Masterpiece/g, "Шедевр") .replace(/Select/g, "Нет") .replace(/Dropped/g, "Брошено") .replace(/On-Hold/g, "Отложено") .replace(/Watching/g, "Смотрю") .replace(/Plan to Watch/g, "Запланировано") .replace(/Completed/g, "Просмотрено") .replace(/Status:/g, "Статус:") .replace(/Eps Seen:/g, "Прогресс:") .replace(/Your Score:/g, "Оценка:") .replace(/Edit Details/g, "Редактировать"); } else if (node.nodeType === Node.ELEMENT_NODE) { for (let child of node.childNodes) { replaceAddToListText(child); } // Target the specific input button and change its value const updateButton = node.querySelector('input[name="myinfo_submit"]'); if (updateButton && updateButton.classList.contains('js-anime-update-button')) { updateButton.value = "Обновить"; } } } // Process #addtolist div const addToListElement = document.getElementById('addtolist'); if (addToListElement) { replaceAddToListText(addToListElement); } // New functionality to replace text in .notice_open_public function replaceNoticeText() { const noticeSpans = document.querySelectorAll('.notice_open_public'); noticeSpans.forEach(span => { // Replace all text content in the span span.textContent = '* Ваш список публичный по умолчанию.'; }); } // Run the new functionality replaceNoticeText();