One‑click (and now fully automated) jump from RU anime sites → Shikimori → MyAnimeList. Fast, ad‑skipping, with UI‑lock overlay.
// ==UserScript==
// @name MAL Turbo Jump
// @namespace mal_jumper
// @version 2.1
// @description One‑click (and now fully automated) jump from RU anime sites → Shikimori → MyAnimeList. Fast, ad‑skipping, with UI‑lock overlay.
// @author Kotaytqee
// @match https://animego.me/*
// @match https://jut.su/*
// @match https://duckduckgo.com/*
// @match https://shikimori.one/*
// @icon https://www.google.com/s2/favicons?sz=64&domain=myanimelist.net
// @grant GM_openInTab
// @run-at document-idle
// @license MIT
// ==/UserScript==
(() => {
'use strict';
/*=============================================================
| CONSTANTS & SHORTCUTS
*===========================================================*/
const MAL_BLUE = '#2E51A2';
const SHIKI_FILTER = 'site:shikimori.one/animes'; // hop‑1
const MAL_FILTER = 'site:myanimelist.net'; // hop‑2
const log = (...m) => console.debug('[MAL]', ...m);
/*=============================================================
| STYLE & OVERLAY
*===========================================================*/
function injectStyles() {
if (document.getElementById('mal-styles')) return;
const s = document.createElement('style');
s.id = 'mal-styles';
s.textContent = `
.mal-btn {
background: ${MAL_BLUE};
color: #fff;
border: none;
padding: 5px 10px;
border-radius: 3px;
font-size: 14px;
font-family: inherit;
cursor: pointer;
transition: background-color 0.2s ease;
}
.mal-btn:hover {
background-color: #294693; /* чуть темнее для эффекта hover */
}
#mal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,.6);
display: flex;
align-items: center;
justify-content: center;
z-index: 999999;
color: #fff;
font: 20px/1.4 sans-serif;
pointer-events: all;
}
`;
document.head.appendChild(s);
}
/** Show blocking overlay with msg; returns remover fn */
function showOverlay (msg='Подождите…') {
injectStyles();
const o = document.createElement('div');
o.id = 'mal‑overlay';
o.textContent = `⏳ ${msg}`;
document.body.appendChild(o);
return () => o.remove();
}
const buildURL = (filter, title) =>
`https://duckduckgo.com/?q=${encodeURIComponent(`${filter} ${title}`)}`;
/*=============================================================
| animego / jut.su (RU source sites)
*===========================================================*/
const SOURCES = {
'animego.me': {
anchor: () => document.querySelector('.anime-title > div'),
title : () => document.querySelector('.anime-title h1')?.textContent?.trim()
},
'jut.su': {
anchor: () => document.querySelector('h1.header_video'),
title : () => {
const t = document.querySelector('h1.header_video span[itemprop="name"]')?.textContent || '';
return t.replace(/Смотреть\s*/i,'').replace(/\s*\d+\s*серия/i,'').trim();
},
center:true
}
};
/**
* Поиск прямой ссылки на MyAnimeList через публичный Shikimori API.
* @param {string} title — название аниме для поиска
* @returns {Promise<string|null>} — URL на myanimelist.net или null, если не найдено
*/
async function fetchMalByShikiApi(title) {
try {
// 1) Поиск anime-id по названию
const searchUrl = `https://shikimori.one/api/animes?search=${encodeURIComponent(title)}`;
const searchResp = await fetch(searchUrl, { headers: { 'User-Agent': 'MAL-Turbo-Jump/2.0' } });
if (!searchResp.ok) return null;
const list = await searchResp.json();
if (!Array.isArray(list) || list.length === 0) return null;
const animeId = list[0].id;
// 2) Запрос подробной информации с external_links
const infoUrl = `https://shikimori.one/api/animes/${animeId}`;
const infoResp = await fetch(infoUrl, { headers: { 'User-Agent': 'MAL-Turbo-Jump/2.0' } });
if (!infoResp.ok) return null;
const info = await infoResp.json();
// 3) Извлечение ссылки на MAL
if (!Array.isArray(info.external_links)) return null;
const malLinkObj = info.external_links.find(link =>
link.site.toLowerCase().includes('myanimelist')
);
return malLinkObj ? malLinkObj.url : null;
} catch (e) {
console.error('fetchMalByShikiApi error:', e);
return null;
}
}
function enhanceSourceSite () {
const key = Object.keys(SOURCES).find(k => location.hostname.endsWith(k));
if (!key) return;
const cfg = SOURCES[key];
const anchor = cfg.anchor();
const title = cfg.title();
if (!anchor || !title) {
log('title/anchor missing');
return;
}
injectStyles();
const btn = document.createElement('button');
btn.className = 'mal-btn';
btn.textContent = 'MAL';
btn.title = title;
if (cfg.center) {
btn.style.display = 'block';
btn.style.margin = '10px auto';
}
// Вот тут вешаем новый, «надстройочный» сценарий:
btn.onclick = async () => {
const removeOverlay = showOverlay('Поиск через Shikimori API…');
try {
const malUrl = await fetchMalByShikiApi(title);
removeOverlay();
if (malUrl) {
// Нашли прямой MAL-URL — открываем его
GM_openInTab(malUrl, { active: true });
} else {
// Не нашли — возвращаемся к старому сценарию через DuckDuckGo
const remove2 = showOverlay('Перевод на MAL (фоллбэк)…');
GM_openInTab(buildURL(MAL_FILTER, title), { active: true });
setTimeout(remove2, 2000);
}
} catch (err) {
removeOverlay();
console.error('Ошибка поиска по Shikimori API:', err);
const remove2 = showOverlay('Перевод на MAL (фоллбэк)…');
GM_openInTab(buildURL(MAL_FILTER, title), { active: true });
setTimeout(remove2, 2000);
}
};
anchor.appendChild(btn);
log('Button added on', key);
}
/*=============================================================
| Shikimori → auto hop to MAL
*===========================================================*/
function getJPTitle(){
const h1=document.querySelector('h1');
if(!h1) return null;
const parts=h1.textContent.split('/');
return parts[1]?.trim()||null;
}
function handleShikimori(){
const jp=getJPTitle();
if(!jp){log('JP title missing');return;}
injectStyles();
const h1=document.querySelector('h1');
const btn=document.createElement('button');
btn.className='mal‑btn';
btn.textContent='MAL';
btn.title=jp;
btn.style.marginLeft='10px';
btn.onclick=()=>GM_openInTab(buildURL(MAL_FILTER,jp),{active:true});
h1.appendChild(btn);
// auto‑redirect once per session
if(!sessionStorage.getItem('mal_autohop')){
sessionStorage.setItem('mal_autohop','1');
showOverlay('Перенаправляем на MAL…');
location.replace(buildURL(MAL_FILTER,jp));
}
log('Shikimori processed');
}
/*=============================================================
| DuckDuckGo (ad‑skip & fast redirect)
*===========================================================*/
function ctxFromQuery(){
const q=new URLSearchParams(location.search).get('q')||'';
if(q.includes(SHIKI_FILTER)) return {domain:'shikimori.one'};
if(q.includes(MAL_FILTER)) return {domain:'myanimelist.net'};
return null;
}
function tryRedirect(domain){
const items=[
...document.querySelectorAll('li[data-layout="organic"]'),
...document.querySelectorAll('#links .result')
];
for(const it of items){
if(it.querySelector('.badge--ad')) continue;
const a=it.querySelector('a[data-testid="result-title-a"],a.result__a');
if(a&&a.href.includes(domain)){
log('→',a.href);
showOverlay('Загружаем…');
location.href=a.href;
return true;
}
}
return false;
}
async function handleDDG(){
const ctx=ctxFromQuery();
if(!ctx) return;
log('DDG context',ctx.domain);
if(tryRedirect(ctx.domain)) return; // immediate DOM pass
const obsSel='ol.react-results--main,#links';
const ok=await new Promise(r=>{
const to=setTimeout(()=>r(false),4000); // shorter timeout
const obs=new MutationObserver(()=>{
if(tryRedirect(ctx.domain)){clearTimeout(to);obs.disconnect();r(true);} });
obs.observe(document.documentElement,{childList:true,subtree:true});
});
if(!ok) log('No redirect found');
}
/*=============================================================
| ROUTER
*===========================================================*/
if(location.hostname.endsWith('duckduckgo.com')) handleDDG();
else if(location.hostname.endsWith('shikimori.one')) handleShikimori();
else enhanceSourceSite();
})();
QingJ © 2025
镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址