🏠 返回首頁 

Greasy Fork is available in English.

old.myshows.me

С 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% какие-то проверки лишние.
// Но когда пытался сократить их количество — что-нибудь переставало работать
})();