Greasy Fork is available in English.
С 1 мая 2024 года обещали отключить old.myshows.me. Под ручку с нейросетями попытался починить нужные мне места.
// ==UserScript== // @name old.myshows.me // @namespace http://tampermonkey.net/ // @version 2025-v35 // @description С 1 мая 2024 года обещали отключить old.myshows.me. Под ручку с нейросетями попытался починить нужные мне места. // @ Желательно использовать вместе с внешним видом от другого энтузиаста: https://userstyles.world/style/15722/old-myshows-me (инструкцию ищите там же) // @author SanBest93 // @match https://*.myshows.me/* // @icon https://www.google.com/s2/favicons?sz=64&domain=myshows.me // @grant none // @license MIT // ==/UserScript== (function () { 'use strict'; /** * Класс для управления настройками скрипта с сохранением в `localStorage`. */ class Settings { /** Добавлены настройки на страницу, собсно, «Настройки» (myshows.me/profile/edit/) * Сохраняются в локальное хранение `localStorage`. Удаляются при очистке кэша. * Вписывайте ниже значение `value` жёстко, если это критично */ /** * Инициализирует настройки с значениями из `localStorage`. */ constructor() { /** * Объект с настройками скрипта, где ключ — идентификатор настройки, а значение — объект с её параметрами. * @type {Object.<string, {value: boolean|string|null, labelText: string, applied: boolean, textContent: string, tooltipText: string}>} */ this.v = { ModifyShowTitleLink: { // Ссылка только в русском названии шоу на странице `myshows.me/profile/` value: getItem('ModifyShowTitleLink') ?? false, labelText: '«Мои сериалы»: оставить ссылку только в русском названии', applied: false, textContent: '', tooltipText: '' }, ModifyShowSeasonMeta: { // Замена "5 эпизодов с e1" на "5 эпизодов с e01" на странице `myshows.me/profile/` value: getItem('ModifyShowSeasonMeta') ?? false, labelText: '«Мои сериалы»: замена "N эпизодов с e1" на "N эпизодов с e01"', applied: false, textContent: '', tooltipText: '' }, ModifyProfileNumbers: { // Вывод полных чисел в шапке профиля value: getItem('ModifyProfileNumbers') ?? false, labelText: '«Профиль»: вывод полных чисел в шапке', applied: false, textContent: '', tooltipText: '' }, OriginalTitleIsNeeded: { // Всегда выводить оригинальные названия на странице `myshows.me/profile/next/` value: getItem('OriginalTitleIsNeeded') ?? false, labelText: '«Календарь»: всегда выводить оригинальные названия', applied: false, textContent: '', tooltipText: `Перезагрузите страницу, если настройка не применилась.\nТакое бывает, если открыть /profile/next/ после /profile/edit/` }, ModifyS01E01: { // Замена "1 x 1" на "s01e01" (и ещё по мелочи) на странице `myshows.me/profile/next/` value: getItem('ModifyS01E01') ?? false, labelText: '«Календарь»: замена "1 x 1" на "s01e01" (и ещё по мелочи)', applied: false, textContent: '', tooltipText: '' }, s01e01Postfix: { // Текст после s01e01 на странице `myshows.me/profile/next/`. Мне так удобнее на торрентах искать value: getItem('s01e01Postfix') ?? false, labelText: '«Календарь»: текст после s01e01', applied: false, textContent: getItem('s01e01PostfixValue'), tooltipText: '' } }; /** * Объект с дополнительными флагами состояния скрипта. * @type {Object.<string, boolean>} */ this.o = { watchSoonElementsModified: false, }; } /** * Устанавливает значение свойства настройки или флага выполнения. * @param {string} id — Идентификатор настройки или флага. * @param {any} value — Новое значение свойства. * @param {string} key — Ключ свойства (например, 'value' или 'applied'). * @returns {void} — Функция не возвращает значений. */ setProperty(id, value, key) { if (id in this.v) { this.v[id][key] = value; } else if (id in this.o) { this.o[id] = value; } } /** * Получает значение свойства настройки или флага выполнения. * @param {string} id — Идентификатор настройки или флага. * @param {string} key — Ключ свойства (например, 'value' или 'applied'). * @returns {any} — Значение свойства или `undefined`, если идентификатор не найден. */ getProperty(id, key) { if (id in this.v) { return this.v[id][key]; } else if (id in this.o) { return this.o[id]; } } /** * Устанавливает значение настройки. * @param {string} id — Идентификатор настройки. * @param {boolean|string|null} value — Новое значение настройки. * @returns {void} — Функция не возвращает значений. */ setValue(id, value) { this.setProperty(id, value, 'value'); } /** * Получает значение настройки. * @param {string} id — Идентификатор настройки. * @returns {boolean|string|null} — Значение настройки или `undefined`, если настройка не найдена. */ getValue(id) { return this.getProperty(id, 'value'); } /** * Устанавливает флаг применения настройки. * @param {string} id — Идентификатор настройки или флага. * @param {boolean} value — Новое значение флага. * @returns {void} — Функция не возвращает значений. */ setApplied(id, value) { this.setProperty(id, value, 'applied'); } /** * Получает флаг применения настройки. * @param {string} id - Идентификатор настройки или флага. * @returns {boolean|undefined} - Значение флага или `undefined`, если идентификатор не найден. */ getApplied(id) { return this.getProperty(id, 'applied'); } /** * Сбрасывает все флаги применения настроек и дополнительных состояний. * @returns {void} — Функция не возвращает значений. */ resetAllFlags() { Object.keys(this.v).forEach(id => this.setApplied(id, false)); Object.keys(this.o).forEach(id => this.setApplied(id, false)); } /** * Возвращает данные для создания чекбокса на основе настройки. * @param {string} id — Идентификатор настройки. * @returns {{labelText: string, textContent: string, tooltipText: string}|null} - Данные для чекбокса или `null`, если настройка не найдена. */ getCheckboxData(id) { return this.v[id] ? { labelText: this.v[id].labelText, textContent: this.v[id].textContent, tooltipText: this.v[id].tooltipText } : null; } } let nuxtMap = new Map(); // Сюда будем складывать соответствие showId — titleOriginal let STYLE; let lastUrl = location.href; let observers = new Map(); // Глобальный объект для хранения наблюдателей let pendingApiRequests = new Set(); let apiRequestsInProgress = new Map(); // ID — Promise let lastRetryTime = 0; const userName = document.querySelector('div.HeaderLogin__username')?.textContent; // Запоминаем userName const rowHeight = '30px'; const months = Array.from({ length: 12 }, (_, i) => { return new Intl.DateTimeFormat('ru', { month: 'long' }).format(new Date(2000, i)); }); // Создаем массив названий месяцев для русской локали const defaultTimeout = 200; // Таймаут по умолчанию const _Settings = new Settings(); // Создаём экземпляр настроек const RETRY_DELAY = 5000; /** * Класс для хранения данных о шоу и его эпизодах. * Используется для структурирования информации о сериалах на странице `/profile/next/` * перед сортировкой и генерацией новых элементов DOM. */ class ShowData { /** * Создаёт экземпляр данных о шоу. * @param {number} index — Индекс группы (обычно соответствует дню в календаре). * @param {string} showTitle — Название шоу (русское или оригинальное, в зависимости от настроек). * @param {string} episodeInfo — Информация об эпизоде в формате, например, "s01e01". * @param {string} innerHTML — HTML-код для вставки в DOM, содержащий ссылки и форматированный текст. */ constructor(index, showTitle, episodeInfo, innerHTML) { /** * Индекс группы, используется для сортировки по дням. * @type {number} */ this.index = index; /** * Название шоу (может быть русским или оригинальным). * @type {string} */ this.showTitle = showTitle; /** * Информация об эпизоде (например, "s01e01"). * @type {string} */ this.episodeInfo = episodeInfo; /** * HTML-код для отображения шоу и эпизода в DOM. * @type {string} */ this.innerHTML = innerHTML; } } /** * Пытается разобрать неполный JSON, добавляя различные комбинации закрывающих скобок рекурсивно. * Ограничивает глубину рекурсии для предотвращения бесконечного цикла. * @param {string} jsonString — Неполная строка JSON для разбора. * @param {number} [maxDepth=10] — (необязательный) Максимальная глубина рекурсии (по умолчанию 10). * @param {string} [closingBrackets=''] — (необязательный) Текущая комбинация закрывающих скобок, добавляемая к строке (по умолчанию пустая строка). * @param {Array<string>} [possibleClosers=['"', ']', '}']] — (необязательный) Массив возможных закрывающих символов для попыток (по умолчанию ['"', ']', '}']). * @returns {Object|null} — Разобранный объект JSON или `null`, если разбор не удался. */ function parseIncompleteJSON(jsonString, maxDepth = 10, closingBrackets = '', possibleClosers = ['"', ']', '}']) { // Base case: prevent infinite recursion if (maxDepth <= 0) { return null; } // Try to parse the current string with the current closing brackets try { const attemptedJSON = jsonString + closingBrackets; const parsedData = JSON.parse(attemptedJSON); console.log("[old.myshows.me] __NUXT_DATA__ успешно разобран с закрывающими скобками: ", closingBrackets || 'null'); return parsedData; } catch (error) { // If parsing fails, try adding each possible closing bracket for (const closer of possibleClosers) { const r###lt = parseIncompleteJSON( jsonString, maxDepth - 1, closingBrackets + closer, possibleClosers ); if (r###lt !== null) { return r###lt; } } // If all combinations fail at this level, return null return null; } } /** * Получает содержимое элемента `__NUXT_DATA__` и преобразует его в объект JavaScript. * @returns {Object|null} — Возвращает объект JavaScript, если парсинг успешен, или `null` в случае ошибки. */ function parseScriptData() { const scriptElement = document.getElementById('__NUXT_DATA__'); if (!scriptElement) return null; const parsedData = parseIncompleteJSON(scriptElement?.textContent); if (parsedData) { return parsedData; } else { console.error("[old.myshows.me] Не удалось разобрать JSON с любой комбинацией закрывающих скобок"); return null; } } /** * Возвращает значение настройки из `localStorage` по id * (`localStorage` — место в браузере пользователя, * в котором сайты могут сохранять разные данные) * @param {string} id — Имя настройки (см. `_Settings`) * @returns {boolean|string} — Значение настройки. На текущий момент: булево, текст */ function getItem(id) { const value = localStorage.getItem(id); return value === 'true' ? true : value === 'false' ? false : value; } /** * Возвращает id шоу из pathname элемента * @param {string} pathname — Значение pathname элемента * @returns {boolean|string} — Значение настройки. На текущий момент: булево, текст */ function getShowIdFromPathname(pathname) { return pathname.split("/").slice(-2)[0]; } /** * Возвращает номер месяца по русскому тексту * @param {string} monthName — Полное русское название месяца * @returns {number|null} — Номер месяца или null, если такой текст не найден */ function getMonthNumber(monthName) { const index = months.findIndex(month => month.toLowerCase() === monthName.toLowerCase()); return index !== -1 ? index + 1 : null; } /** * Проверяет, изменился ли URL страницы, и сбрасывает флаги в `_Settings`, если изменение произошло. * Обновляет значение `lastUrl` текущим адресом. * @returns {void} — Функция не возвращает значений. */ function checkUrlChange() { const currentUrl = location.href; if (currentUrl !== lastUrl) { lastUrl = currentUrl; // Запоминаем текущий адрес _Settings.resetAllFlags(); // Сбрасываем все флаги } } /** * Сравнивает две ссылки, игнорируя завершающие слэши. * @param {string} link1 - Первая ссылка для сравнения. * @param {string} link2 - Вторая ссылка для сравнения. * @returns {boolean} - `true`, если ссылки идентичны, иначе `false`. */ function linksAreSimilar(link1, link2) { return link1.replace(/\/$/, '') === link2.replace(/\/$/, ''); } /** * Ищет ключ в массиве объектов и возвращает значение первого найденного ключа. * Если ключ не найден, возвращает `undefined`. * @param {Array} data — Массив объектов, в котором производится поиск. * @param {string} key — Ключ, который нужно найти в объектах. * @param {number} [N=data.length] — (необязательный) Максимальное количество элементов для поиска (по умолчанию равно длине массива). * @returns {*} — Значение первого найденного ключа или `undefined`, если ключ не найден. */ function findKeyInArray(data, key, N = data.length) { for (let i = 0; i < Math.min(N, data.length); i += 1) { // Проверяем, является ли элемент объектом и содержит ли указанный ключ if (typeof data[i] === 'object' && !!data[i] && key in data[i]) { // Если да, выводим значение ключа return data[i][key]; } } return undefined; // Возвращаем undefined, если ключ не найден } /** * Ищет значение в данных `__NUXT_DATA__` по указанному пути. * Поддерживает поиск в массивах и объектах, включая структуры с `Reactive` и `ShallowReactive`. * Если путь не найден, возвращает `undefined`. * @param {Array|Object} data — Данные, в которых производится поиск. * @param {string} path — Путь к значению в формате 'key1.key2.key3'. * @returns {*} — Найденное значение или `undefined`, если путь не найден. */ function findValueByPath(data, path) { // Разделяем путь на компоненты let keys = path.split('.'); let index = 0; let currentData = data[index]; // Проходимся по каждому компоненту пути for (let key of keys) { if (Array.isArray(currentData) && currentData.length > 0) { // Если текущие данные являются массивом, то // на 2024.06.18 структура такая, что вид ['Reactive', число]; // 2025.03.11: ещё может начаться с ['ShallowReactive', число] let indexReactive = Math.min( Math.max(currentData[0].indexOf('Reactive'), 0), Math.max(currentData[0].indexOf('ShallowReactive'), 0) ) + 1; // Это число if (!isNaN(indexReactive) && indexReactive > 0 && indexReactive < currentData.length) { index = currentData[indexReactive]; currentData = data[index]; } else { return undefined; // Если индекс некорректный или за пределами массива } } if (typeof currentData === 'object' && currentData !== null) { // Если текущие данные являются объектом if (key in currentData) { index = currentData[key]; currentData = data[index]; } else { return undefined; // Если ключ не найден в текущем объекте } } else { return undefined; // Если текущие данные не являются ни объектом, ни массивом } } return currentData; } /** * Создаёт флажок (чекбокс) с меткой и добавляет его в указанный родительский элемент. * Поддерживает добавление текстового поля и значка с подсказкой. * Состояние флажка сохраняется в `localStorage`. * @param {HTMLElement} parent — Родительский элемент, в который будет добавлен флажок. * @param {string} id — Уникальный идентификатор флажка. * @param {string} labelText — Текст метки флажка. * @param {boolean} [textContent] — (необязательный) Если заполнено, добавляет текстовое поле рядом с флажком. * @param {string} [tooltipText] — (необязательный) Текст подсказки для значка вопроса. * @returns {HTMLElement} — Созданный элемент метки (label). */ function createCheckbox(parent, id, labelText, textContent, tooltipText) { // Создаём элемент метки (label) const label = document.createElement('label'); label.className = 'oldMyshowsSettings-label'; // Устанавливаем класс метки // Создаём элемент флажка (input) const checkbox = document.createElement('input'); checkbox.className = 'oldMyshowsSettings-checkbox'; checkbox.type = 'checkbox'; checkbox.id = id; checkbox.checked = !!_Settings.getValue(id); // Значение было получено из `localStorage` при создании `_Settings` checkbox.addEventListener('change', function() { // Добавляем обработчик события изменения состояния флажка localStorage.setItem(id, this.checked); // Сохраняем состояние флажка в `localStorage` _Settings.setValue(id, this.checked); // Сохраняем состояние флажка в `_Settings` }); label.appendChild(checkbox); // Добавляем флажок в метку // Создаём элемент span для текстового содержимого метки const span = document.createElement('span'); span.innerText = labelText; label.appendChild(span); // Добавляем span в метку // Если требуется добавить дополнительное текстовое содержимое if (textContent === null || !!textContent) { const idValue = `${id}Value`; // Генерируем id для дополнительного текстового поля const checkboxTextContent = document.createElement('input'); checkboxTextContent.className = 'oldMyshowsSettings-checkbox-textContent'; checkboxTextContent.type = 'text'; checkboxTextContent.id = idValue; checkboxTextContent.value = textContent ?? ''; // Значение было получено из `localStorage` при создании `_Settings` checkboxTextContent.addEventListener('change', function() { // Добавляем обработчик события изменения значения текстового поля localStorage.setItem(idValue, this.value); // Сохраняем значение текстового поля в `localStorage` _Settings.v[id].textContent = this.value; // Сохраняем состояние текстового поля в `_Settings` }); label.appendChild(checkboxTextContent); // Добавляем текстовое поле в метку } // Добавляем значок вопроса с подсказкой if (!!tooltipText) { const tooltipIcon = document.createElement('span'); tooltipIcon.className = 'tooltip-icon'; tooltipIcon.textContent = '?'; tooltipIcon.title = tooltipText; tooltipIcon.style.marginLeft = '5px'; tooltipIcon.style.color = '#007bff'; tooltipIcon.style.cursor = 'pointer'; tooltipIcon.style.textDecoration = 'underline'; label.appendChild(tooltipIcon); } parent.appendChild(label); // Добавляем метку в указанный родительский элемент return label; // Возвращаем созданную метку } /** * Создает группу настроек с флажками (checkboxes) на странице. * @returns {void} — Функция не возвращает значений. */ function createCheckboxes() { const groupTitleTextContent = 'Настройки скрипта [old.myshows.me]'; // Проверяем, существует ли уже наша группа. // Если уже существует, прекращаем выполнение функции if (document.querySelector('.oldMyshowsSettings')) return; const parentElement = document.querySelector('.mb-5'); // Элемент, в который мы хотим добавить новую группу const thelastChild = parentElement?.lastChild; // Его последний дочерний элемент if (!parentElement || !thelastChild) return; // Создаём группу let sectionAccordion = document.createElement('div'); sectionAccordion.classList.add('SectionAccordion'); // Создаём заголовок для группы let sectionAccordionTitle = document.createElement('div'); sectionAccordionTitle.textContent = groupTitleTextContent; sectionAccordionTitle.classList.add('SectionAccordion-title'); // Создаём контейнер для флажков let checkboxesContainer = document.createElement('div'); checkboxesContainer.classList.add('oldMyshowsSettings'); // Создаём контейнер для стиля let checkboxesStyle = document.createElement('div'); checkboxesStyle.classList.add('oldMyshowsSettings-style'); // Проходим по всем настройкам и создаём чекбоксы Object.keys(_Settings.v).forEach(id => { const checkboxData = _Settings.getCheckboxData(id); if (checkboxData) { createCheckbox( checkboxesStyle, id, checkboxData.labelText, checkboxData.textContent, checkboxData.tooltipText ); } }); // Добавляем флажки в контейнер checkboxesContainer.appendChild(checkboxesStyle); checkboxesContainer.classList.toggle('hidden'); // Добавляем заголовок и контейнер с флажками в сворачиваемую группу sectionAccordion.appendChild(sectionAccordionTitle); sectionAccordion.appendChild(checkboxesContainer); // Добавляем обработчик события клика на заголовок для переключения видимости контейнера с флажками sectionAccordionTitle.addEventListener('click', function() { checkboxesContainer.classList.toggle('hidden'); }); // Вставляем новую группу перед последним дочерним элементом parentElement.insertBefore(sectionAccordion, thelastChild); } /** * Заменяет название шоу на оригинальное, используя данные из карты `nuxtMap`. * Если название не найдено, пытается обновить карту из DOM или запросить данные через API. * @param {Object} show — Объект шоу, содержащий путь (pathname). * @param {boolean} [retry=true] — (необязательный) Если `true`, разрешает повторные попытки поиска названия. * @returns {string} — Оригинальное название шоу или пустая строка, если название не найдено. */ function fixTitle(show, retry = true) { if (!show || !show.pathname) return ''; const showId = getShowIdFromPathname(show.pathname); if (!showId) return ''; // Проверяем, есть ли уже название в карте let title = nuxtMap.get(showId) || ''; if (title !== '') return title; // Если названия нет, но разрешены повторные попытки if (retry && Date.now() - lastRetryTime > RETRY_DELAY) { lastRetryTime = Date.now(); // Проверяем, не запрошен ли уже этот ID if (!pendingApiRequests.has(showId)) { console.log('[old.myshows.me] Название не найдено для showId: ', showId, 'Повторная попытка...'); // Сначала попробуем обновить map из DOM console.log('[old.myshows.me] Вызов createNuxtMap из fixTitle()'); createNuxtMap(() => { const retryTitle = nuxtMap.get(showId) || ''; // Если title всё ещё не найден, запрашиваем через API if (retryTitle === '') { fetchTitleFromAPI(showId); } else { console.log('[old.myshows.me] Название найдено в DOM для showId: ', showId, 'Название: ', retryTitle); } return retryTitle; }, 1); } } return nuxtMap.get(showId) || ''; } /** * Добавляет префикс (например, 'S' или 'E') к числу, представляющему сезон или серию. * Если число меньше 10, добавляет ведущий ноль. * @param {string} text — Текст, содержащий число. * @param {string} prefix — Префикс, который нужно добавить (например, 'S' для сезона или 'E' для серии). * @returns {string} — Строка с добавленным префиксом и числом. Если текст не является числом, возвращает исходный текст. */ function addPrefix(text, prefix) { const num = parseInt(text, 10); return isNaN(num) ? text : `${prefix}${num < 10 ? '0' : ''}${num}`; } /** * Преобразует строку с информацией об эпизоде (например, "1 x 1 - название эпизода") * в объект с полями `se` (сезон и серия в формате 's01e01') и `name` (название эпизода). * Если включена настройка `ModifyS01E01`, форматирует сезон и серию. * @param {string} episodeText — Строка с информацией об эпизоде. * @returns {{ se: string, name: string }} — Объект с двумя полями: * — `se`: строка в формате sXXeYY (например, "s01e01"). * — `name`: название эпизода (например, "название эпизода"). */ function fixEpisodeInfo(episodeText) { let se = ''; let name = ''; if (!episodeText) return { se, name }; // Если входная строка пустая или отсутствует, возвращаем пустой объект const s01e01Postfix = _Settings.getValue('s01e01Postfix') && _Settings.getCheckboxData('s01e01Postfix').textContent || ''; if (_Settings.getValue('ModifyS01E01')) { // Если включена настройка ModifyS01E01, преобразуем строку в формат s01e01 const parts = episodeText.split(' '); // Разделяем строку по пробелам if (parts.length >= 3) { const season = parts[0]; // Номер сезона (первый элемент) const episode = parts[2]; // Номер эпизода (третий элемент) const s = addPrefix(season, 's'); // Добавляем префикс "s" к номеру сезона const e = `${addPrefix(episode, 'e')} ${s01e01Postfix}`.trim(); // Добавляем префикс "e" и, если нужно, постфикс se = `${s}${e}`; // Собираем полную строку name = parts.slice(3).join(' ').replace(/^-\s*/, '').trim(); // Извлекаем название эпизода, удаляя начальные дефисы и пробелы } } else { // Если настройка ModifyS01E01 выключена, используем другой формат разбора строки const parts = episodeText.split(' - '); // Разделяем строку по " - " if (parts.length >= 2) { se = parts[0].trim(); // Первая часть до " - " — это "s01e01" или аналог name = parts.slice(1).join(' - ').trim(); // Остальная часть — название эпизода } else { se = episodeText.trim(); // Если разделитель " - " отсутствует, вся строка считается частью "se" } } return { se, name }; // Возвращаем объект с результатами } /** * Сортирует элементы эпизодов внутри каждого блока `.WatchSoon-item` по числу дня. * Удаляет избыточные ссылки на даты для повторяющихся дней. * Ничего не делает, если есть активные запросы к API. * @returns {void} — Функция не возвращает значений. */ function sortWatchSoonItems() { // PS: 2025.03.11: https://disk.yandex.ru/i/hEe_3lzeFPFlxg — пруф. // Отключил полностью все скрипты и стили. Сортировка кривая! Бесит, правим // Прекращаем выполнение, если есть активные API-запросы. // Это предотвращает рекурсию, вызванную текущей реализацией setupObserver if (apiRequestsInProgress.size > 0) return; let watchSoonItems = document.querySelectorAll('.WatchSoon-item'); // Получаем все элементы-контейнеры для месяцев из списка WatchSoon if (!watchSoonItems) return; let changes = 0; // Проходим по каждому контейнеру месяца. watchSoonItems.forEach(watchSoonItem => { let rows = watchSoonItem.querySelectorAll('.Row:not(.WatchSoon-header):not(.FIXED)'); // Выбираем все строки (элементы .Row), исключая заголовок с месяцем if (rows.length === 0) return; let rowsArray = Array.from(rows); let dayText = ''; // Добавляем атрибут data-day к каждой строке, содержащий номер дня rowsArray.forEach(row => { const dayElement = row.querySelector('.WatchSoon-left'); // Находим элемент с датой if (dayElement) { dayText = dayElement.textContent.replace(/вчера/gi, '-1').replace(/yesterday/gi, '-1').replace(/вчора/gi, '-1') .replace(/сегодня/gi, '0').replace(/today/gi, '0').replace(/сьогодні/gi, '0'); const dayNumber = parseInt(dayText.split(' ')[0].trim(), 10); // Извлекаем номер дня if (!isNaN(dayNumber)) { row.dataset.day = dayNumber; // Сохраняем номер дня в dataset if (!row.classList.contains('FIXED')) { row.classList.add('FIXED'); } // Добавляем FIXED только если его нет } } }); // Сортируем строки по возрастанию номера дня. rowsArray.sort((a, b) => { return (a.dataset.day || 0) - (b.dataset.day || 0); // Если data-day отсутствует, используем 0 }); // Очищаем контейнер с эпизодами и добавляем отсортированные строки обратно const container = watchSoonItem.querySelector('.WatchSoon-episodes'); if (container) { container.innerHTML = ''; // Очищаем контейнер rowsArray.forEach(row => { container.appendChild(row); // Добавляем строки в отсортированном порядке }); } // Удаляем лишние ссылки на даты, которые дублируются в соседних строках let curDay = 0; // Переменная для отслеживания текущего дня rows = watchSoonItem.querySelectorAll('.Row:not(.WatchSoon-header)'); // Обновляем список строк rowsArray = Array.from(rows); rowsArray.forEach(row => { if (curDay !== row?.dataset?.day) { curDay = row?.dataset?.day; // Обновляем текущий день, если он изменился } else { const redundantLink = row.querySelector('.router-link-exact-active'); if (redundantLink) redundantLink.innerHTML = ''; changes += 1; // Если день совпадает с предыдущим, удаляем ссылку на дату } }); }); if (changes !== 0) console.log(`[old.myshows.me] Пересортирован список эпизодов`); } /** * Исправляет текст, который стал занимать несколько строк после обновления сайта. * Сортирует элементы эпизодов по дням, названиям шоу и номерам эпизодов. * Создаёт новые элементы с исправленным текстом и скрывает оригинальные элементы. * Ничего не делает, если есть активные запросы к API. * @returns {void} — Функция не возвращает значений. */ function fixWatchSoonElements() { const changeIsNeeded = _Settings.getValue('OriginalTitleIsNeeded') || _Settings.getValue('ModifyS01E01') || _Settings.getValue('s01e01PostfixValue'); if (!changeIsNeeded) return; if (apiRequestsInProgress.size > 0) return; let ohCrapHereWeGoAgain = false; if (_Settings.getApplied('watchSoonElementsModified') && nuxtMap.size > 0) { const firstChild = document.querySelector('.OldMyShowsClass')?.firstChild; if (!!firstChild && firstChild.textContent === nuxtMap.get(getShowIdFromPathname(firstChild.pathname))) { return; } else { ohCrapHereWeGoAgain = true; } } const watchSoonElements = document.querySelectorAll('.WatchSoon__title-wrapper'); if (!watchSoonElements) return; // Проверяем, есть ли уже элементы OldMyShowsClass const existingCustomElements = document.querySelectorAll('.OldMyShowsClass'); if (!ohCrapHereWeGoAgain && !!watchSoonElements.length && existingCustomElements.length >= watchSoonElements.length) { _Settings.setApplied('watchSoonElementsModified', true); // Если элементы уже есть, считаем задачу выполненной return; } const showsData = []; // Массив объектов для сортировки данных о шоу и эпизодах let index = -1; // Индекс для сортировки по дням let prevWatchSoonLeft = ''; // Для проверки, что текст сменился let changes = 0; // Заполняем массив объектами на основе данных на странице watchSoonElements.forEach(element => { const showLink = element.querySelector('.WatchSoon-show'); const episodeLink = element.querySelector('.WatchSoon-episode'); if (!showLink || !episodeLink || !episodeLink.textContent.includes(' - ')) return; // Находим родительский элемент с классом ".WatchSoon-left" let parent = element; while (parent && !parent.querySelector('.WatchSoon-left')) { parent = parent.parentElement; } if (!parent) return; // Если не найден родительский элемент, выходим // Добавим признак группировки из правой колонки (да, я вижу, что в коде она называется left) const watchSoonLeft = parent.querySelector('.WatchSoon-left').textContent.trim(); if (watchSoonLeft !== prevWatchSoonLeft) { prevWatchSoonLeft = watchSoonLeft; index += 1; } let showTitle = _Settings.getValue('OriginalTitleIsNeeded') === true ? fixTitle(showLink) : showLink.textContent; showTitle = showTitle === '' ? showLink.textContent : showTitle; // Если ничего не получилось, то всё ещё оставим хоть какой-то текст const episodeInfo = fixEpisodeInfo(episodeLink.textContent); const innerHTML = `<a href="${showLink.href}" target="_blank">${showTitle}</a> <span> — ${episodeInfo.se} — </span> <a href="${episodeLink.href}" target="_blank">${episodeInfo.name}</a>`; // Добавляем данные в массив объектов showsData.push(new ShowData(index, showTitle, episodeInfo.se, innerHTML)); // Скрываем исходный элемент if (!element.classList.contains('hidden')) { element.classList.add('hidden'); } // Добавляем hidden только если его нет }); // Сортируем массив по index, затем по showText, затем по episodeInfo showsData.sort((a, b) => { if (a.index !== b.index) return a.index - b.index; if (a.showTitle !== b.showTitle) return a.showTitle.localeCompare(b.showTitle); return a.episodeInfo.localeCompare(b.episodeInfo); }); existingCustomElements.forEach(el => el.remove()); // Вставляем элементы на основе отсортированных данных showsData.forEach((data, index) => { const newElement = document.createElement('div'); newElement.innerHTML = data.innerHTML; newElement.classList.add('OldMyShowsClass'); // (описание классов см. в initStyle()) const parent = watchSoonElements[index]; if (parent) { parent.parentNode.insertBefore(newElement, parent.nextSibling); changes += 1; } }); // Меняем стили через CSS initStyle(); _Settings.setApplied('watchSoonElementsModified', changes !== 0); if (changes !== 0) console.log(`[old.myshows.me] Применены настройки для «Календарь»`); if (changes !== 0) updateRowsHeightsBasedOnOldMyShowsClass(); } /** * Загружает оригинальное название шоу из API по заданному идентификатору (showId). * Добавляет полученное название в карту `nuxtMap` и обновляет элементы на странице, если данные получены. * Обрабатывает ошибки сети с повторными попытками через 5 секунд до исчерпания лимита попыток. * @param {string} showId - Идентификатор шоу для запроса к API. * @param {string} [defaultText=''] - Текст по умолчанию, возвращаемый при ошибке. * @param {number} [retries=3] - Максимальное количество повторных попыток при ошибке сети. * @returns {Promise<string>} - Промис, возвращающий оригинальное название шоу или текст по умолчанию. */ function fetchTitleFromAPI(showId, defaultText = '', retries = 3) { // Проверяем, не выполняется ли уже запрос для этого showId if (pendingApiRequests.has(showId) || apiRequestsInProgress.has(showId)) { // Если запрос уже идёт, возвращаем существующий промис, чтобы избежать дублирования return apiRequestsInProgress.get(showId); } // Логируем начало запроса с указанием showId // console.log('[old.myshows.me] Запрос к API для showId:', showId); // Добавляем showId в список ожидающих запросов pendingApiRequests.add(showId); // Создаём промис для выполнения HTTP-запроса к API const fetchPromise = fetch('https://api.myshows.me/v2/rpc/', { method: 'POST', // Метод запроса — POST headers: { 'Content-Type': 'application/json', // Указываем, что отправляем JSON 'Accept': 'application/json' // Ожидаем JSON в ответе }, body: JSON.stringify({ "jsonrpc": "2.0", // Версия JSON-RPC протокола "method": "shows.GetById", // Метод API для получения данных о шоу "params": { "showId": Number(showId), // Преобразуем showId в число для API "withEpisodes": false // Не запрашиваем эпизоды, только основную информацию }, "id": 1 // Идентификатор запроса }) }) .then(response => { // Проверяем, успешен ли запрос if (!response.ok) { // Если статус не 200-299, выбрасываем ошибку с кодом статуса throw new Error(`API ответил статусом ${response.status}`); } // Преобразуем ответ в JSON return response.json(); }) .then(data => { // Извлекаем оригинальное название шоу из ответа или используем текст по умолчанию const titleOriginal = data?.r###lt?.titleOriginal || defaultText; // Логируем успешный результат с полученным названием // console.log('[old.myshows.me] Результат API для showId:', showId, 'Название:', titleOriginal); // Если название получено (не равно defaultText), сохраняем его в карту if (titleOriginal !== defaultText) { nuxtMap.set(showId, titleOriginal); // Если мы на странице календаря, обновляем элементы после небольшой задержки if (location.href.includes('myshows.me/profile/next/')) { // Очищаем предыдущий таймаут, чтобы избежать наложения clearTimeout(window.fixWatchSoonElementsTimeout); // Устанавливаем новый таймаут для обновления элементов window.fixWatchSoonElementsTimeout = setTimeout(() => { // Сбрасываем флаг применения изменений _Settings.setApplied('watchSoonElementsModified', false); // Применяем изменения к элементам календаря fixWatchSoonElements(); }, defaultTimeout); } } // Удаляем showId из списков ожидающих и выполняющихся запросов pendingApiRequests.delete(showId); apiRequestsInProgress.delete(showId); // Возвращаем полученное название return titleOriginal; }) .catch(error => { // Логируем ошибку с указанием showId и её описанием console.error('[old.myshows.me] Ошибка API для showId:', showId, error); // Удаляем showId из списков, так как запрос завершён (даже с ошибкой) pendingApiRequests.delete(showId); apiRequestsInProgress.delete(showId); // Если ошибка связана с сетью и остались попытки if (error.message.includes('Failed to fetch') && retries > 0) { // Логируем количество оставшихся попыток console.log('[old.myshows.me] Ошибка сети, осталось попыток:', retries); // Возвращаем новый промис для повторной попытки return new Promise(resolve => { // Ждём заданную задержку перед следующей попыткой setTimeout(() => { // Проверяем, не появилось ли название в карте за это время if (!nuxtMap.has(showId)) { // Рекурсивно вызываем функцию с уменьшенным числом попыток resolve(fetchTitleFromAPI(showId, defaultText, retries - 1)); } else { // Если название уже есть, возвращаем его resolve(nuxtMap.get(showId)); } }, RETRY_DELAY); }); } else if (error.message.includes('Failed to fetch')) { // Если попытки исчерпаны, выводим предупреждение console.warn('[old.myshows.me] Исчерпано количество попыток для showId:', showId); } // В случае окончательной ошибки возвращаем текст по умолчанию return defaultText; }); // Сохраняем промис в карту выполняющихся запросов apiRequestsInProgress.set(showId, fetchPromise); // Возвращаем промис вызывающей стороне return fetchPromise; } /** * Пытается загрузить недостающие данные о шоу из API для элементов с классом `.WatchSoon-show`. * Собирает уникальные идентификаторы шоу (showId) и инициирует запросы к API для тех, которых нет в карте `nuxtMap`. * @returns {void} — Функция не возвращает значений. */ function tryFetchingMissingShowsFromAPI() { const showLinks = document.querySelectorAll('.WatchSoon-show'); if (!showLinks.length) return; console.log('[old.myshows.me] Попытка загрузить недостающие шоу из API'); // Соберем все уникальные showId, чтобы не дублировать запросы const uniqueShowIds = new Set(); showLinks.forEach(showLink => { if (!showLink || !showLink.pathname) return; const showId = getShowIdFromPathname(showLink.pathname); if (!nuxtMap.has(showId) && !pendingApiRequests.has(showId) && !apiRequestsInProgress.has(showId) && !uniqueShowIds.has(showId)) { uniqueShowIds.add(showId); } }); // Теперь запросим данные для уникальных showId uniqueShowIds.forEach(showId => { fetchTitleFromAPI(showId); }); } /** * Создаёт карту `nuxtMap` с соответствием `showId` и `titleOriginal` на основе данных из `__NUXT_DATA__`. * Пытается извлечь данные из различных структур (`list`, `userShows`, `profileShows`) или запрашивает их через API, если данные недоступны. * Выполняет callback-функцию после завершения, если она передана. * @param {Function} [callback] — (необязательный) Функция, вызываемая после создания карты. * @param {number} [attempts=1] — (необязательный) Количество оставшихся попыток для повторного выполнения при отсутствии данных. * @returns {void} — Функция не возвращает значений. */ function createNuxtMap(callback, attempts = 1) { if (apiRequestsInProgress.size > 0) { console.log('[old.myshows.me] Ожидаем ответа от API...'); return; } console.log('[old.myshows.me] Создание карты nuxtMap, осталось попыток:', attempts, '; размер nuxtMap:', nuxtMap.size); if (attempts <= 0) { console.warn('[old.myshows.me] Прекращено ожидание __NUXT_DATA__'); // Если не удалось получить данные из DOM, можно попробовать получить из API // для конкретных showId, которые нам нужны в данный момент tryFetchingMissingShowsFromAPI(); if (callback) callback(); return; } // Перенёс это в конец. Не знаю зачем const dataObject = parseScriptData(); if (!dataObject) { // console.log('createNuxtMap из начала createNuxtMap()'); // setTimeout(() => createNuxtMap(callback, attempts - 1), 200); return; } let iShowIDs = findKeyInArray(dataObject, 'list', 30) || findKeyInArray(dataObject, 'userShows', 30); if (iShowIDs) { const showIDs = dataObject?.[iShowIDs]; if (Array.isArray(showIDs) && showIDs.length) { showIDs.forEach(element => { try { const show = dataObject?.[element]?.show; if (show && dataObject?.[show]?.id && dataObject?.[show]?.titleOriginal) { nuxtMap.set( dataObject[dataObject[show].id].toString().trim(), dataObject[dataObject[show].titleOriginal].trim() ); } } catch (error) { console.error('[old.myshows.me] Ошибка при сопоставлении структуры list||userShows:', error); } }); console.log('[old.myshows.me] Карта создана (list||userShows structure)'); if (callback) callback(); return; } } const profileShowsIdx = findKeyInArray(dataObject, 'profileShows'); if (profileShowsIdx) { const profileShows = dataObject[profileShowsIdx]; const showFiltersIdx = profileShows?.showFilters; if (showFiltersIdx && Array.isArray(dataObject[showFiltersIdx])) { const m1 = dataObject[showFiltersIdx]; m1.forEach(m1Idx => { const m1Data = dataObject[m1Idx]; if (!m1Data || !m1Data.shows) return; const showsIdx = m1Data.shows; if (showsIdx && Array.isArray(dataObject[showsIdx])) { const m2 = dataObject[showsIdx]; m2.forEach(m2Idx => { const show = dataObject[m2Idx]; if (show && show.id && show.titleOriginal) { try { nuxtMap.set( dataObject[show.id].toString().trim(), dataObject[show.titleOriginal].trim() ); } catch (error) { console.error('[old.myshows.me] Ошибка при сопоставлении структуры showFilters:', error); } } }); } }); console.log('[old.myshows.me] Карта создана (структура showFilters)'); if (callback) callback(); return; } } console.log('[old.myshows.me] Подходящая структура в __NUXT_DATA__ не найдена'); tryFetchingMissingShowsFromAPI(); if (callback) callback(); // console.log('[old.myshows.me] createNuxtMap из конца createNuxtMap()'); // setTimeout(() => createNuxtMap(callback, attempts - 1), 200); } /** * Изменяет отображение чисел в шапке профиля на странице `myshows.me/<userName>/`. * Заменяет сокращённые значения ("1к") на полные числа ("1 000") с использованием данных из `__NUXT_DATA__`. * Работает только если настройка `ModifyProfileNumbers` включена и изменения ещё не применены. * @returns {void} — Функция не возвращает значений. */ function modifyProfileNumbers() { if (!_Settings.getValue('ModifyProfileNumbers') || _Settings.getApplied('ModifyProfileNumbers')) return; // Выбираем все div с классом UserHeader__stats-row на странице const statsRows = document.querySelectorAll('div.UserHeader__stats-row'); if (!statsRows.length) return; // Пытаемся понять, с фильмами или без const statsTitles = document.querySelectorAll('.UserHeader__stats-title'); if (!statsTitles.length) return; const withMovies = [...statsTitles].some(el => /(фильм|фільм|movie)\w*/i.test(el.textContent.trim()) // Адаптация для разных языков ) ? 'statsTotal' : 'stats'; const dataObject = parseScriptData(); if (!dataObject) return; const path1 = 'data.User.profile.stats.watchedEpisodes'; const path2 = 'data.User.profile.statsMovies.watchedMovies'; const path3 = `data.User.profile.${withMovies}.watchedHours`; // Ищем значения в __NUXT_DATA__. Могут быть в разных местах в зависимости от открытой страницы let value1 = findValueByPath(dataObject, path1); if (!value1) value1 = dataObject?.[findKeyInArray(dataObject, path1.split('.').pop())] ?? undefined; if (!value1) return; let value2 = findValueByPath(dataObject, path2); if (!value2) value2 = dataObject?.[findKeyInArray(dataObject, path2.split('.').pop())] ?? undefined; if (!value2) return; let value3 = findValueByPath(dataObject, path3); if (!value3) value3 = dataObject?.[dataObject?.[findKeyInArray(dataObject, `${withMovies}`)]?.watchedHours] ?? undefined; if (!value3) return; let value4 = Math.ceil(value3 / 24); // Сохраняем значения по ключам const valueMap = new Map([ ['э', value1], // эпизодов (рус.) ['е', value1], // епізодів (укр.) ['e', value1], // episodes (англ.) ['ф', value2], // фильмов/фільмів (рус./укр.) ['m', value2], // movies (англ.) ['ч', value3], // часов (рус.) ['г', value3], // години (укр.) ['h', value3], // hours (англ.) ['д', value4], // дней/днів (рус./укр.) ['d', value4], // days (англ.) ]); let changes = 0; // Перебираем коллекцию элементов и меняем их содержимое statsRows.forEach(element => { const valueElement = element.querySelector('.UserHeader__stats-value'); const titleElement = element.querySelector('.UserHeader__stats-title'); if (!valueElement || !titleElement) return; // Получаем первую букву подписи const key = titleElement.textContent.trim().charAt(0).toLowerCase(); // Ищем такую в сохранённых if (valueMap.has(key)) { const value = valueMap.get(key); if (value !== undefined && value !== null) { // Если не пустая — присваиваем (без всяких привязок к классам и стилям, может потом) valueElement.textContent = Math.round(value).toLocaleString(); changes += 1; } } }); _Settings.setApplied('ModifyProfileNumbers', changes !== 0); if (changes !== 0) console.log(`[old.myshows.me] Применена настройка '${_Settings.getCheckboxData('ModifyProfileNumbers').labelText}'`); } /** * Удаляет ссылку из элемента с классом `Unwatched-showTitle` на странице `myshows.me/profile`, * оставляя ссылку только в русском названии шоу (элемент с классом `Unwatched-showTitle-title`). * Работает только если настройка `ModifyShowTitleLink` включена и изменения ещё не применены. * @returns {void} — Функция не возвращает значений. */ function modifyShowTitleLink() { if (!_Settings.getValue('ModifyShowTitleLink') || _Settings.getApplied('ModifyShowTitleLink')) return; let elements = document.querySelectorAll('a.Unwatched-showTitle'); let changes = 0; elements.forEach(element => { // Получаем значение href из элемента с классом 'Unwatched-showTitle' const hrefValue = element.getAttribute('href'); // Создаём новый элемент <div> const newElement = document.createElement('div'); newElement.className = 'Unwatched-showTitle'; // Перебираем все дочерние элементы элемента <a> while (element.firstChild) { // Перемещаем каждый дочерний элемент из <a> в новый <div> newElement.appendChild(element.firstChild); } // Заменяем элемент <a> на новый элемент <div> element.parentNode.replaceChild(newElement, element); // Ищем внутри нового элемента элементы с классом "Unwatched-showTitle-title" и заменяем их на ссылки let titleElements = newElement.querySelectorAll('span.Unwatched-showTitle-title'); if (!titleElements) return; titleElements.forEach(titleElement => { const newLink = document.createElement('a'); newLink.href = hrefValue; newLink.className = 'Unwatched-showTitle-title'; newLink.innerHTML = titleElement.innerHTML; // Заменяем элемент <span> на новый элемент <a> titleElement.parentNode.replaceChild(newLink, titleElement); changes += 1; if (!element.classList.contains('FIXED')) { element.classList.add('FIXED'); } }); }); _Settings.setApplied('ModifyShowTitleLink', changes !== 0); if (changes !== 0) console.log(`[old.myshows.me] Применена настройка '${_Settings.getCheckboxData('ModifyShowTitleLink').labelText}'`); } /** * Изменяет текст в элементах с классом `Unwatched-showSeasonMeta` на странице `myshows.me/profile`. * Заменяет формат "N эпизодов с eX" на "N эпизодов с e0X" для чисел меньше 10, добавляя ведущий ноль. * Работает только если настройка `ModifyShowSeasonMeta` включена и изменения ещё не применены. * @returns {void} — Функция не возвращает значений. */ function modifyShowSeasonMeta() { if (!_Settings.getValue('ModifyShowSeasonMeta') || _Settings.getApplied('ModifyShowSeasonMeta')) return; // Находим все элементы <div> с классом "Unwatched-showSeasonMeta" const elements = document.querySelectorAll('div.Unwatched-showSeasonMeta'); // const regex = / с e(0(?=$)|[1-9]\d*$)/; // Не помню, почему именно так было const regex = / (from|с|з) e([0-9])$/i; // Адаптация для разных языков let changes = 0; // Перебираем каждый элемент elements.forEach(element => { // Получаем текстовое содержимое элемента const text = element.textContent; // Ищем подстроку " с e" и последующей цифрой const match = text.match(regex); // Если подстрока найдена и цифра меньше 10 if (match) { // Заменяем найденную цифру на "0" + цифра. // Устанавливаем новый текстовый контент элемента element.textContent = text.replace(match[0], ` ${match[1]} e0${match[2]}`); changes += 1; } }); _Settings.setApplied('ModifyShowSeasonMeta', changes !== 0); if (changes !== 0) console.log(`[old.myshows.me] Применена настройка '${_Settings.getCheckboxData('ModifyShowSeasonMeta').labelText}'`); } /** * Обновляет высоту строк (`.Row`) на странице `/profile/next/` * на основе высоты элементов с классом `.OldMyShowsClass`. * Если высота элемента превышает 20 пикселей (многострочный текст), * устанавливает автоматическую высоту, иначе задаёт фиксированную. * @returns {void} — Функция не возвращает значений. */ function updateRowsHeightsBasedOnOldMyShowsClass() { // Получаем все элементы с классом OldMyShowsClass const elements = document.querySelectorAll('.OldMyShowsClass'); if (!elements.length) return; let changes = 0; // Перебираем каждый элемент. // Если высота больше 20 (то есть содержит текста на несколько строк) — ставим автовысоту у `.Row` // 2025.03.13: На текущий момент показывает, что 18.18 — обычная высота. 36.36 — когда текст уже на две строки переносится elements.forEach((element) => { const closestRow = element.closest('.Row'); if (closestRow) { const heightNow = closestRow.style.getPropertyValue('height') || rowHeight; // Текущая высота. Если пустая, то считаем стандартной const heightToBe = element.clientHeight > 20 ? 'auto' : rowHeight; if (heightNow !== heightToBe) { closestRow.style.setProperty('height', heightToBe, 'important'); changes += 1; } } }); if (changes !== 0) console.log(`[old.myshows.me] Исправлена высота некоторых строк (${changes} шт.)`); } /** * Инициализирует или обновляет стили CSS для элементов на странице, добавляя их в элемент `<style>`. * Создаёт или переиспользует существующий элемент `<style>` в `<head>`, применяя правила для различных разделов сайта. * Устанавливает стили с приоритетом `!important` для обеспечения корректного отображения изменений. * @returns {void} — Функция не возвращает значений. */ function initStyle() { // Получить существующий / создать новый элемент <style> STYLE = document.querySelector('style') || document.createElement('style'); if (!STYLE.parentNode) { // Вставить новый элемент <style> в <head> document.head.appendChild(STYLE); } const statsRowColor = 'white'; // Не будем много раз добавлять одно и то же const startPhrase = '/* old.myshows.me.start */'; const endPhrase = '/* old.myshows.me.end */'; // Находим индексы начала и конца текста между фразами let styleContent = STYLE.textContent; const startIndex = styleContent.indexOf(startPhrase); const endIndex = styleContent.indexOf(endPhrase) + endPhrase.length; if (startIndex !== -1 && endIndex !== -1) { // Удаляем существующий текст между startPhrase и endPhrase STYLE.textContent = (styleContent.substring(0, startIndex) + styleContent.substring(endIndex)).trim(); } STYLE.textContent += /*CSS*/ ` ${startPhrase} .hidden { display: none; } /* ============================================================================================= */ /* myshows.me/<userName> */ .UserHeader__stats-row { text-shadow: -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 1px 0 black; color: ${statsRowColor}; } .UserHeader__stats-title { color: ${statsRowColor} } /* ============================================================================================= */ /* myshows.me/profile/next/ */ .OldMyShowsClass { font-size: 14px; } .WatchSoon-episodes .Row { height: ${rowHeight}; padding: 0 10px 0 10px; } .WatchSoon-date { max-width: 43px; font-weight: 500; font-size: 13px; } .WatchSoon-date a { display: flex; flex-wrap: wrap; gap: 0 4px; } .WatchSoon-date a div:first-child::after { content: ','; } .WatchSoon-show { font-size: 14px; } .calendar-day__counter { margin: 0 0 0 2px; } /* Уменьшение отступа между датой и количеством серий в календарике */ /* ============================================================================================= */ /* myshows.me/profile */ .Unwatched-showTitle-inline { display: inline-flex; } .Unwatched-showTitle-subTitle { display: none; } .Unwatched-showTitle-title { align-self: auto; padding-right: 10px; } .Unwatched-season~div .UnwatchedEpisodeItem { height: ${rowHeight}; } .UnwatchedEpisodeItem__info { display: contents; } /* ============================================================================================= */ /* myshows.me/profile/edit */ .oldMyshowsSettings-style { display: grid; } .oldMyshowsSettings-label { display: inline-flex; margin-top: 20px; } .oldMyshowsSettings-label>* { margin-right: 7px; } /* Увеличение отступа между полем флажка и текстом */ .oldMyshowsSettings-checkbox input[type="checkbox"] { width: 10px; height: 10px; } /* Ширина/высота поля флажка */ ${endPhrase} `.replace(/;\s/g, ' !important;'); } /** * Проверяет текущий URL и применяет соответствующие модификации страницы в зависимости от настроек и адреса. * Вызывает функции сортировки, исправления элементов и стилей для страниц `/profile/next/`, `/profile/`, `/<userName>/` и `/profile/edit/`. * @returns {void} — Функция не возвращает значений. */ function ensureModifications() { const currentUrl = window.location.href; checkUrlChange(); if (currentUrl.includes('myshows.me/profile/next/')) { sortWatchSoonItems(); updateRowsHeightsBasedOnOldMyShowsClass(); if (apiRequestsInProgress.size === 0) { if (_Settings.getValue('OriginalTitleIsNeeded') && nuxtMap.size === 0) { createNuxtMap(() => fixWatchSoonElements()); } else { fixWatchSoonElements(); } } } if (currentUrl.includes('myshows.me/profile/')) { modifyShowTitleLink(); modifyShowSeasonMeta(); } if (currentUrl.includes('myshows.me/profile/edit/')) { createCheckboxes(); } modifyProfileNumbers(); // Оказывается, у других людей тоже есть цифры в профиле xD initStyle(); } /** * Настраивает наблюдатель (`MutationObserver`) для отслеживания изменений в DOM на странице. * При обнаружении изменений, требующих повторного применения модификаций, вызывает `ensureModifications`. * Игнорирует изменения в уже модифицированных элементах или скрытых классах. * @returns {void} — Функция не возвращает значений. */ function setupObserver() { const observer = new MutationObserver((mutations) => { let shouldRun = false; mutations.forEach(mutation => { const currentUrl = window.location.href; if (currentUrl.includes('myshows.me/profile/')) { if (mutation.target.querySelector('a.Unwatched-showTitle:not(.FIXED)')) { _Settings.setApplied('ModifyShowTitleLink', false); shouldRun = true; } if (mutation.target.querySelector('div.Unwatched-showSeasonMeta')) { _Settings.setApplied('ModifyShowSeasonMeta', false); shouldRun = true; } } if (currentUrl.includes('myshows.me/profile/next/') && !document.querySelector('.OldMyShowsClass')) { _Settings.setApplied('watchSoonElementsModified', false); if (!!document.querySelector('.WatchSoon__title-wrapper')) { shouldRun = true; } } if (mutation.target.querySelector('div.UserHeader__stats-row:not(.FIXED)')) { _Settings.setApplied('ModifyProfileNumbers', false); shouldRun = true; } if (currentUrl.includes('myshows.me/profile/edit/') && !document.querySelector('.oldMyshowsSettings')) { shouldRun = true; } }); if (shouldRun) { console.log('[old.myshows.me] Observer обнаружил изменения, повторное применение модификаций'); ensureModifications(); } }); observer.observe(document.querySelector('.Main-content') || document.body, { childList: true, subtree: true, attributes: true }); } /** * Выполняет начальную настройку страницы при её загрузке. * Устанавливает наблюдатель, применяет модификации и стили с небольшой задержкой. * @returns {void} — Функция не возвращает значений. */ function onPageLoad() { setupObserver(); setTimeout(ensureModifications, defaultTimeout); initStyle(); } window.addEventListener('load', onPageLoad); window.addEventListener('popstate', () => setTimeout(ensureModifications, defaultTimeout)); window.addEventListener('resize', updateRowsHeightsBasedOnOldMyShowsClass); const originalPushState = history.pushState; history.pushState = function () { originalPushState.apply(this, arguments); setTimeout(ensureModifications, defaultTimeout); }; const originalReplaceState = history.replaceState; history.replaceState = function () { originalReplaceState.apply(this, arguments); setTimeout(ensureModifications, defaultTimeout); }; // С вероятностью 95% какие-то проверки лишние. // Но когда пытался сократить их количество — что-нибудь переставало работать })();