Advanced Streaming | aniworld.to & s.to

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

目前為 2023-03-06 提交的版本,檢視 最新版本

您需要先安裝使用者腳本管理器擴展,如 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.3.7
// @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/account/subscribed
// @match        	https://aniworld.to/anime/stream/*
// @match      		https://aniworld.to/animekalender*
// @match      		https://aniworld.to/animes*
// @match        	https://aniworld.to/account/subscribed
// @require         https://greasyfork.org/scripts/455253-kamikaze-script-utils/code/Kamikaze'%20Script%20Utils.js
// @grant        	none
// @license      	MIT
// ==/UserScript==


// # # # # # #
// 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,
	'MyAnimeList': true,
	'AmazonVideo': 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


// # # # # # #
// 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 */

const log = new Logger( "Advanced Streaming" );
let streamData = null;

( async () => {

	if ( enableFilterSeriesCalendar ) filterSeriesCalendar()

	if ( enableImprovedSearchBox ) improvedSearchBox()

	streamData = await getStreamData()

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

	fixAnimeTrailerWatchButton()


	// Styles

	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 ( hideSeenEpisodes ) {
		if ( streamData.currentEpisode === 0 ) return
		addGlobalStyle( `
            #stream > ul:nth-child(4) li .seen {
                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;
			  }
		` )
	}

} )();

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() {
	const container = await waitForElm( ".ContentContainerBox" )
	if ( !container ) return
	container.style = "display: none;"
	log.info( "Season suggestions hidden" )
}

async function closeMenuOnHoverLeave() {
	const menu = await waitForElm( ".dd" )
	const modal = await waitForElm( ".modal" )

	menu.addEventListener( 'mouseleave', () => {
		modal.style = "display:none"
	} )
}

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'] } )
		return
	}

	addGlobalStyle( `
	section.title,
	section.title .backdrop {
		height: ${ headerHeight + 20 }px;
	}`, true )
}

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: "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: "AmazonVideo" },
	]

	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: "AmazonVideo" },
		{ 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() {

	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
			alert( 'Last Episode and Last Season' )
		}
	}

	if ( !nextEpisode ) {
		alert( 'Episode not found' )
		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() {

	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 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.` )

		alert( `Saved ${ titles.length } subscribed series.` )

	} else {
		log.warn( "No subscribed series found." )
		alert( "No subscribed series found." )
	}

	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() {
	let doNewSearch = false

	const searchInput = await waitForElm( "input#serInput" )
	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)
		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
		}
	} )
}