This Script allows You to play Openings and Endings directly on AniList
// ==UserScript== // @name Anime Songs - AniList Player // @namespace Openings and Endings Player // @version 2.2.1 // @description This Script allows You to play Openings and Endings directly on AniList // @author NurarihyonMaou // @match https://anilist.co/* // @icon https://www.google.com/s2/favicons?sz=64&domain=anilist.co // @grant GM_xmlhttpRequest // @grant GM_setValue // @grant GM_getValue // @grant GM_addStyle // @require http://code.jquery.com/jquery-3.5.1.min.js // ==/UserScript== const $ = window.jQuery; let result; let openings, endings; let audioUrls = ['https://animethemes.moe/audio/', 'https://a.animethemes.moe/', 'https://beta.animethemes.moe/audio/']; let videoUrls = ['https://animethemes.moe/video/', 'https://v.animethemes.moe/', 'https://beta.animethemes.moe/video/']; let urlsIndex = GM_getValue("urlsIndex") ?? 1; //let audioURLAddon = "-NCBD1080"; let url = "https://graphql.anilist.co"; let AnimeID; let AniID = { id: parseInt(window.location.pathname.split("/")[2]) }; const VideoIcon = `<svg class="svg-inline--fa fa-solid fa-video"aria-hidden="true" data-fa-processed="" data-prefix="fa" data-icon="video" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 576 512"><path d="M0 128C0 92.7 28.7 64 64 64H320c35.3 0 64 28.7 64 64V384c0 35.3-28.7 64-64 64H64c-35.3 0-64-28.7-64-64V128zM559.1 99.8c10.4 5.6 16.9 16.4 16.9 28.2V384c0 11.8-6.5 22.6-16.9 28.2s-23 5-32.9-1.6l-96-64L416 337.1V320 192 174.9l14.2-9.5 96-64c9.8-6.5 22.4-7.2 32.9-1.6z"/></svg>`; const MusicIcon = `<svg class="svg-inline--fa fa-solid fa-music"aria-hidden="true" data-fa-processed="" data-prefix="fa" data-icon="music" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path d="M499.1 6.3c8.1 6 12.9 15.6 12.9 25.7v72V368c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V147L192 223.8V432c0 44.2-43 80-96 80s-96-35.8-96-80s43-80 96-80c11.2 0 22 1.6 32 4.6V200 128c0-14.1 9.3-26.6 22.8-30.7l320-96c9.7-2.9 20.2-1.1 28.3 5z"/></svg>`; const ToggleStyle = `.AudioMode { width:45px; height:15px; background-color: grey; position: relative; transition: ease-in-out 0.5s; display: flex; justify-content: center; align-items: center; } .circle { border-radius:50%; width: 30px; max-height: 30px; color:red; z-index: 9999; position: absolute; left:0; transition: ease-in-out 0.5s; } .circle svg { transition: ease-in-out 0.5s; color:red; } .active{ left:50%; } `; GM_addStyle(ToggleStyle); let AudioMode = GM_getValue("AudioMode") ?? false; async function getAID() { AniID = { id: parseInt(window.location.pathname.split("/")[2]) }; let query = ` query ($id: Int) { Media (id: $id, type: ANIME) { idMal } } `; let options = { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json", }, body: JSON.stringify({ query: query, variables: AniID, }), }; await fetch(url, options) .then(handleResponse) .then(handleData) .catch(handleError); function handleResponse(response) { return response.json().then(function (json) { return response.ok ? json : Promise.reject(json); }); } function handleData(data) { AnimeID = data.data.Media.idMal; } function handleError(error) { console.error(error); } } async function returnThemes() { await getAID(); let response = $.ajax({ url: `https://api.jikan.moe/v4/anime/${AnimeID}/themes`, method: "GET", success: function (data) { return data; }, error: function (error) { alert("Something went wrong.\n" + error.errorMessage); }, }); return response; } function appendThemes() { if ($("body").find("div.openings").length == 0) $("body div.characters").after( "<div class='openings'><h2>Openings</h2></div>" ); else { $("body div.characters").after($("body").find("div.openings")); $("body").find("div.openings").html("<h2>Openings</h2>"); } if ($("body").find("div.endings").length == 0) $("body div.openings").after("<div class='endings'><h2>Endings</h2></div>"); else { $("body div.openings").after($("body").find("div.endings")); $("body").find("div.endings").html("<h2>Endings</h2>"); } themes.data.openings.forEach((opening) => { $("body").find("div.openings").append(`<ul class='tag'>${opening}</ul>`); }); themes.data.endings.forEach((ending) => { $("body").find("div.endings").append(`<ul class='tag'>${ending}</ul>`); }); if (parseInt(AniID.id) != 112323) { if ($("body").find("div.AudioMode").length == 0) $("body div.openings").before( `<input id="urlsIndex" type="range" value="${urlsIndex}" min="0" max="2"/> URL Slider - use this when the Player(s) don't work<div class="AudioMode"><button class="${ !AudioMode ? "circle" : "circle active" }">${AudioMode ? MusicIcon : VideoIcon}</button></div></br>` ); else $("body div.openings").before($("body").find("div.AudioMode")); } } $("body").on("click", "div.AudioMode", function () { AudioMode = !AudioMode; if (AudioMode) { $(this).children().addClass("active"); $(this).children().html(MusicIcon); } else { $(this).children().removeClass("active"); $(this).children().html(VideoIcon); } GM_setValue("AudioMode", AudioMode); $("body").find(".AnimeThemesPlayer").remove(); loadVideos(); }); $("body").on("change", "input#urlsIndex", function () { urlsIndex = document.getElementById('urlsIndex').value; GM_setValue("urlsIndex", urlsIndex); $("body").find(".AnimeThemesPlayer").remove(); loadVideos(); }); function loadVideos() { let Song = 0; (function init() { openings = document.getElementsByClassName("openings"); endings = document.getElementsByClassName("endings"); if (openings.length > 0 || endings.length > 0) { GM_xmlhttpRequest({ method: "GET", url: "https://api.animethemes.moe/anime?filter[has]=resources&filter[site]=MyAnimeList&filter[external_id]=" + AnimeID + "&include=animethemes.animethemeentries.videos,animethemes.song.artists", data: AnimeID, headers: { "Content-Type": "application/json" }, onload: function (response) { result = JSON.parse(response.responseText); openings = openings.length > 0 ? openings : endings; if ($("body .AnimeThemesPlayer").length == 0) { $(openings).append( AudioMode ? "<audio style='display: none' class='AnimeThemesPlayer' width='500px' height='300' src=''controls autoplay/>" : "<video style='display: none' class='AnimeThemesPlayer' width='500px' height='300' src='' allowfullscreen controls autoplay/>" ); } $("body div.IMOE").remove(); if ($("body").find("a#AnimeThemesLink").length == 0) $("body") .find("div.openings") .before( `<a id="AnimeThemesLink" href="https://animethemes.moe/anime/${result.anime[0].slug}">AnimeThemes - Videos Source</a></br></br>` ); for (Song; Song < result.anime[0].animethemes.length; Song++) { let CurrSongMatch = false; $("div.openings, div.endings") .children(".tag") .filter(function (x) { let entriesCount = $("div.openings, div.endings").children( ".tag" ).length; let currSong = $(this); let safetyIndicator = result.anime[0].animethemes[Song] .animethemeentries[0].nsfw ? "NSFW" : ""; safetyIndicator += safetyIndicator == "NSFW" ? result.anime[0].animethemes[Song].animethemeentries[0] .spoiler ? " && Spoilers " : " " : result.anime[0].animethemes[Song].animethemeentries[0] .spoiler ? "Spoilers " : " "; if ( $(currSong) .text() .toLowerCase() .split(" by ")[0] .replace(/\(([^)]+)\)/, "") .replace(/[“"”]+/g, "") .replace(/\#\d*:/g, "") .replace(/\d*\:/g, "") .replace(/[^\w\s]/gi, " ") .replace(/ /g, "") .trim() == result.anime[0].animethemes[Song].song.title .toLowerCase() .replace(/\(([^)]+)\)/, "") .replace(/\#\d*:/g, "") .replace(/[^\w\s]/gi, " ") .replace(/^0+/, "") .replace(/ /g, "") .trim() ) { CurrSongMatch = true; $(currSong).html( AudioMode ? "<span style='color: red;'>" + safetyIndicator + "</span><a href='"+audioUrls[urlsIndex]+ result.anime[0].animethemes[Song].animethemeentries[0] .videos[0].filename + ".ogg'>" + $(currSong).text() + "</a>" : "<span style='color: red;'>" + safetyIndicator + "</span><a href='"+videoUrls[urlsIndex]+ result.anime[0].animethemes[Song].animethemeentries[0] .videos[0].basename + "'>" + $(currSong).text() + "</a>" ); } if (x == entriesCount - 1 && CurrSongMatch == false) { if ($("div.IMOE").length == 0) { if ($("div.endings").length > 0) { $("div.endings").after( '<div class="IMOE" style="margin-bottom: 30px;"><h2>Additional OPs and EDs</h2></div>' ); } else if ("div.openings".length > 0) { $("div.endings").after( '<div class="IMOE" style="margin-bottom: 30px;"><h2>Additional OPs and EDs</h2></div>' ); } else { $("div.characters").after( '<div class="IMOE" style="margin-bottom: 30px;"><h2>Additional OPs and EDs</h2></div>' ); } } let CurrentSongArtists = ""; for ( let Artists = 0; Artists < result.anime[0].animethemes[Song].song.artists.length; Artists++ ) { CurrentSongArtists += result.anime[0].animethemes[Song].song.artists[Artists] .name + " "; } if (CurrentSongArtists == "") CurrentSongArtists = "???"; $("div.IMOE").append( AudioMode ? "<span style='color: red;'>" + safetyIndicator + "</span><ul class='tag'><a href='"+audioUrls[urlsIndex]+ result.anime[0].animethemes[Song].animethemeentries[0] .videos[0].filename + ".ogg'>" + result.anime[0].animethemes[Song].song.title + " by " + CurrentSongArtists + "</a></ul>" : "<span style='color: red;'>" + safetyIndicator + "</span><ul class='tag'><a href='"+videoUrls[urlsIndex]+ result.anime[0].animethemes[Song].animethemeentries[0] .videos[0].basename + "'>" + result.anime[0].animethemes[Song].song.title + " by " + CurrentSongArtists + "</a></ul>" ); CurrentSongArtists = ""; } }); } }, onerror: function (error) { console.log(error); }, }); } else { setTimeout(init, 0); } })(); } function SetArifureta() { $("body").find(".AudioMode").remove(); $("body").find("#AnimeThemesLink").remove(); if ($("body iframe.AnimeThemesPlayer").length == 0) { $("body") .find("div.openings") .append( "<iframe style='display: none' class='AnimeThemesPlayer' width='500px' height='300' src='' allowfullscreen/>" ); } if ($("body").find("a#MyYTChannel").length == 0) $("body") .find("div.openings") .before( `<a id="MyYTChannel" href="https://www.youtube.com/c/NurarihyonMaou">My YT Channel</a></br></br>` ); $("div.openings") .children(".tag") .html( '<a href="https://www.youtube.com/embed/s0vm9JVjexY?autoplay=1">"Daylight" by MindaRyn</a>' ); $("div.endings") .children(".tag") .html( '<a href="https://www.youtube.com/embed/5aJWWk9B78E?autoplay=1">"Gedou Sanka (外道讃歌)" by FantasticYouth</a>' ); } let themes; async function loadThemesAndVideos() { themes = await returnThemes(); appendThemes(); if (parseInt(AniID.id) == 112323) SetArifureta(); else loadVideos(); } $(window).on("load", async function () { if (window.location.pathname.split("/")[1] === "anime") { await loadThemesAndVideos(); $("body").on("click", ".tag", function (e) { e.preventDefault(); if ($(this).children().length == 0) { alert( "Sadly, this OP/ED is Missing or there's a bug in the Script (Check Instruction)" ); } else if ( $("body .AnimeThemesPlayer").css("display") == "none" || $(this).children("a").attr("href") != $("body .AnimeThemesPlayer").attr("src") ) { $("body .AnimeThemesPlayer").css("display", "block"); $("body .AnimeThemesPlayer").attr( "src", $(this).children("a").attr("href") ); } else if ( $(this).children("a").attr("href") == $("body .AnimeThemesPlayer").attr("src") ) { $("body .AnimeThemesPlayer").css("display", "none"); $("body .AnimeThemesPlayer").attr("src", ""); } }); } }); let oldURL; (function () { let pushState = history.pushState; let replaceState = history.replaceState; history.pushState = function () { pushState.apply(history, arguments); window.dispatchEvent(new Event("pushstate")); window.dispatchEvent(new Event("locationchange")); }; history.replaceState = function () { replaceState.apply(history, arguments); window.dispatchEvent(new Event("replacestate")); window.dispatchEvent(new Event("locationchange")); }; window.addEventListener("popstate", function () { window.dispatchEvent(new Event("locationchange")); }); $(window).on("load", function () { GM_setValue("oldURL", $(location).attr("pathname").split("/")[1]); }); window.addEventListener("locationchange", function () { oldURL = GM_getValue("oldURL"); if ( oldURL != "anime" && $(location).attr("pathname").split("/")[1] == "anime" ) { GM_setValue("oldURL", $(location).attr("pathname").split("/")[1]); location.reload(); } else if ( oldURL == "anime" && $(location).attr("pathname").split("/")[1] == "anime" ) { loadThemesAndVideos(); } else { GM_setValue("oldURL", $(location).attr("pathname").split("/")[1]); } }); })();