// ==UserScript==
// @name Advanced Streaming | aniworld.to & s.to
// @name:de Erweitertes Streaming | aniworld.to & s.to
// @namespace https://greasyfork.org/users/928242
// @version 3.6.6
// @description Minimizing page elements to fit smaller screens and adding some usability improvements.
// @description:de Minimierung der Seitenelemente zur Anpassung an kleinere Bildschirme und Verbesserung der Benutzerfreundlichkeit.
// @author Kamikaze (https://github.com/Kamiikaze)
// @supportURL https://github.com/Kamiikaze/Tampermonkey/issues
// @iconURL https://s.to/favicon.ico
// @match https://s.to/serie/stream/*
// @match https://s.to/serienkalender*
// @match https://s.to/serien*
// @match https://s.to/genre*
// @match https://s.to/account/subscribed
// @match https://s.to/account/watchlist*
// @match https://aniworld.to/anime/stream/*
// @match https://aniworld.to/animekalender*
// @match https://aniworld.to/animes*
// @match https://aniworld.to/genre*
// @match https://aniworld.to/account/subscribed
// @match https://aniworld.to/account/watchlist*
// @require https://greasyfork.org/scripts/455253-kamikaze-script-utils/code/Kamikaze'%20Script%20Utils.js
// @require https://cdnjs.cloudflare.com/ajax/libs/toastify-js/1.12.0/toastify.min.js
// @resource toastifyCss https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css
// @license MIT
// @grant GM_getResourceText
// @grant GM_addStyle
// ==/UserScript==
// Load Toastify CSS
// # # # # # #
// CONFIG
// You can disable features by replacing the value true with false.
// # # # # # #
// Enables shorter Window Tab Title
// Example: S3E8 - Black Clover | AniWorld.to
const enableShortWindowTitle = true;
// Hides the section of Season Suggestions below the video
const enableHideSeasonSuggestions = true;
// Closing the dropdown menu when mouse leaves (fix the perma-open menu)
const enableCloseMenuOnHoverLeave = true;
// Adding a Link below "Watch Trailer" to search for it on YT (Because sometimes there is a Homepage linked to the Anime)
const enableAddTrailerSearchLink = true;
// Adding a small box at bottom left to search the Anime on sites like MyAnimeList, Crunchyroll & more
const enableAddAnimeSearchBox = true;
// Enable/Disable search providers by changing the value either to true or false
// If you want to add your own provider let me know
const animeSearchProviderList = {
Crunchyroll: false,
aniSearch: false,
AnimePlanet: false,
Kitsu: true,
MyAnimeList: true,
"Amazon Video": true,
};
// Adding a small box at bottom left to search the Series on sites like Amazon, Netflix & more
const enableAddSeriesSearchBox = true;
// Enable/Disable search providers by changing the value either to true or false
// If you want to add your own provider let me know
const seriesSearchProviderList = {
"Amazon Video": true,
Netflix: true,
};
// Adding a small button at the right corner of the video frame to get to the next episode
const enableEpisodeNavButtons = true;
// Allows filtering the Series Calendar by subscribed series
// To use this feature you need to go to https://s.to/account/subscribed and wait for the script to save the
// subscribed series. After that you can go to https://s.to/serienkalender and use the filter.
const enableFilterSeriesCalendar = true;
// Adds a link to search series in the release calendar
const enableAddCalendarSearch = true;
// Enable improved Search Box
// When pressing a key, search box will be automatically focused. Clicking the search box will select all input.
// By clicking outside the search box and pressing a key, the search box will be focused and cleared for new input.
const enableImprovedSearchBox = true;
// Enables Notebox (Beta)
// Allows you to save notes to each Series/Animes
const enableNoteBox = false;
// # # # # # #
// Styling
// Some adjustments to layout.
// You can disable features by replacing the value true with false.
// # # # # # #
// Set the height of the video player. (in pixel)
// Set to 0 to disabled it. Default: 480
const reducePlayerHeight = 150;
// Hides the text to show/edit the description of the episode below episode title
const hideDescriptionEdit = true;
// Hides the language box above the video player
const hideLanguageBox = true;
// Hides seen episodes (marked green) from the Episode-List (You can still see them in the season overview
const hideSeenEpisodes = true;
// Use Scrollbar for Episode-List (good for seasons with a large amount of episodes)
const useScrollbarForEpisodeList = true;
/*** DO NOT CHANGE BELOW ***/
/* global Logger getStreamData waitForElm addGlobalStyle searchSeries GM_getResourceText */
const log = new Logger("Advanced Streaming");
let streamData = null;
let streamDetails = null;
(async () => {
generateStyles();
await getSubscribedSeries();
hideSeen();
sortWatchlist();
if (enableFilterSeriesCalendar) filterSeriesCalendar();
if (enableImprovedSearchBox) improvedSearchBox();
streamData = await getStreamData();
// streamDetails = await getStreamDetails()
await toggleSubscribedSeries();
if (hideSeenEpisodes) {
if (streamData.currentEpisode !== 0) {
addGlobalStyle(`
#stream > ul:nth-child(4) li .seen {
display: none;
}
`);
}
}
/**
{
"host": "aniworld.to",
"title": "Komi Can’t Communicate",
"currentSeason": 2,
"seasonsCount": 2,
"currentEpisode": 1,
"episodesCount": 12,
"episodeTitle": {
"de": "Es ist nur der Winteranfang. Und mehr.",
"en": "It's just the arrival of winter. Plus more."
},
"hasMovies": false
}
**/
console.log("streamData:", streamData);
/**
{
"title": "Komi Can’t Communicate",
"seasonsCount": 2,
"episodesCount": 12,
"episodeTitle": {
"de": "Es ist nur der Winteranfang. Und mehr.",
"en": "It's just the arrival of winter. Plus more."
},
"hasMovies": false
}
**/
console.log("streamDetails:", streamDetails);
// Features
if (enableShortWindowTitle) shortWindowTitle();
if (enableHideSeasonSuggestions) hideSeasonSuggestions();
if (enableCloseMenuOnHoverLeave) closeMenuOnHoverLeave();
if (enableAddTrailerSearchLink) addTrailerSearchLink();
if (enableAddAnimeSearchBox) addAnimeSearchBox();
if (enableAddSeriesSearchBox) addSeriesSearchBox();
if (enableEpisodeNavButtons) addEpisodeNavButtons();
if (enableAddCalendarSearch) addCalendarSearch();
if (enableNoteBox) addNotesBox();
fixAnimeTrailerWatchButton();
})();
function hideSeen() {
const subscribedSeries = localStorage.subscribedSeries;
let animeList = document.querySelector(".seriesListContainer");
if (!animeList || !subscribedSeries) return;
animeList = animeList.children;
for (let i = 0; i < animeList.length; i++) {
let anime = animeList[i];
let title = anime.querySelector("h3")?.innerText;
if (subscribedSeries.includes(title)) {
log.debug(title, "found");
anime.querySelector("a").classList.add("subbed");
}
}
addGlobalStyle(`
.seriesListContainer a.subbed {filter: blur(1px) grayscale(1) opacity(0.5);}
.seriesListContainer a.subbed:hover {filter: unset;}
.seriesListContainer>div>a:hover h3 {white-space: break-spaces;}
`);
}
async function addNotesBox() {
//const container = await waitForElm("#series > section > div.container.row")
const container = document.querySelector(
"#series > section > div.container.row",
);
const notesVisible = localStorage.getItem(`notes-visible`) === "true";
console.error("notesVisible", notesVisible);
if (!container) return;
const notesEl = document.createElement("div");
notesEl.id = "notes-box";
notesEl.innerHTML = `
<div id="notes-toolbar">
<button id="notes-save">Save Notes</button>
<button id="notes-toggle">${notesVisible ? "Show" : "Hide"} Notes</button>
</div>
<textarea id="notes-text" placeholder="Save Notes for this Anime" class="${notesVisible ? "seen" : "hidden"}" ></textarea>
`;
container.append(notesEl);
addGlobalStyle(
`
#notes-box {
display: flex;
flex-direction: column;
position: relative;
width: 100%;
}
#notes-box.hidden {
display: none;
}
#notes-toolbar {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
padding-top: 10px;
}
#notes-toolbar > button {
font-size: 13px;
text-align: center;
cursor: pointer;
display: block;
color: #fff;
background: #637cf9;
border-radius: 3px;
padding: 10px;
font-weight: 600;
text-transform: uppercase;
margin-left: 10px;
}
`,
false,
);
const title = window.location.pathname.split("/")[3];
const notesText = document.getElementById("notes-text");
notesText.value = localStorage.getItem(`notes-${title}`);
const saveBtn = document.getElementById("notes-save");
saveBtn.addEventListener("click", () => {
localStorage.setItem(`notes-${title}`, notesText.value);
saveBtn.innerHTML = "Saved!";
saveBtn.style.backgroundColor = "green";
setTimeout(() => {
saveBtn.innerHTML = "Save Notes";
saveBtn.style.backgroundColor = "";
}, 2000);
notify("Notes saved");
});
const toggleBtn = document.getElementById("notes-toggle");
toggleBtn.addEventListener("click", () => {
const notes = document.getElementById("notes-text");
const hidden = notes.classList.toggle("hidden");
hidden
? (toggleBtn.innerHTML = "Show Notes")
: (toggleBtn.innerHTML = "Hide Notes");
localStorage.setItem(`notes-visible`, !hidden);
});
}
async function sortWatchlist() {
if (!window.location.pathname.includes("watchlist")) return;
const nav = await waitForElm(".seriesListNavigation");
nav.innerHTML = nav.innerHTML.replace("oder", "|");
nav.style = `{
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
gap: 10px;
}`;
const sortByGenre = document.createElement("a");
sortByGenre.href = "/account/watchlist/genre";
sortByGenre.innerText = "Genre";
nav.append(" | ");
nav.append(sortByGenre);
const sortOrder = window.location.pathname.split("/")[3];
if (!sortOrder) return;
const sortReset = document.createElement("a");
sortReset.href = "/account/watchlist";
sortReset.innerText = "Zurücksetzen";
sortReset.style = "color: #cd3e3e;";
nav.append(" | ");
nav.append(sortReset);
const sortTag = sortOrder === "genre" ? "small" : "h3";
// Select the container
const container = await waitForElm(".seriesListContainer");
// Convert HTMLCollection to an array
const elementArray = Array.from(container.children);
// Sort the array based on the text content of the h3 elements
elementArray.sort((a, b) => {
const titleA = a.querySelector(sortTag).textContent.trim();
const titleB = b.querySelector(sortTag).textContent.trim();
if (sortOrder === "asc" || sortOrder === "genre")
return titleA.localeCompare(titleB);
return titleB.localeCompare(titleA);
});
// Re-append the sorted elements to the container
elementArray.forEach((element) => container.appendChild(element));
}
function generateStyles() {
const toastifyCss = GM_getResourceText("toastifyCss");
GM_addStyle(toastifyCss);
if (reducePlayerHeight > 0) {
addGlobalStyle(`
.inSiteWebStream, .inSiteWebStream iframe {height: ${reducePlayerHeight}px; }
.hosterSiteTitle {padding: 5px 0 10px;}
`);
}
if (hideDescriptionEdit) {
addGlobalStyle(`
.descriptionSpoilerLink, .descriptionSpoilerPlaceholder,
.submitNewDescription, .submitNewTitle, .hosterSectionTitle {
display: none;
}
`);
}
if (hideLanguageBox) {
addGlobalStyle(`
.changeLanguageBox {
display: none;
}
`);
}
if (useScrollbarForEpisodeList) {
addGlobalStyle(`
#stream > ul:nth-child(4) {
overflow-x: auto;
display: flex;
flex-direction: row;
justify-content: flex-start;
flex-wrap: nowrap;
align-items: center;
}
#stream > ul:nth-child(4) li:nth-child(1) {
position: absolute;
}
#stream > ul:nth-child(4) > li:nth-child(2) {
margin-left: 119px;
}
/* ===== Scrollbar CSS ===== */
/* Firefox */
* {
scrollbar-height: auto;
scrollbar-color: #637cf9 #243743;
}
/* Chrome, Edge, and Safari */
#stream > ul:nth-child(4)::-webkit-scrollbar {
height: 10px;
}
#stream > ul:nth-child(4)::-webkit-scrollbar-track {
background: #243743;
}
#stream > ul:nth-child(4)::-webkit-scrollbar-thumb {
background-color: #637cf9;
border-radius: 10px;
border: 1px solid #ffffff;
}
`);
}
// Header Section and Backdrop Image size
addGlobalStyle(`
section.title {
min-height: 450px;
}
#series .backdrop {
height: 100%;
}
`);
// seasonEpisodesList
addGlobalStyle(`
.seasonEpisodesList .editFunctions a,
.seasonEpisodesList td:nth-child(4) a,
.seasonEpisodesList .editFunctions {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-items: center;
justify-content: center;
}
.seasonEpisodesList .editFunctions a .flag,
.seasonEpisodesList .editFunctions img.flag,
.seasonEpisodesList td:nth-child(4) a .icon {
margin-right: 2px;
}
.seasonEpisodesList>tbody>tr>td {
padding-right: 15px;
}
.seasonEpisodesList>tbody>tr>td:nth-child(1) {
min-width: 110px;
}
`);
}
function shortWindowTitle() {
if (!streamData.title) return;
let pageTitle = "";
if (streamData.currentSeason > 0) pageTitle += "S" + streamData.currentSeason;
if (streamData.currentEpisode > 0)
pageTitle += "E" + streamData.currentEpisode;
window.document.title = `${pageTitle.length > 1 ? pageTitle + " - " : ""}${streamData.title} | ${streamData.host}`;
}
async function hideSeasonSuggestions() {
if (!window.location.pathname.includes("episode")) return;
const container = await waitForElm(".ContentContainerBox");
if (!container) return;
container.style = "display: none;";
log.info("Season suggestions hidden");
}
async function closeMenuOnHoverLeave() {
let menu = await waitForElm(".dd");
menu.replaceWith(menu.cloneNode(true));
menu = await waitForElm(".dd");
const modal = await waitForElm(".modal");
menu.addEventListener("mouseout", () => {
modal.style = "display:none";
});
menu.addEventListener("mouseover", () => {
modal.style = "display:block";
});
}
async function addTrailerSearchLink() {
const seriesTitle = streamData.title;
const trailerBoxEl = await waitForElm(".add-series .collections");
const ytSearchLink = "https://www.youtube.com/results?search_query=";
const searchTrailerEl = document.createElement("li");
searchTrailerEl.classList.add(
"col-md-12",
"col-sm-12",
"col-xs-6",
"buttonAction",
);
searchTrailerEl.innerHTML = `
<div title="Deutschen Trailer von ${seriesTitle} bei YouTube suchen." itemprop="trailer" itemscope="" itemtype="http://schema.org/VideoObject">
<a itemprop="url" target="_blank" href="${ytSearchLink + seriesTitle} Trailer Deutsch"><i class="fas fa-external-link-alt"></i><span class="collection-name">Trailer suchen</span></a>
<meta itemprop="name" content="${seriesTitle} Trailer">
<meta itemprop="description" content="Nach Offiziellen Trailer der TV-Serie ${seriesTitle} bei YouTube suchen.">
<meta itemprop="thumbnailUrl" content="https://zrt5351b7er9.static-webarchive.org/img/facebook.jpg">
</div>`;
increaseHeaderSize();
addLinkToList(trailerBoxEl, searchTrailerEl);
}
async function addCalendarSearch() {
const seriesTitle = streamData.title;
const trailerBoxEl = await waitForElm(".add-series .collections");
const calendarUrl = (() => {
if (getStreamPageLocation().host === "s.to") {
return "https://s.to/serienkalender?q=" + seriesTitle;
} else if (getStreamPageLocation().host === "aniworld.to") {
return "https://aniworld.to/animekalender?q=" + seriesTitle;
} else {
log.error("Host not supported");
}
})();
const searchCalendarEl = document.createElement("li");
searchCalendarEl.classList.add(
"col-md-12",
"col-sm-12",
"col-xs-6",
"buttonAction",
);
searchCalendarEl.innerHTML = `
<div title="Suche ${seriesTitle} im Release Kalender." itemprop="trailer" itemscope="" itemtype="http://schema.org/VideoObject">
<a itemprop="url" target="_blank" href="${calendarUrl}"><i class="fas fa-external-link-alt"></i><span class="collection-name">Im Kalender suchen</span></a>
<meta itemprop="name" content="${seriesTitle} Trailer">
<meta itemprop="description" content="Suche ${seriesTitle} im Release Kalender.">
<meta itemprop="thumbnailUrl" content="https://zrt5351b7er9.static-webarchive.org/img/facebook.jpg">
</div>`;
increaseHeaderSize();
addLinkToList(trailerBoxEl, searchCalendarEl);
}
async function fixAnimeTrailerWatchButton() {
const seriesTitle = streamData.title;
const watchButton = await waitForElm(".trailerButton");
if (!watchButton) return;
watchButton.style.display = "none";
if (!watchButton) return;
const trailerBoxEl = await waitForElm(".add-series .collections");
const watchTrailerPlaceholder = trailerBoxEl.querySelector(`li:nth-child(3)`);
watchTrailerPlaceholder.removeChild(watchTrailerPlaceholder.children[0]);
const watchTrailerEl = document.createElement("div");
watchTrailerEl.innerHTML = `
<div title="Trailer von ${seriesTitle} ansehen." itemprop="trailer" itemscope="" itemtype="http://schema.org/VideoObject">
<a itemprop="url" target="_blank" href="${watchButton.href}"><i class="fas fa-external-link-alt"></i><span class="collection-name">Anime-Trailer</span></a>
<meta itemprop="name" content="${seriesTitle} Trailer">
<meta itemprop="description" content="Offiziellen Trailer der TV-Serie ${seriesTitle} jetzt ansehen.">
<meta itemprop="thumbnailUrl" content="https://zrt5351b7er9.static-webarchive.org/img/facebook.jpg">
</div>`;
watchTrailerPlaceholder.append(watchTrailerEl);
}
function addLinkToList(parent, el) {
if (!parent) return;
const beforeElement = parent.querySelector(
`li:nth-child(${parent.childElementCount})`,
);
parent.insertBefore(el, beforeElement);
}
async function increaseHeaderSize() {
/**
* @type {HTMLElement}
*/
const header = await waitForElm("section.title");
if (!header) return;
const headerHeight = header.offsetHeight;
if (headerHeight === 0) {
log.debug("Header is not visible. Waiting for header to be visible");
const observer = new MutationObserver(() => {
if (header.offsetHeight > 0) {
log.info("Header is visible. Increasing Header height");
setTimeout(() => {
increaseHeaderSize();
}, 500);
observer.disconnect();
}
});
observer.observe(header, { attributes: true, attributeFilter: ["style"] });
}
}
async function addAnimeSearchBox() {
if (window.location.hostname !== "aniworld.to") return;
const rightColEl = await waitForElm(".add-series");
const seriesTitel = streamData.title;
const searchBoxEl = document.createElement("div");
searchBoxEl.classList.add("anime-search");
const searchBoxTitel = document.createElement("p");
searchBoxTitel.innerText = "Anime suchen auf:";
if (!rightColEl) return;
rightColEl.append(searchBoxEl);
searchBoxEl.append(searchBoxTitel);
const sites = [
{
domain: "crunchyroll.com",
searchUrl: "https://www.crunchyroll.com/de/search?q=#TITEL#",
name: "Crunchyroll",
},
{
domain: "anisearch.de",
searchUrl: "https://www.anisearch.de/anime/index?text=#TITEL#",
name: "aniSearch",
},
{
domain: "anime-planet.com",
searchUrl: "https://www.anime-planet.com/anime/all?name=#TITEL#",
name: "AnimePlanet",
},
{
domain: "kitsu.io",
searchUrl: "https://kitsu.io/anime?text=#TITEL#",
name: "Kitsu",
},
{
domain: "myanimelist.net",
searchUrl: "https://myanimelist.net/anime.php?q=#TITEL#&cat=anime",
name: "MyAnimeList",
},
{
domain: "amazon.de",
searchUrl: "https://www.amazon.de/s?k=#TITEL#&i=instant-video",
name: "Amazon Video",
},
];
for (let i = 0; i < sites.length; i++) {
const site = sites[i];
if (animeSearchProviderList[site.name]) {
const siteElement = document.createElement("a");
siteElement.classList.add("sites");
siteElement.target = "_blank";
siteElement.href = site.searchUrl.replace("#TITEL#", seriesTitel);
siteElement.innerHTML =
`<img src="https://www.google.com/s2/favicons?sz=64&domain=${site.domain}" alt='${site.name} Logo Icon' />` +
site.name;
searchBoxEl.append(siteElement);
}
}
}
async function addSeriesSearchBox() {
if (window.location.hostname !== "s.to") return;
const rightColEl = await waitForElm(".add-series");
const seriesTitel = streamData.title;
const searchBoxEl = document.createElement("div");
searchBoxEl.classList.add("anime-search");
const searchBoxTitel = document.createElement("p");
searchBoxTitel.innerText = "Serie suchen auf:";
rightColEl.append(searchBoxEl);
searchBoxEl.append(searchBoxTitel);
const sites = [
{
domain: "amazon.de",
searchUrl: "https://www.amazon.de/s?k=#TITEL#&i=instant-video",
name: "Amazon Video",
},
{
domain: "netflix.com",
searchUrl: "https://www.netflix.com/search?q=#TITEL#",
name: "Netflix",
},
];
for (let i = 0; i < sites.length; i++) {
const site = sites[i];
if (seriesSearchProviderList[site.name]) {
const siteElement = document.createElement("a");
siteElement.classList.add("sites");
siteElement.target = "_blank";
siteElement.href = site.searchUrl.replace("#TITEL#", seriesTitel);
siteElement.innerHTML =
`<img src="https://www.google.com/s2/favicons?sz=64&domain=${site.domain}" alt='${site.name} Logo Icon' />` +
site.name;
searchBoxEl.append(siteElement);
}
}
}
addGlobalStyle(`
.anime-search {
display: flex;
flex-direction: column;
flex-wrap: nowrap;
margin: 15px 5px;
background: #313d4f;
padding: 15px;
border-radius: 3px;
width: fit-content;
position: fixed;
left: 0;
bottom: -8px;
z-index: 99;
}
.anime-search .sites {
padding: 5px 0;
}
.anime-search .sites img {
max-width: 32px;
width: 16px;
margin-right: 5px;
border-radius: 16px;
}
`);
async function addEpisodeNavButtons() {
if (!window.location.pathname.includes("episode")) return;
const episodeControls = document.createElement("div");
episodeControls.id = "episodeControls";
const nextBtn = document.createElement("button");
nextBtn.classList.add("nextBtn");
nextBtn.innerText = "Next";
const currentSeason = streamData.currentSeason;
const currentEpisode = streamData.currentEpisode;
const maxSeasons = streamData.seasonsCount;
const maxEpisodes = streamData.episodesCount;
nextBtn.addEventListener("click", function () {
nextEpisode(currentSeason, currentEpisode, maxSeasons, maxEpisodes);
});
episodeControls.append(nextBtn);
const videoContainer = await waitForElm(".hosterSiteVideo");
videoContainer.insertBefore(
episodeControls,
videoContainer.querySelector(".inSiteWebStream"),
);
}
function nextEpisode(currSeason, currEpisode, maxSeasons, maxEpisodes) {
let nextEpisode = currEpisode + 1;
let nextSeason = currSeason;
log.debug({
currSeason,
currEpisode,
maxSeasons,
maxEpisodes,
nextEpisode,
nextSeason,
});
if (nextEpisode <= maxEpisodes) {
log.info("Next Episode", nextEpisode);
}
if (nextEpisode > maxEpisodes) {
nextSeason++;
if (nextSeason <= maxSeasons) {
log.info("Next Season", nextSeason);
nextEpisode = 1;
log.info("Next Episode", nextEpisode);
}
if (nextSeason > maxSeasons) {
nextEpisode = false;
notify("Last Episode of Last Season", undefined, "error");
}
}
if (!nextEpisode) {
notify("Episode not found", undefined, "error");
return;
}
window.location.pathname =
window.location.pathname.split("/").slice(0, 4).join("/") +
`/staffel-${nextSeason}/episode-${nextEpisode}`;
}
addGlobalStyle(
`
#episodeControls {
width: 100%;
height: 50px;
display: flex;
flex-direction: row;
flex-wrap: nowrap;
align-content: center;
justify-content: flex-end;
align-items: center;
margin: 10px 0;
}
#episodeControls button {
width: 120px;
height: fit-content;
position: relative;
padding: 10px 20px;
background: #4160f9;
color: #fff;
font-size: 13px;
border: none;
border-radius: 6px;
cursor: pointer;
}
.nextBtn::after {
content: ">";
padding-left: 10px;
}
`,
false,
);
async function filterSeriesCalendar() {
if (!window.location.pathname.includes("kalender")) return;
log.info("Calendar Filter enabled");
await getSubscribedSeries();
let onlySubbedEpisodes = false;
const container = await waitForElm("#seriesContainer");
if (!container) throw new Error("Could not find seriesContainer");
const filterToggleContainer = document.createElement("div");
filterToggleContainer.id = "filterToggleContainer";
const filterToggle = document.createElement("button");
filterToggle.innerText = "Zeige nur Abonnierte Serien";
filterToggle.id = "filterToggleButton";
filterToggle.classList.add("button", "blue", "small");
filterToggle.addEventListener("click", function () {
toggleAiringEpisodes()
.then(() => {
onlySubbedEpisodes = !onlySubbedEpisodes;
filterToggle.innerText = onlySubbedEpisodes
? "Zeige alle Serien"
: "Zeige nur Abonnierte Serien";
})
.catch((error) => {
log.error(`An error occurred while toggling airing episodes: ${error}`);
});
});
filterToggleContainer.prepend(filterToggle);
container.prepend(filterToggleContainer);
}
async function toggleSubscribedSeries() {
const subButton = await waitForElm(".series-add ul > li:nth-child(1)");
if (!subButton) return;
subButton.addEventListener("click", () => {
const isSubbed = subButton.classList.contains("true");
const subscribesSeries = JSON.parse(
localStorage.getItem("subscribedSeries"),
);
const title = streamData.title.trim();
if (isSubbed) {
const index = subscribesSeries.indexOf(title);
if (index === -1) return;
subscribesSeries.splice(index, 1);
} else {
subscribesSeries.push(title);
}
localStorage.setItem("subscribedSeries", JSON.stringify(subscribesSeries));
});
}
async function getSubscribedSeries() {
if (!window.location.href.includes("subscribed")) return;
log.info("Getting subscribed series...");
const container = await waitForElm(".seriesListContainer");
if (!container) throw new Error("Could not find seriesListContainer");
const subscsribedTitles = container.querySelectorAll("h3");
const titles = Array.from(subscsribedTitles).map(
(title) => title.textContent?.trim() || "",
);
if (titles.length > 0) {
log.debug(`Found ${titles.length} subscribed series.`);
localStorage.setItem("subscribedSeries", JSON.stringify(titles));
log.info(`Saved ${titles.length} subscribed series.`);
notify(`Saved ${titles.length} subscribed series.`);
} else {
log.warn("No subscribed series found.");
notify("No subscribed series found.", undefined, "error");
}
return titles;
}
async function toggleAiringEpisodes() {
log.info("Toggling airing episodes...");
const subscribedSeries = localStorage.getItem("subscribedSeries");
log.info(`Subscribed Series: ${subscribedSeries}`);
if (!subscribedSeries || subscribedSeries.length === 0) {
log.warn("No subscribed series found.");
alert(`
No subscribed series found.
To use this feature you need to go to:
https://s.to/account/subscribed
and wait for the script to save the subscribed series. After that you can come back and use the filter.`);
return;
}
const containers = document.querySelectorAll(".seriesListContainer");
if (!containers) throw new Error("Could not find seriesListContainer");
log.debug(`Found ${containers.length} containers`);
containers.forEach((container) => {
const episodes = container.querySelectorAll("div");
log.debug(`Found ${episodes.length} episodes`);
episodes.forEach((episode) => {
const title = episode.querySelector("h3")?.innerText;
if (title && !subscribedSeries?.includes(title)) {
const isHidden = episode.style.display === "none";
log.debug(
`Hiding episode ${title} (${isHidden ? "hidden" : "visible"})`,
);
if (!isHidden) {
episode.style.display = "none";
} else {
episode.style.display = "block";
}
}
});
});
}
addGlobalStyle(
`
div#filterToggleContainer {
display: flex;
flex-wrap: nowrap;
justify-content: center;
align-items: center;
padding: 15px 0 0;
}
`,
false,
);
async function improvedSearchBox() {
if (
!window.location.pathname.includes("animes") &&
!window.location.pathname.includes("serien") &&
!window.location.pathname.includes("kalender")
)
return;
let doNewSearch = false;
const searchInput = await waitForElm("input#serInput");
if (!searchInput) return;
searchInput.focus();
if (window.location.search.includes("q=")) {
const searchQuery = window.location.search.split("q=")[1];
log.info(`Found search query: ${searchQuery}`);
searchInput.value = decodeURI(searchQuery)
.replaceAll("+", " ")
.replaceAll("’", "'");
searchSeries(); // global function
}
document.addEventListener("keypress", () => {
searchInput.focus();
if (doNewSearch) {
searchInput.value = "";
doNewSearch = false;
}
});
searchInput.addEventListener("click", () => {
searchInput.select();
});
document.addEventListener("focusout", function (event) {
if (event.target.id === "serInput") {
doNewSearch = true;
}
});
// Auto-Open Anime if only 1 result
const genreList = document.getElementById("seriesContainer");
const activeGenres = Array.from(genreList.children).filter(
(g) => g.style.display !== "none",
);
console.log(activeGenres);
if (activeGenres.length === 1) {
const activeSeries = Array.from(
activeGenres[0].querySelector("ul").children,
).filter((g) => g.style.display !== "none");
console.log(activeSeries);
if (activeSeries.length === 1) {
activeSeries[0].querySelector("a").click();
}
}
}