Advanced Streaming | aniworld.to & s.to

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

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

// ==UserScript==
// @name         	Advanced Streaming | aniworld.to & s.to
// @name:de			Erweitertes Streaming | aniworld.to & s.to
// @namespace    	https://gf.qytechs.cn/users/928242
// @version      	3.3.9
// @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://gf.qytechs.cn/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
		}
	} )
}

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址