您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Minimizing page elements to fit smaller screens and adding some usability improvements.
当前为
// ==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.8 // @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() .then( () => { log.info("enableFilterSeriesCalendar", enableFilterSeriesCalendar) } ) if ( enableImprovedSearchBox ) improvedSearchBox() .then( () => { log.info("enableFilterSeriesCalendar", enableFilterSeriesCalendar) } ) streamData = await getStreamData() // Features if ( enableShortWindowTitle ) shortWindowTitle() .then( () => { log.info("shortWindowTitle", shortWindowTitle) } ) if ( enableHideSeasonSuggestions ) hideSeasonSuggestions .then( () => { log.info("enableHideSeasonSuggestions", enableHideSeasonSuggestions) } ) if ( enableCloseMenuOnHoverLeave ) closeMenuOnHoverLeave .then( () => { log.info("enableCloseMenuOnHoverLeave", enableCloseMenuOnHoverLeave) } ) if ( enableAddTrailerSearchLink ) addTrailerSearchLink .then( () => { log.info("enableAddTrailerSearchLink", enableAddTrailerSearchLink) } ) if ( enableAddAnimeSearchBox ) addAnimeSearchBox .then( () => { log.info("enableAddAnimeSearchBox", enableAddAnimeSearchBox) } ) if ( enableAddSeriesSearchBox ) addSeriesSearchBox .then( () => { log.info("enableAddSeriesSearchBox", enableAddSeriesSearchBox) } ) if ( enableEpisodeNavButtons ) addEpisodeNavButtons .then( () => { log.info("enableEpisodeNavButtons", enableEpisodeNavButtons) } ) if ( enableAddCalendarSearch ) addCalendarSearch() .then( () => { log.info("enableAddCalendarSearch", enableAddCalendarSearch) } ) fixAnimeTrailerWatchButton() .then( () => { log.info("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 }` return Promise.resolve() } 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 } } ) }