Advanced Streaming | aniworld.to & s.to

Minimizing page elements to fit smaller screens and adding some usability improvements.

目前為 2024-05-23 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 Tampermonkey,才能安裝此腳本

您需要先安裝使用者腳本管理器擴充功能後才能安裝該腳本。

(我已經安裝了使用者腳本管理器,讓我安裝!)

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展,比如 Stylus,才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

你需要先安裝一款使用者樣式管理器擴展後才能安裝此樣式

(我已經安裝了使用者樣式管理器,讓我安裝!)

// ==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()
        }
    }
}