// ==UserScript==
// @name ShikiPlayer
// @description Shikimori видеоплееры для просмотра аниме Turbo → Lumex → Alloha → Kodik → Collaps → Veoveo → Vibix
// @namespace https://github.com/Onzis/ShikiPlayer
// @author Onzis
// @license GPL-3.0 license
// @version 1.67
// @homepageURL https://github.com/Onzis/ShikiPlayer
// @grant GM.xmlHttpRequest
// @connect shikimori.me
// @connect kodikapi.com
// @connect apicollaps.cc
// @connect api.kinobox.tv
// @match *://shikimori.one/*
// @match *://beggins-as.pljjalgo.online/*
// @match *://beggins-as.allarknow.online/*
// @match *://beggins-as.algonoew.online/*
// @run-at document-end
// ==/UserScript==
/* jshint -W097 */
"use strict";
// --- INJECTION OF DARK THEME CSS ---
const darkThemeCSS = `
/* ==ShikiPlayer Dark Theme== */
:root {
/* Основные цвета тёмной темы */
--sp-bg-primary: #0a0a0a;
--sp-bg-secondary: #1a1a1a;
--sp-bg-tertiary: #252525;
--sp-bg-hover: #2a2a2a;
--sp-bg-active: #333333;
/* Текстовые цвета */
--sp-text-primary: #e0e0e0;
--sp-text-secondary: #b0b0b0;
--sp-text-muted: #808080;
--sp-text-inverse: #000000;
/* Акцентные цвета */
--sp-accent: #6366f1;
--sp-accent-hover: #818cf8;
--sp-accent-active: #4f46e5;
--sp-accent-light: #e0e7ff;
/* Статусные цвета */
--sp-success: #10b981;
--sp-warning: #f59e0b;
--sp-error: #ef4444;
--sp-online: #22c55e;
--sp-offline: #64748b;
--sp-loading: #3b82f6;
/* Границы */
--sp-border-color: #333333;
--sp-border-light: #404040;
/* Радиусы и отступы */
--sp-radius-sm: 4px;
--sp-radius-md: 8px;
--sp-radius-lg: 12px;
--sp-radius-xl: 16px;
--sp-spacing-xs: 4px;
--sp-spacing-sm: 8px;
--sp-spacing-md: 10px;
--sp-spacing-lg: 24px;
--sp-spacing-xl: 32px;
/* Анимации */
--sp-transition-fast: 150ms ease;
--sp-transition-normal: 250ms ease;
--sp-transition-slow: 350ms ease;
}
/* Внешний контейнер для центрирования кнопки */
.sp-outer-wrapper {
margin: var(--sp-spacing-lg) 0 !important;
}
/* Контейнер для кнопки с фоном как у плеера */
.sp-button-container {
background: linear-gradient(135deg, #2b2a39eb 0%, #2b2a39eb 100%) !important;
border: 1px solid var(--sp-border-color) !important;
border-top: none !important;
border-radius: 0 0 var(--sp-radius-lg) var(--sp-radius-lg) !important;
padding: var(--sp-spacing-md) !important;
display: flex !important;
justify-content: center !important;
align-items: center !important;
margin-top: -1px !important;
gap: var(--sp-spacing-md) !important;
}
/* Базовые стили для ShikiPlayer */
.sp-wrapper {
background: var(--sp-bg-primary) !important;
border: 1px solid var(--sp-border-color) !important;
border-radius: var(--sp-radius-lg) var(--sp-radius-lg) 0 0 !important;
overflow: hidden !important;
transition: all var(--sp-transition-normal) !important;
position: relative !important;
}
/* Контейнер плеера */
.sp-container {
background: var(--sp-bg-secondary) !important;
border-radius: var(--sp-radius-lg) var(--sp-radius-lg) 0 0 !important;
overflow: hidden !important;
}
/* Заголовок плеера */
.sp-header {
background: linear-gradient(135deg, #2b2a39eb 0%, #2b2a39eb 100%) !important;
padding: var(--sp-spacing-md) var(--sp-spacing-lg) !important;
border-bottom: 1px solid var(--sp-border-color) !important;
display: flex !important;
align-items: center !important;
justify-content: space-between !important;
}
.sp-title {
color: var(--sp-text-primary) !important;
font-size: 18px !important;
font-weight: 600 !important;
margin: 0 !important;
display: flex !important;
align-items: center !important;
gap: var(--sp-spacing-sm) !important;
}
/* Выпадающий список плееров */
.sp-dropdown {
position: relative !important;
margin-left: auto !important;
}
.sp-dropdown-toggle {
background: var(--sp-bg-tertiary) !important;
border: 1px solid var(--sp-border-light) !important;
border-radius: var(--sp-radius-md) !important;
color: var(--sp-text-primary) !important;
padding: var(--sp-spacing-sm) var(--sp-spacing-md) !important;
cursor: pointer !important;
display: flex !important;
align-items: center !important;
gap: var(--sp-spacing-sm) !important;
transition: all var(--sp-transition-fast) !important;
font-size: 14px !important;
font-weight: 500 !important;
}
.sp-dropdown-toggle:hover {
background: var(--sp-bg-hover) !important;
border-color: var(--sp-accent) !important;
transform: translateY(-1px) !important;
}
.sp-dropdown-toggle::after {
content: "▼" !important;
font-size: 12px !important;
transition: transform var(--sp-transition-fast) !important;
}
.sp-dropdown.open .sp-dropdown-toggle::after {
transform: rotate(180deg) !important;
}
.sp-dropdown-menu {
position: absolute !important;
top: 100% !important;
right: 0 !important;
background: var(--sp-bg-tertiary) !important;
border: 1px solid var(--sp-border-light) !important;
border-radius: var(--sp-radius-md) !important;
min-width: 100px !important;
z-index: 1000 !important;
opacity: 0 !important;
visibility: hidden !important;
transform: translateY(-10px) !important;
transition: all var(--sp-transition-fast) !important;
margin-top: var(--sp-spacing-xs) !important;
max-height: 300px !important;
overflow-y: auto !important;
}
.sp-dropdown.open .sp-dropdown-menu {
opacity: 1 !important;
visibility: visible !important;
transform: translateY(0) !important;
}
.sp-dropdown-item {
padding: var(--sp-spacing-sm) var(--sp-spacing-md) !important;
cursor: pointer !important;
transition: all var(--sp-transition-fast) !important;
display: flex !important;
align-items: center !important;
gap: var(--sp-spacing-sm) !important;
color: var(--sp-text-secondary) !important;
font-size: 14px !important;
border-bottom: 1px solid var(--sp-border-color) !important;
}
.sp-dropdown-item:last-child {
border-bottom: none !important;
}
.sp-dropdown-item:hover {
background: var(--sp-bg-hover) !important;
color: var(--sp-text-primary) !important;
}
.sp-dropdown-item.active {
background: #3d3d3d !important;
color: #ffffff !important;
}
.sp-dropdown-item.loading {
color: var(--sp-text-muted) !important;
cursor: not-allowed !important;
}
/* Индикаторы статуса */
.sp-status-indicator {
width: 8px !important;
height: 8px !important;
border-radius: 50% !important;
margin-left: auto !important;
transition: all var(--sp-transition-fast) !important;
}
.sp-status-indicator.online {
background: var(--sp-online) !important;
animation: pulse 2s infinite !important;
}
.sp-status-indicator.offline {
background: var(--sp-offline) !important;
}
.sp-status-indicator.loading {
background: var(--sp-loading) !important;
animation: spin 1s linear infinite !important;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* Область просмотра плеера */
.sp-viewer {
background: var(--sp-bg-primary) !important;
min-height: 500px !important;
height: 500px !important;
position: relative !important;
overflow: hidden !important;
}
.sp-viewer iframe {
border: none !important;
width: 100% !important;
height: 100% !important;
min-height: 400px !important;
}
/* Оверлей загрузки */
.sp-loading-overlay {
position: absolute !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
background: rgba(10, 10, 10, 0.9) !important;
display: flex !important;
align-items: center !important;
justify-content: center !important;
z-index: 100 !important;
backdrop-filter: blur(4px) !important;
}
.sp-loading-overlay::after {
content: "" !important;
width: 40px !important;
height: 40px !important;
border: 3px solid var(--sp-border-color) !important;
border-top: 3px solid var(--sp-accent) !important;
border-radius: 50% !important;
animation: spin 1s linear infinite !important;
}
/* Кнопка режима кинотеатра - квадратная с закругленными углами */
.sp-theater-btn {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 48px !important;
height: 48px !important;
padding: var(--sp-spacing-sm) !important;
background: var(--sp-bg-tertiary) !important;
border: 1px solid var(--sp-border-light) !important;
border-radius: var(--sp-radius-md) !important;
color: var(--sp-text-primary) !important;
cursor: pointer !important;
transition: all var(--sp-transition-fast) !important;
position: relative !important;
}
.sp-theater-btn:hover {
background: var(--sp-bg-hover) !important;
border-color: var(--sp-accent) !important;
transform: translateY(-2px) !important;
}
.sp-theater-btn:active {
transform: translateY(0) !important;
}
.sp-theater-btn svg {
width: 24px !important;
height: 24px !important;
flex-shrink: 0 !important;
}
/* Кнопка добавления эпизода */
.sp-episode-btn {
display: flex !important;
align-items: center !important;
justify-content: center !important;
width: 48px !important;
height: 48px !important;
padding: var(--sp-spacing-sm) !important;
background: var(--sp-bg-tertiary) !important;
border: 1px solid var(--sp-border-light) !important;
border-radius: var(--sp-radius-md) !important;
color: var(--sp-text-primary) !important;
cursor: pointer !important;
transition: all var(--sp-transition-fast) !important;
position: relative !important;
}
.sp-episode-btn:hover {
background: var(--sp-bg-hover) !important;
border-color: var(--sp-accent) !important;
transform: translateY(-2px) !important;
}
.sp-episode-btn:active {
transform: translateY(0) !important;
}
.sp-episode-btn svg {
width: 24px !important;
height: 24px !important;
flex-shrink: 0 !important;
}
/* Счетчик просмотренных серий */
.sp-episode-count {
position: absolute !important;
bottom: -5px !important;
right: -5px !important;
background: var(--sp-accent) !important;
color: #ffffff !important;
font-size: 12px !important;
font-weight: bold !important;
padding: 2px 4px !important;
border-radius: 10px !important;
min-width: 28px !important;
text-align: center !important;
line-height: 12px !important;
}
/* Кнопка закрытия в режиме кинотеатра */
.sp-theater-close {
position: absolute !important;
top: var(--sp-spacing-md) !important;
right: var(--sp-spacing-md) !important;
width: 40px !important;
height: 40px !important;
background: rgba(0, 0, 0, 0.7) !important;
border: 1px solid var(--sp-border-color) !important;
border-radius: var(--sp-radius-md) !important;
display: none !important;
align-items: center !important;
justify-content: center !important;
cursor: pointer !important;
z-index: 10000 !important;
transition: all var(--sp-transition-fast) !important;
backdrop-filter: blur(4px) !important;
}
.sp-theater-close:hover {
background: rgba(239, 68, 68, 0.8) !important;
border-color: var(--sp-error) !important;
transform: scale(1.1) !important;
}
.sp-theater-close svg {
width: 20px !important;
height: 20px !important;
color: var(--sp-text-primary) !important;
}
/* Режим кинотеатра */
.sp-wrapper.theater-mode {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
z-index: 9999 !important;
margin: 0 !important;
border-radius: 0 !important;
border: none !important;
background: var(--sp-bg-primary) !important;
}
.sp-wrapper.theater-mode .sp-viewer {
height: 100vh !important;
min-height: unset !important;
}
.sp-wrapper.theater-mode .sp-header {
display: none !important;
}
.sp-wrapper.theater-mode .sp-theater-close {
display: flex !important;
}
/* Стили для кастомных подсказок */
.sp-tooltip {
position: absolute !important;
background: var(--sp-bg-secondary) !important;
color: var(--sp-text-primary) !important;
padding: var(--sp-spacing-xs) var(--sp-spacing-sm) !important;
border-radius: var(--sp-radius-sm) !important;
font-size: 14px !important;
font-weight: 500 !important;
white-space: nowrap !important;
z-index: 10000 !important;
pointer-events: none !important;
opacity: 0 !important;
transform: translateY(-5px) !important;
transition: all var(--sp-transition-fast) !important;
backdrop-filter: blur(4px) !important;
border: 1px solid var(--sp-border-color) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
.sp-tooltip.visible {
opacity: 1 !important;
transform: translateY(0) !important;
}
.sp-tooltip::before {
content: "" !important;
position: absolute !important;
bottom: 100% !important;
left: 50% !important;
transform: translateX(-50%) !important;
border-width: 5px !important;
border-style: solid !important;
border-color: transparent transparent var(--sp-bg-secondary) transparent !important;
}
/* Скроллбар */
.sp-dropdown-menu::-webkit-scrollbar {
width: 6px !important;
}
.sp-dropdown-menu::-webkit-scrollbar-track {
background: var(--sp-bg-secondary) !important;
}
.sp-dropdown-menu::-webkit-scrollbar-thumb {
background: var(--sp-border-light) !important;
border-radius: 3px !important;
}
.sp-dropdown-menu::-webkit-scrollbar-thumb:hover {
background: var(--sp-accent) !important;
}
/* Адаптивность */
@media (max-width: 768px) {
.sp-header {
padding: var(--sp-spacing-sm) var(--sp-spacing-md) !important;
flex-direction: column !important;
gap: var(--sp-spacing-sm) !important;
}
.sp-dropdown {
width: 100% !important;
}
.sp-dropdown-toggle {
width: 100% !important;
justify-content: space-between !important;
}
.sp-dropdown-menu {
position: absolute !important;
top: 100% !important;
left: 0 !important;
transform: none !important;
width: 100% !important;
max-width: none !important;
border-radius: 0 0 var(--sp-radius-md) var(--sp-radius-md) !important;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15) !important;
}
}
/* Дополнительные улучшения */
.sp-wrapper * {
box-sizing: border-box !important;
}
.sp-wrapper button,
.sp-wrapper select,
.sp-wrapper input {
background: var(--sp-bg-tertiary) !important;
border: 1px solid var(--sp-border-light) !important;
border-radius: var(--sp-radius-sm) !important;
color: var(--sp-text-primary) !important;
padding: var(--sp-spacing-xs) var(--sp-spacing-sm) !important;
transition: all var(--sp-transition-fast) !important;
}
.sp-wrapper button:hover,
.sp-wrapper select:hover,
.sp-wrapper input:hover {
border-color: var(--sp-accent) !important;
}
.sp-wrapper button:focus,
.sp-wrapper select:focus,
.sp-wrapper input:focus {
outline: none !important;
border-color: var(--sp-accent) !important;
}
/* Анимация появления */
.sp-outer-wrapper {
animation: fadeInUp 0.5s ease-out !important;
}
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
`;
function injectDarkTheme() {
if (document.getElementById("shikiplayer-dark-theme")) return;
const style = document.createElement("style");
style.id = "shikiplayer-dark-theme";
style.textContent = darkThemeCSS;
document.head.appendChild(style);
}
injectDarkTheme();
// --- END OF CSS INJECTION ---
// Класс для создания кастомных подсказок
class Tooltip {
constructor() {
this.tooltip = null;
this.targetElement = null;
this.showTimeout = null;
this.hideTimeout = null;
this.init();
}
init() {
// Создаем элемент подсказки
this.tooltip = document.createElement("div");
this.tooltip.className = "sp-tooltip";
document.body.appendChild(this.tooltip);
}
// Добавление подсказки к элементу
attach(element, text) {
// Удаляем стандартный атрибут title, чтобы избежать дублирования
const title = element.getAttribute("title");
if (title) {
element.setAttribute("data-tooltip-text", title);
element.removeAttribute("title");
} else if (text) {
element.setAttribute("data-tooltip-text", text);
}
// Добавляем обработчики событий
element.addEventListener("mouseenter", this.show.bind(this));
element.addEventListener("mouseleave", this.hide.bind(this));
element.addEventListener("click", this.hide.bind(this));
}
// Показ подсказки
show(event) {
const element = event.currentTarget;
const text = element.getAttribute("data-tooltip-text");
if (!text) return;
// Отменяем скрытие, если оно было запланировано
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
this.hideTimeout = null;
}
// Запланируем показ с минимальной задержкой
this.showTimeout = setTimeout(() => {
this.targetElement = element;
this.tooltip.textContent = text;
// Позиционирование подсказки
const rect = element.getBoundingClientRect();
const tooltipRect = this.tooltip.getBoundingClientRect();
// Показываем подсказку снизу от элемента
let top = rect.bottom + 10;
let left = rect.left + (rect.width - tooltipRect.width) / 2;
// Проверяем, не выходит ли подсказка за пределы экрана снизу
if (top + tooltipRect.height > window.innerHeight) {
// Если не помещается снизу, показываем сверху
top = rect.top - tooltipRect.height - 10;
}
// Проверяем, не выходит ли подсказка за пределы экрана по горизонтали
if (left < 0) {
left = 5;
} else if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 5;
}
this.tooltip.style.top = `${top + window.scrollY}px`;
this.tooltip.style.left = `${left + window.scrollX}px`;
// Показываем подсказку с анимацией
this.tooltip.classList.add("visible");
}, 100); // Уменьшенная задержка перед показом
}
// Скрытие подсказки
hide() {
// Отменяем показ, если он был запланирован
if (this.showTimeout) {
clearTimeout(this.showTimeout);
this.showTimeout = null;
}
// Немедленно скрываем подсказку
this.tooltip.classList.remove("visible");
this.targetElement = null;
}
// Уничтожение подсказки
destroy() {
if (this.showTimeout) {
clearTimeout(this.showTimeout);
}
if (this.hideTimeout) {
clearTimeout(this.hideTimeout);
}
if (this.tooltip && this.tooltip.parentNode) {
this.tooltip.parentNode.removeChild(this.tooltip);
}
}
}
// Базовый класс для ошибок
class ErrorBase extends Error {
constructor(message, options) {
super(message, options);
this.name = new.target.name;
}
}
// Ошибка для некорректных HTTP-ответов
class ResponseError extends ErrorBase {
constructor(response) {
super(
`Received response with unsuccessful code ${response.status} ${response.statusText}`
);
this.response = response;
}
}
// HTTP-клиент для GM.xmlHttpRequest с таймаутом
class GMHttp {
async fetch(input, init) {
const methods = [
"GET",
"POST",
"PUT",
"DELETE",
"PATCH",
"HEAD",
"TRACE",
"OPTIONS",
"CONNECT",
];
let requestMethod = init?.method ?? "GET";
if (!methods.includes(requestMethod)) {
throw new Error(`HTTP method ${requestMethod} is not supported`);
}
let requestUrl = input.toString();
let requestBody = init?.body
? await new Response(init.body).text()
: undefined;
let requestHeaders = init?.headers
? Object.fromEntries(new Headers(init.headers))
: {};
// Добавляем таймаут по умолчанию 5 секунд
const timeout = init?.timeout || 5000;
const timeoutId = setTimeout(() => {
throw new Error(`Request timeout after ${timeout}ms`);
}, timeout);
let gmResponse = await new Promise((resolve, reject) => {
GM.xmlHttpRequest({
url: requestUrl,
method: requestMethod,
data: requestBody,
headers: requestHeaders,
responseType: "blob",
timeout: timeout,
onload: (response) => {
clearTimeout(timeoutId);
resolve(response);
},
onerror: (error) => {
clearTimeout(timeoutId);
reject(error);
},
ontimeout: () => {
clearTimeout(timeoutId);
reject(new Error(`Request timeout after ${timeout}ms`));
},
});
});
let responseHeaders = gmResponse.responseHeaders
.trim()
.split(/\r?\n/)
.map((line) => line.split(/:\s*/, 2));
return new Response(gmResponse.response, {
status: gmResponse.status,
statusText: gmResponse.statusText,
headers: responseHeaders,
});
}
}
// Утилита для проверки JSON
class Json {
static parse(text, type) {
let value;
try {
value = JSON.parse(text);
} catch (e) {
throw new Error(`Error parsing JSON: ${text}`);
}
if (!type(value)) {
throw new Error(`Invalid JSON type`);
}
return value;
}
}
// Базовый класс плеера
class PlayerBase {
getEpisode() {
return 0;
}
setEpisode(value) {}
getTime() {
return 0;
}
setTime(value) {}
getTranslation() {
return "";
}
setTranslation(value) {}
dispose() {}
}
// Kodik Player
class KodikPlayer extends PlayerBase {
constructor(uid, results) {
super();
this.uid = uid;
this._results = results;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this._translation = results[0] || new Error("No translation found");
this.rebuildIFrameSrc();
addEventListener("message", this.onMessage);
}
name = "Kodik";
element;
_episode = 1;
_time = 0;
_translation;
getEpisode() {
return this._episode;
}
setEpisode(value) {
this._episode = value;
this.rebuildIFrameSrc();
}
getTime() {
return this._time;
}
setTime(value) {
this._time = value;
this.rebuildIFrameSrc();
}
getTranslation() {
return this._translation.translation.id + "";
}
setTranslation(value) {
this._translation =
this._results.find((r) => r.translation.id === +value) ||
new Error(`Translation '${value}' not found`);
this.rebuildIFrameSrc();
}
rebuildIFrameSrc() {
let src = new URL(`https:${this._translation.link}`);
src.searchParams.set("uid", this.uid);
src.searchParams.set("episode", this._episode + "");
src.searchParams.set("start_from", this._time + "");
this.element.src = src.toString();
}
onMessage = (ev) => {
if (ev.source !== this.element.contentWindow) return;
let message;
try {
message = JSON.parse(ev.data);
} catch (e) {
return;
}
if (message.key === "kodik_player_time_update") {
this._time = message.value;
}
};
dispose() {
removeEventListener("message", this.onMessage);
}
}
// Kodik Factory
class KodikFactory {
constructor(uid, api) {
this.uid = uid;
this._api = api;
}
name = "Kodik";
async create(animeId, abort) {
let results = await this._api.search(animeId, abort);
if (results.length === 0) return null;
return new KodikPlayer(this.uid, results);
}
}
// Alloha Player
class AllohaPlayer extends PlayerBase {
constructor(url, season, lastEpisode) {
super();
this._url = url;
this._season = season;
this._lastEpisode = lastEpisode;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this.rebuildIFrameSrc();
addEventListener("message", this.onMessage);
}
name = "Alloha";
element;
_translation = "";
_episode = 1;
_season;
_lastEpisode;
_time = 0;
getEpisode() {
return this._episode;
}
setEpisode(value) {
this._episode = Math.min(value, this._lastEpisode);
this.rebuildIFrameSrc();
}
getSeason() {
return this._season;
}
setSeason(value) {
this._season = value;
this.rebuildIFrameSrc();
}
getTime() {
return this._time;
}
setTime(value) {
this._time = value;
this.rebuildIFrameSrc();
}
getTranslation() {
return this._translation;
}
setTranslation(value) {
this._translation = value;
this.rebuildIFrameSrc();
}
rebuildIFrameSrc() {
let src = new URL(this._url);
src.searchParams.set("season", this._season + "");
src.searchParams.set("translation", this._translation);
src.searchParams.set("episode", this._episode + "");
src.searchParams.set("start", this._time + "");
this.element.src = src.toString();
}
onMessage = (ev) => {
if (ev.source !== this.element.contentWindow) return;
let message;
try {
message = JSON.parse(ev.data);
} catch (e) {
return;
}
if (message.event === "timeupdate") {
this._time = message.time;
} else if (message.event === "sp_season") {
this.setSeason(message.season);
} else if (message.event === "sp_episode") {
this.setEpisode(message.episode);
} else if (message.event === "sp_translation") {
this.setTranslation(message.translation);
}
};
dispose() {
removeEventListener("message", this.onMessage);
}
}
// Alloha Factory - ИЗМЕНЕНО: Используем Kinobox API вместо Alloha API
class AllohaFactory {
constructor(kodikApi, kinoboxApi) {
this._kodikApi = kodikApi;
this._kinoboxApi = kinoboxApi;
}
name = "Alloha";
async create(animeId, abort) {
let kodikResults = await this._kodikApi.search(animeId);
let kodikResult = kodikResults[0];
if (!kodikResult || !kodikResult.kinopoisk_id) return null;
// Используем Kinobox API для получения плеера Alloha
let kinoboxResult = await this._kinoboxApi.players(
kodikResult.kinopoisk_id,
abort
);
// Ищем плеер Alloha в результатах
let alloha = kinoboxResult.data.find((p) => p.type === "Alloha");
if (!alloha || !alloha.iframeUrl) return null;
let season = kodikResult.last_season || 1;
// Устанавливаем значение по умолчанию для последнего эпизода
let lastEpisode = 12; // Значение по умолчанию, так как Kinobox API не предоставляет эту информацию
return new AllohaPlayer(alloha.iframeUrl, season, lastEpisode);
}
}
// Collaps Player
class CollapsPlayer extends PlayerBase {
constructor(url, season, lastEpisode) {
super();
this._url = url;
this._season = season;
this._lastEpisode = lastEpisode;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this.rebuildIFrameSrc();
}
name = "Collaps";
element;
_episode = 1;
_time = 0;
getEpisode() {
return this._episode;
}
setEpisode(value) {
this._episode = Math.min(value, this._lastEpisode);
this.rebuildIFrameSrc();
}
getTime() {
return this._time;
}
setTime(value) {
this._time = value;
this.rebuildIFrameSrc();
}
rebuildIFrameSrc() {
let src = new URL(this._url);
src.searchParams.set("season", this._season + "");
src.searchParams.set("episode", this._episode + "");
src.searchParams.set("time", this._time + "");
this.element.src = src.toString();
}
}
// Collaps Factory
class CollapsFactory {
constructor(kodikApi, collapsApi) {
this._kodikApi = kodikApi;
this._collapsApi = collapsApi;
}
name = "Collaps";
async create(animeId, abort) {
let kodikResults = await this._kodikApi.search(animeId);
let kodikResult = kodikResults[0];
if (!kodikResult || !kodikResult.kinopoisk_id) return null;
let collapsResults = await this._collapsApi.list(
kodikResult.kinopoisk_id,
abort
);
let collapsResult = collapsResults[0];
if (!collapsResult) return null;
let season = kodikResult.last_season || 1;
let lastEpisode =
collapsResult.seasons?.find((s) => s.season === season)?.episodes
.length || 1;
return new CollapsPlayer(collapsResult.iframe_url, season, lastEpisode);
}
}
// Turbo Player
class TurboPlayer extends PlayerBase {
constructor(url, season) {
super();
this._url = url;
this._season = season;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this.rebuildIFrameSrc();
}
name = "Turbo";
element;
rebuildIFrameSrc() {
let src = new URL(this._url);
this.element.src = src.toString();
}
}
// Turbo Factory
class TurboFactory {
constructor(kodikApi, kinoboxApi) {
this._kodikApi = kodikApi;
this._kinoboxApi = kinoboxApi;
}
name = "Turbo";
async create(animeId, abort) {
let kodikResults = await this._kodikApi.search(animeId);
let kodikResult = kodikResults[0];
if (!kodikResult || !kodikResult.kinopoisk_id) return null;
let kinoboxResult = await this._kinoboxApi.players(
kodikResult.kinopoisk_id,
abort
);
let turbo = kinoboxResult.data.find((p) => p.type === "Turbo");
if (!turbo || !turbo.iframeUrl) return null;
let season = kodikResult.last_season || 1;
return new TurboPlayer(turbo.iframeUrl, season);
}
}
// Lumex Player
class LumexPlayer extends PlayerBase {
constructor(url) {
super();
this._url = url;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this.rebuildIFrameSrc();
}
name = "Lumex";
element;
rebuildIFrameSrc() {
let src = new URL(this._url);
this.element.src = src.toString();
}
}
// Lumex Factory
class LumexFactory {
constructor(kodikApi, kinoboxApi) {
this._kodikApi = kodikApi;
this._kinoboxApi = kinoboxApi;
}
name = "Lumex";
async create(animeId, abort) {
let kodikResults = await this._kodikApi.search(animeId);
let kodikResult = kodikResults[0];
if (!kodikResult || !kodikResult.kinopoisk_id) return null;
let kinoboxResult = await this._kinoboxApi.players(
kodikResult.kinopoisk_id,
abort
);
let lumex = kinoboxResult.data.find((p) => p.type === "Lumex");
if (!lumex || !lumex.iframeUrl) return null;
return new LumexPlayer(lumex.iframeUrl);
}
}
// Veoveo Player
class VeoveoPlayer extends PlayerBase {
constructor(url) {
super();
this._url = url;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this.rebuildIFrameSrc();
}
name = "Veoveo";
element;
rebuildIFrameSrc() {
let src = new URL(this._url);
this.element.src = src.toString();
}
}
// Veoveo Factory
class VeoveoFactory {
constructor(kodikApi, kinoboxApi) {
this._kodikApi = kodikApi;
this._kinoboxApi = kinoboxApi;
}
name = "Veoveo";
async create(animeId, abort) {
let kodikResults = await this._kodikApi.search(animeId);
let kodikResult = kodikResults[0];
if (!kodikResult || !kodikResult.kinopoisk_id) return null;
let kinoboxResult = await this._kinoboxApi.players(
kodikResult.kinopoisk_id,
abort
);
let veoveo = kinoboxResult.data.find((p) => p.type === "Veoveo");
if (!veoveo || !veoveo.iframeUrl) return null;
return new VeoveoPlayer(veoveo.iframeUrl);
}
}
// Vibix Player
class VibixPlayer extends PlayerBase {
constructor(url) {
super();
this._url = url;
this.element = document.createElement("iframe");
this.element.allowFullscreen = true;
this.element.width = "100%";
this.element.style.aspectRatio = "16 / 9";
this.rebuildIFrameSrc();
}
name = "Vibix";
element;
rebuildIFrameSrc() {
let src = new URL(this._url);
this.element.src = src.toString();
}
}
// Vibix Factory
class VibixFactory {
constructor(kodikApi, kinoboxApi) {
this._kodikApi = kodikApi;
this._kinoboxApi = kinoboxApi;
}
name = "Vibix";
async create(animeId, abort) {
let kodikResults = await this._kodikApi.search(animeId);
let kodikResult = kodikResults[0];
if (!kodikResult || !kodikResult.kinopoisk_id) return null;
let kinoboxResult = await this._kinoboxApi.players(
kodikResult.kinopoisk_id,
abort
);
let vibix = kinoboxResult.data.find((p) => p.type === "Vibix");
if (!vibix || !vibix.iframeUrl) return null;
return new VibixPlayer(vibix.iframeUrl);
}
}
// API для Kodik
class KodikApi {
constructor(http, token) {
this._http = http;
this._token = token;
}
async search(shikimoriId, abort) {
let url = new URL("https://kodikapi.com/search");
url.searchParams.set("token", this._token);
url.searchParams.set("shikimori_id", shikimoriId + "");
let response = await this._http.fetch(url, {
signal: abort,
timeout: 3000,
});
if (!response.ok) throw new ResponseError(response);
let text = await response.text();
let data = Json.parse(
text,
(v) =>
typeof v === "object" &&
v !== null &&
Array.isArray(v.results) &&
v.results.every(
(e) =>
typeof e === "object" &&
e !== null &&
typeof e.link === "string" &&
(typeof e.kinopoisk_id === "undefined" ||
typeof e.kinopoisk_id === "string") &&
(typeof e.imdb_id === "undefined" ||
typeof e.imdb_id === "string") &&
typeof e.translation === "object" &&
e.translation !== null &&
typeof e.translation.id === "number" &&
(typeof e.last_season === "undefined" ||
typeof e.last_season === "number")
)
);
return data.results;
}
}
// API для Kinobox (используется для Turbo, Lumex, Alloha, Veoveo и теперь Vibix)
class KinoboxApi {
constructor(http) {
this._http = http;
}
_sessionId = Math.trunc(Math.random() * 100);
async players(kinopoisk, abort) {
let url = new URL("https://api.kinobox.tv/api/players");
url.searchParams.set("kinopoisk", kinopoisk + "");
url.searchParams.set("ts", this.getTs());
let response = await this._http.fetch(url, {
headers: {
Referer: "https://kinohost.web.app/",
Origin: "https://kinohost.web.app",
"Sec-Fetch-Site": "cross-site",
},
signal: abort,
timeout: 5000,
});
if (!response.ok) throw new ResponseError(response);
let text = await response.text();
return Json.parse(
text,
(v) =>
typeof v === "object" &&
v !== null &&
Array.isArray(v.data) &&
v.data.every(
(e) =>
typeof e === "object" &&
e !== null &&
typeof e.type === "string" &&
(e.iframeUrl === null || typeof e.iframeUrl === "string")
)
);
}
getTs() {
let s = Math.ceil(Date.now() / 1e3) % 1e5;
let i = s % 100;
let r = i - (i % 3);
return s - i + r + "." + this._sessionId;
}
}
// API для Collaps
class CollapsApi {
constructor(http, token) {
this._http = http;
this._token = token;
}
async list(kinopoiskId, abort) {
let url = new URL("https://apicollaps.cc/list");
url.searchParams.set("token", this._token);
url.searchParams.set("kinopoisk_id", kinopoiskId);
let response = await this._http.fetch(url, {
signal: abort,
timeout: 5000,
});
if (!response.ok) throw new ResponseError(response);
let text = await response.text();
let data = Json.parse(
text,
(v) =>
typeof v === "object" &&
v !== null &&
Array.isArray(v.results) &&
v.results.every(
(e) =>
typeof e === "object" &&
e !== null &&
typeof e.iframe_url === "string" &&
(typeof e.seasons === "undefined" ||
(Array.isArray(e.seasons) &&
e.seasons.every(
(s) =>
typeof s === "object" &&
s !== null &&
Array.isArray(s.episodes) &&
typeof s.season === "number"
)))
)
);
return data.results;
}
}
// Основной класс Shikiplayer
class Shikiplayer {
constructor(playerFactories) {
this._playerFactories = playerFactories;
// Создаем внешний контейнер
this.element = document.createElement("div");
this.element.className = "sp-outer-wrapper";
// ИСПРАВЛЕННАЯ HTML СТРУКТУРА: кнопка в отдельном контейнере
this.element.innerHTML = `
<div class="sp-wrapper">
<div class="sp-container">
<div class="sp-header">
<div class="sp-title">Онлайн-просмотр</div>
<div class="sp-dropdown">
<div class="sp-dropdown-toggle">
<span class="sp-selected-player">Выберите плеер</span>
</div>
<div class="sp-dropdown-menu"></div>
</div>
</div>
<div class="sp-viewer">
<div class="sp-loading-overlay"></div>
</div>
</div>
<button class="sp-theater-close" title="Закрыть режим кинотеатра">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="18" y1="6" x2="6" y2="18"></line>
<line x1="6" y1="6" x2="18" y2="18"></line>
</svg>
</button>
</div>
<div class="sp-button-container">
<button class="sp-theater-btn" title="Театральный режим">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M8 3H5a2 2 0 0 0-2 2v3m18 0V5a2 2 0 0 0-2-2h-3m0 18h3a2 2 0 0 0 2-2v-3M3 16v3a2 2 0 0 0 2 2h3"></path>
</svg>
</button>
<button class="sp-episode-btn" title="Отметить серию как просмотренную">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<line x1="12" y1="5" x2="12" y2="19"></line>
<line x1="5" y1="12" x2="19" y2="12"></line>
</svg>
<span class="sp-episode-count">0/0</span>
</button>
</div>
`;
this._wrapper = this.element.querySelector(".sp-wrapper");
this._container = this.element.querySelector(".sp-container");
this._dropdown = this.element.querySelector(".sp-dropdown");
this._dropdownToggle = this.element.querySelector(".sp-dropdown-toggle");
this._dropdownMenu = this.element.querySelector(".sp-dropdown-menu");
this._selectedPlayerText = this.element.querySelector(
".sp-selected-player"
);
this._viewer = this.element.querySelector(".sp-viewer");
this._loadingOverlay = this.element.querySelector(".sp-loading-overlay");
this._theaterBtn = this.element.querySelector(".sp-theater-btn");
this._theaterCloseBtn = this.element.querySelector(".sp-theater-close");
this._episodeBtn = this.element.querySelector(".sp-episode-btn");
this._currentPlayer = null;
this._playerInstances = new Map();
this._isTheaterMode = false;
// Инициализация системы подсказок
this._tooltip = new Tooltip();
// Обработчики событий для выпадающего списка
this._dropdownToggle.addEventListener("click", () => {
this._dropdown.classList.toggle("open");
});
// Закрытие выпадающего списка при клике вне его
document.addEventListener("click", (e) => {
if (!this._dropdown.contains(e.target)) {
this._dropdown.classList.remove("open");
}
});
// Обработчик для кнопки режима кинотеатра
this._theaterBtn.addEventListener("click", () => {
this.toggleTheaterMode();
});
// Обработчик для кнопки закрытия режима кинотеатра
this._theaterCloseBtn.addEventListener("click", () => {
this.toggleTheaterMode();
});
// Обработчик для кнопки добавления эпизода
this._episodeBtn.addEventListener("click", () => {
this.incrementEpisode();
});
// Обработчик для закрытия режима кинотеатра по клавише Esc
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && this._isTheaterMode) {
this.toggleTheaterMode();
}
});
}
toggleTheaterMode() {
this._isTheaterMode = !this._isTheaterMode;
if (this._isTheaterMode) {
this._wrapper.classList.add("theater-mode");
// Сохраняем текущую позицию прокрутки
this._scrollPosition = window.pageYOffset;
// Блокируем прокрутку страницы
document.body.style.overflow = "hidden";
document.body.style.position = "fixed";
document.body.style.top = `-${this._scrollPosition}px`;
document.body.style.width = "100%";
} else {
this._wrapper.classList.remove("theater-mode");
// Восстанавливаем прокрутку страницы
document.body.style.overflow = "";
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";
window.scrollTo(0, this._scrollPosition);
}
}
incrementEpisode() {
// ИСПРАВЛЕНИЕ: Улучшенный поиск кнопки увеличения эпизода
// Пробуем несколько возможных селекторов для кнопки
let incrementButton = document.querySelector(".item-add.increment");
if (!incrementButton) {
incrementButton = document.querySelector(".b-user_rate .increment");
}
if (!incrementButton) {
incrementButton = document.querySelector(".b-add_to_list .increment");
}
if (incrementButton) {
// Кликаем по ней
incrementButton.click();
// Добавляем визуальную обратную связь
this._episodeBtn.style.background = "var(--sp-success)";
setTimeout(() => {
this._episodeBtn.style.background = "";
}, 500);
// ИСПРАВЛЕНИЕ: Увеличиваем задержку и добавляем несколько попыток обновления счетчика
// Обновляем счетчик серий с увеличенной задержкой
setTimeout(() => {
this.updateEpisodeCount();
}, 1000);
// Вторая попытка обновления счетчика
setTimeout(() => {
this.updateEpisodeCount();
}, 2000);
} else {
// Если кнопка не найдена, показываем ошибку
this._episodeBtn.style.background = "var(--sp-error)";
setTimeout(() => {
this._episodeBtn.style.background = "";
}, 500);
}
}
updateEpisodeCount() {
// ИСПРАВЛЕНИЕ: Улучшенный поиск элемента с количеством просмотренных серий
// Пробуем несколько возможных селекторов
let rateNumber = document.querySelector(".rate-number");
if (!rateNumber) {
rateNumber = document.querySelector(".b-user_rate .rate-number");
}
if (!rateNumber) {
rateNumber = document.querySelector(".b-add_to_list .rate-number");
}
if (!rateNumber) {
console.error(
"Не удалось найти элемент с количеством просмотренных серий"
);
return;
}
// Получаем текст из элемента
const rateText = rateNumber.textContent;
// Находим элемент счетчика в нашей кнопке
const episodeCount = this._episodeBtn.querySelector(".sp-episode-count");
if (!episodeCount) return;
// ИСПРАВЛЕНИЕ: Добавляем анимацию при обновлении счетчика
// Сохраняем старое значение для сравнения
const oldValue = episodeCount.textContent;
// Обновляем текст счетчика
episodeCount.textContent = rateText;
// Если значение изменилось, добавляем анимацию
if (oldValue !== rateText) {
episodeCount.style.transform = "scale(1.2)";
setTimeout(() => {
episodeCount.style.transform = "scale(1)";
}, 300);
}
}
async start(abort) {
// Очищаем предыдущий контейнер, если он существует
let existing = document.querySelector(".sp-outer-wrapper");
if (existing) existing.remove();
let before = document.querySelector(".b-db_entry");
if (before) before.after(this.element);
let entryText = document
.querySelector(".b-db_entry .b-user_rate")
?.getAttribute("data-entry");
if (!entryText) return;
let entry = JSON.parse(entryText);
if (!entry || typeof entry.id !== "number") return;
// Добавляем подсказки только к кнопкам
this._tooltip.attach(this._theaterBtn, "Театральный режим");
this._tooltip.attach(
this._theaterCloseBtn,
"Закрыть режим кинотеатра (Esc)"
);
this._tooltip.attach(this._episodeBtn, "Отметить серию как просмотренную");
// Обновляем счетчик серий
this.updateEpisodeCount();
// Создаем элементы для всех плееров в выпадающем списке
for (let factory of this._playerFactories) {
let item = document.createElement("div");
item.className = "sp-dropdown-item loading";
item.innerHTML = `
${factory.name}
<span class="sp-status-indicator loading"></span>
`;
item.dataset.playerName = factory.name;
this._dropdownMenu.appendChild(item);
}
// Загружаем Kodik немедленно и отображаем его
let kodikFactory = this._playerFactories.find((f) => f.name === "Kodik");
if (kodikFactory) {
try {
let kodikPlayer = await kodikFactory.create(entry.id, abort);
if (kodikPlayer) {
this._playerInstances.set("Kodik", kodikPlayer);
this.switchPlayer("Kodik", kodikPlayer);
// Обновляем элемент Kodik в выпадающем списке
let kodikItem = this._dropdownMenu.querySelector(
"[data-player-name='Kodik']"
);
if (kodikItem) {
kodikItem.classList.remove("loading");
kodikItem.classList.add("active");
kodikItem
.querySelector(".sp-status-indicator")
.classList.remove("loading");
kodikItem
.querySelector(".sp-status-indicator")
.classList.add("online");
kodikItem.addEventListener("click", () => {
this.switchPlayer("Kodik", kodikPlayer);
this._dropdown.classList.remove("open");
});
}
} else {
// Если Kodik не загрузился, убираем его из списка
let kodikItem = this._dropdownMenu.querySelector(
"[data-player-name='Kodik']"
);
if (kodikItem) {
kodikItem
.querySelector(".sp-status-indicator")
.classList.remove("loading");
kodikItem
.querySelector(".sp-status-indicator")
.classList.add("offline");
kodikItem.classList.remove("loading");
}
}
} catch (e) {
console.error(`Error in Kodik:`, e);
// Если Kodik не загрузился, убираем его из списка
let kodikItem = this._dropdownMenu.querySelector(
"[data-player-name='Kodik']"
);
if (kodikItem) {
kodikItem
.querySelector(".sp-status-indicator")
.classList.remove("loading");
kodikItem
.querySelector(".sp-status-indicator")
.classList.add("offline");
kodikItem.classList.remove("loading");
}
}
}
// Загружаем остальные плееры в фоновом режиме
for (let factory of this._playerFactories) {
if (factory.name === "Kodik") continue; // Пропускаем Kodik, уже загружен
let item = this._dropdownMenu.querySelector(
`[data-player-name='${factory.name}']`
);
if (!item) continue;
// Используем Promise без await, чтобы не блокировать выполнение
factory
.create(entry.id, abort)
.then((player) => {
item.classList.remove("loading");
if (!player) {
item
.querySelector(".sp-status-indicator")
.classList.remove("loading");
item.querySelector(".sp-status-indicator").classList.add("offline");
return;
}
this._playerInstances.set(factory.name, player);
item
.querySelector(".sp-status-indicator")
.classList.remove("loading");
item.querySelector(".sp-status-indicator").classList.add("online");
item.addEventListener("click", () => {
this.switchPlayer(factory.name, player);
this._dropdown.classList.remove("open");
});
})
.catch((e) => {
console.error(`Error in ${factory.name}:`, e);
item
.querySelector(".sp-status-indicator")
.classList.remove("loading");
item.querySelector(".sp-status-indicator").classList.add("offline");
item.classList.remove("loading");
});
}
}
switchPlayer(playerName, player) {
// Показываем индикатор загрузки
this._loadingOverlay.style.display = "flex";
// Удаляем текущий плеер из viewport
this._viewer.innerHTML = "";
if (this._currentPlayer) {
this._currentPlayer.dispose();
}
// Устанавливаем новый плеер
this._viewer.appendChild(player.element);
this._currentPlayer = player;
// Обновляем текст в выпадающем списке
this._selectedPlayerText.textContent = playerName;
// Обновляем активный элемент в выпадающем списке
for (let item of this._dropdownMenu.children) {
item.classList.toggle("active", item.dataset.playerName === playerName);
}
// Скрываем индикатор загрузки с небольшой задержкой для плавности
setTimeout(() => {
this._loadingOverlay.style.display = "none";
}, 500);
}
dispose() {
if (this._currentPlayer) {
this._currentPlayer.dispose();
this._currentPlayer = null;
}
this._playerInstances.clear();
// Уничтожаем систему подсказок
if (this._tooltip) {
this._tooltip.destroy();
}
this.element.remove();
// Восстанавливаем прокрутку страницы, если был режим кинотеатра
if (this._isTheaterMode) {
document.body.style.overflow = "";
document.body.style.position = "";
document.body.style.top = "";
document.body.style.width = "";
window.scrollTo(0, this._scrollPosition);
}
}
}
// Запуск Alloha Helper
async function startAllohaHelper() {
let hostnames = [
"beggins-as.pljjalgo.online",
"beggins-as.allarknow.online",
"beggins-as.algonoew.online",
];
if (!hostnames.includes(location.hostname)) return;
new MutationObserver((mutations) => {
for (let mutation of mutations) {
let target = mutation.target;
if (target.matches(".select__drop-item.active")) {
let event;
if (target.closest("[data-select='seasonType1']")) {
event = { event: "sp_season", season: +target.dataset.id };
} else if (target.closest("[data-select='episodeType1']")) {
event = { event: "sp_episode", episode: +target.dataset.id };
} else if (target.closest("[data-select='translationType1']")) {
event = {
event: "sp_translation",
translation: +target.dataset.id.match(/(?<=t)\d+/)[0],
};
}
if (event) parent.postMessage(JSON.stringify(event), "*");
}
}
}).observe(document, { subtree: true, attributeFilter: ["class"] });
}
// Запуск Shikiplayer с поддержкой Turbolinks
async function startShikiplayer() {
if (location.hostname !== "shikimori.one") return;
const kodikToken = "a0457eb45312af80bbb9f3fb33de3e93";
const kodikUid = "";
const collapsToken = "4c250f7ac0a8c8a658c789186b9a58a5";
let http = new GMHttp();
let kodikApi = new KodikApi(http, kodikToken);
let kinoboxApi = new KinoboxApi(http);
let collapsApi = new CollapsApi(http, collapsToken);
let factories = [
new KodikFactory(kodikUid, kodikApi),
// ИЗМЕНЕНО: AllohaFactory теперь использует Kinobox API вместо Alloha API
new AllohaFactory(kodikApi, kinoboxApi),
new TurboFactory(kodikApi, kinoboxApi),
new LumexFactory(kodikApi, kinoboxApi),
new VeoveoFactory(kodikApi, kinoboxApi),
new VibixFactory(kodikApi, kinoboxApi), // Добавлен новый плеер Vibix
new CollapsFactory(kodikApi, collapsApi),
];
let shikiplayer = null;
// Функция инициализации плеера
async function initializePlayer() {
if (shikiplayer) {
shikiplayer.dispose(); // Очищаем текущий плеер
}
shikiplayer = new Shikiplayer(factories);
await shikiplayer.start(new AbortController().signal);
}
// Первичный запуск
initializePlayer();
// Обработка события Turbolinks
document.addEventListener("turbolinks:load", initializePlayer);
}
void startAllohaHelper();
void startShikiplayer();