// ==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.2
// @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 = {
'AmazonVideo': 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
console.log("watchlist")
const nav = await waitForElm(".seriesListNavigation")
const sortByGenre = document.createElement("a")
sortByGenre.href = "/account/watchlist/genre"
sortByGenre.innerText = "Genre"
nav.append(" oder ")
nav.append(sortByGenre)
const sortOrder = window.location.pathname.split("/")[3]
if (!sortOrder) return
console.log("sortOrder", sortOrder)
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() {
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")
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) {
const beforeElement = parent.querySelector(`li:nth-child(${parent.childElementCount})`);
parent.insertBefore(el, beforeElement)
}
async function increaseHeaderSize() {
/**
* @type {HTMLElement}
*/
const header = await waitForElm("section.title")
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:"
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")) 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()
}
}
}