Overview
Web Scraping Hub features a sophisticated video player system with adaptive theming, episode management, and responsive design that works seamlessly across devices. The player supports movies, series, and anime with specialized controls for each content type.
Player Architecture
Movie Player Single video playback with metadata display:
Full-screen optimized layout
Title and genre information
Synopsis display below player
Direct iframe embedding
Fuchsia pink theme
Series Player Multi-episode navigation system:
Season selector sidebar
Episode list with thumbnails
Previous/Next episode buttons
Current episode highlighting
Neon cyan theme
Anime Player Specialized anime viewing experience:
Season-based organization
Episode thumbnails
Sequential watching support
Progress tracking
Magenta pink theme
Adaptive Theming System
The player automatically adjusts its color scheme based on content type:
const getThemeColors = () => {
if ( isMovie ) {
return {
primary: 'fuchsia-pink' ,
primaryText: 'text-fuchsia-pink' ,
primaryBg: 'bg-fuchsia-pink' ,
border: 'border-fuchsia-pink' ,
hover: 'hover:bg-fuchsia-pink/20' ,
glow: 'text-glow-fuchsia-pink'
};
} else if ( isAnime ) {
return {
primary: 'neon-magenta' ,
primaryText: 'text-neon-magenta' ,
primaryBg: 'bg-neon-magenta' ,
border: 'border-neon-magenta' ,
hover: 'hover:bg-neon-magenta/20' ,
glow: 'text-glow-magenta-pink'
};
} else { // Series
return {
primary: 'neon-cyan' ,
primaryText: 'text-neon-cyan' ,
primaryBg: 'bg-neon-cyan' ,
border: 'border-neon-cyan' ,
hover: 'hover:bg-neon-cyan/20' ,
glow: 'text-glow-cyan'
};
}
};
Movie Theme Fuchsia pink accents with matching glows and borders
Series Theme Electric cyan for TV series content
Anime Theme Neon magenta for anime differentiation
Responsive Design
Orientation Detection
The player includes intelligent orientation handling:
const useOrientation = () => {
const [ isPortrait , setIsPortrait ] = useState ( false );
const [ isMobile , setIsMobile ] = useState ( false );
useEffect (() => {
const checkOrientation = () => {
const isMobileDevice = window . innerWidth <= 768 ;
const isPortraitMode = window . innerHeight > window . innerWidth ;
setIsMobile ( isMobileDevice );
setIsPortrait ( isPortraitMode );
};
checkOrientation ();
window . addEventListener ( 'resize' , checkOrientation );
window . addEventListener ( 'orientationchange' , checkOrientation );
}, []);
return { isPortrait , isMobile , isMobilePortrait: isMobile && isPortrait };
};
Desktop : Multi-column layout with sidebar for series/anime
Mobile Landscape : Full-screen player with minimal controls
Mobile Portrait : Vertical stacking with footer navigation
Tablet : Hybrid layout balancing content and controls
Layout Configurations
Device State Player Height Sidebar Controls Position Desktop Movie 50rem None Below player Desktop Series 40rem Right sidebar Below player Mobile Landscape 100vh Hidden Overlay Mobile Portrait Flex-1 Hidden Footer
Player Controls
Episode Navigation (Series/Anime)
Previous Episode
Button enabled when not on first episode of season
Next Episode
Button enabled when not on last episode of season
Exit Player
Returns to catalog homepage
< div className = "flex gap-3" >
{ ( isSeries || isAnime ) && getCurrentEpisodeIndex () > 0 && (
< button
onClick = { () => navigateToEpisode ( 'prev' ) }
disabled = { getCurrentEpisodeIndex () <= 0 }
className = "px-5 py-2 bg-dark-gray hover:bg-gray-800 rounded-full"
>
< ChevronLeft className = "h-4 w-4" />
Previous
</ button >
) }
{ ( isSeries || isAnime ) && getCurrentEpisodeIndex () < currentSeasonEpisodes . length - 1 && (
< button
onClick = { () => navigateToEpisode ( 'next' ) }
className = "px-5 py-2 bg-dark-gray hover:bg-gray-800 rounded-full"
>
Next
< ChevronRight className = "h-4 w-4" />
</ button >
) }
< button
onClick = { () => navigate ( '/' ) }
className = "px-5 py-2 bg-dark-gray rounded-full"
>
< X className = "h-4 w-4" />
Salir
</ button >
</ div >
Buttons feature rounded design with hover effects that transition from gray to the content-type theme color.
Keyboard Shortcuts
The player supports keyboard controls:
useEffect (() => {
const handleEscape = ( e : KeyboardEvent ) => {
if ( e . key === 'Escape' ) {
navigate ( '/' );
}
};
document . addEventListener ( 'keydown' , handleEscape );
return () => document . removeEventListener ( 'keydown' , handleEscape );
}, [ navigate ]);
Key Action Escape Exit player and return to catalog
Iframe Management
Video Source Handling
The player intelligently handles different video sources:
const videoUrl = isMovie
? ( finalMovieData ?. player_url || '' )
: ( currentEpisode ?. url || '' );
const { data : playerData } = usePlayerData (
isMovie ? '' : videoUrl
);
const iframeUrl = isMovie
? ( finalMovieData ?. player_url || '' )
: ( playerData ?. player_url || '' );
Movie Playback
Series/Anime Playback
Movies use direct iframe URLs from the movie data API:
URL extracted by iframe_extractor.py
Direct embedding without processing
Single source per movie
Series and anime process episode URLs through usePlayerData:
Episode URL extracted from series data
Processed by iframe extractor
Multiple sources per season
Ad Blocking Integration
The player includes client-side ad removal:
const cleanAdsInIframe = () => {
const iframe = iframeRef . current ;
if ( iframe && iframe . contentDocument ) {
const doc = iframe . contentDocument ;
const adSelectors = [
'.modal-content-vast' , '#skipCountdown' , 'video#adVideo' ,
'[id*="ad"]' , '[class*="ad"]' , '[id*="ads"]' ,
'[id*="banner"]' , '[id*="sponsor"]' , '[id*="promo"]'
];
adSelectors . forEach ( selector => {
doc . querySelectorAll ( selector ). forEach ( el => el . remove ());
});
}
};
Iframe ad cleaning only works for same-origin iframes due to browser security restrictions.
Season and Episode Management
Season Selector
Series and anime display a season selector sidebar:
{ seasons . length > 1 && (
< div className = "bg-dark-gray p-5 rounded-xl" >
< h3 className = "orbitron text-lg font-bold mb-3" > Temporadas </ h3 >
< div className = "flex flex-wrap gap-2" >
{ seasons . map (( season ) => (
< button
key = { season }
onClick = { () => handleSeasonChange ( season ) }
className = { `px-3 py-2 rounded-lg font-bold ${
selectedSeason === season
? ` ${ theme . primaryBg } ${ theme . glow } `
: 'bg-space-black hover:bg-gray-700'
} ` }
>
S { season }
</ button >
)) }
</ div >
</ div >
)}
Episode List Display
Episodes show with thumbnails and metadata:
{ currentSeasonEpisodes . slice ( 0 , 10 ). map (( episode : Episode ) => {
const isCurrentEpisode = currentEpisode &&
episode . season === currentEpisode . season &&
episode . episode === currentEpisode . episode ;
return (
< div className = "flex items-center gap-3 p-2 rounded cursor-pointer" >
< div className = "relative w-12 h-8 rounded-sm overflow-hidden" >
{ episode . image ? (
< img src = { episode . image } alt = { episode . title } loading = "lazy" />
) : (
< div className = "flex items-center justify-center bg-gray-700" >
< span > { episode . episode } </ span >
</ div >
) }
</ div >
< div >
< span > Episode { episode . episode } </ span >
{ episode . title && < span > { episode . title } </ span > }
</ div >
</ div >
);
})}
The episode list limits display to 10 episodes for performance, showing the current season’s episodes.
Title Banner
Dynamic title display with emoji indicators:
< div className = "bg-neon-cyan text-space-black text-center py-2 px-4 rounded-full font-bold" >
{ fullTitle } { isMovie ? '🎬' : isAnime ? '🌸' : '📺' }
</ div >
Comprehensive metadata below the player:
Primary Info
Content title
Episode number (if applicable)
Release year
Primary genre
Additional Info
All genres (up to 4 displayed)
Language (Latino)
Content type
Synopsis
< div className = "bg-dark-gray p-5 rounded-xl space-y-4" >
< h2 className = "text-neon-cyan" > { title } </ h2 >
< h1 className = "text-2xl md:text-3xl orbitron font-bold" >
{ isMovie ? 'Movie' : `Episode ${ currentEpisode . episode } ` }
</ h1 >
< p className = "text-gray-light text-sm" >
{ genres && genres . split ( ',' )[ 0 ] } • { year } • Latino
</ p >
{ /* Genre tags */ }
< div className = "flex flex-wrap gap-2" >
{ genres . split ( ',' ). slice ( 0 , 4 ). map (( genre , index ) => (
< span className = "px-3 py-1 bg-space-black rounded-full text-xs" >
{ genre . trim () }
</ span >
)) }
</ div >
{ /* Synopsis */ }
< p className = "text-gray-light text-sm leading-relaxed" >
{ sinopsis || 'Sinopsis no disponible' }
</ p >
</ div >
Data Caching Strategy
The player uses optimized hooks for data fetching:
const { data : finalMovieData } = useCachedMovieData (
isMovie ? ( slug || '' ) : '' ,
passedMovieData
);
const { data : seriesData } = useCachedSeriesData (
isSeries ? ( seriesSlug || '' ) : '' ,
passedSeriesData
);
const { data : animeData } = useCachedAnimeData (
isAnime ? ( seriesSlug || '' ) : '' ,
passedAnimeData
);
Cache Optimization Benefits
Pre-cached data : Uses data passed from catalog navigation
Conditional fetching : Only fetches if data not available
Background updates : Refreshes stale data automatically
Error recovery : Retries failed requests
URL Routing System
Movie URLs
Series URLs
/ver/serie/{series-slug}-{season}x{episode}
Example: /ver/serie/breaking-bad-1x1
Anime URLs
/ver/anime/{anime-slug}-{season}x{episode}
Example: /ver/anime/naruto-1x1
Slug Parsing
const parseSeriesSlug = ( slug : string ) => {
const match = slug . match ( / ^ ( . + ) - ( \d + ) x ( \d + ) $ / );
if ( match ) {
return {
seriesSlug: match [ 1 ],
season: parseInt ( match [ 2 ]),
episode: parseInt ( match [ 3 ])
};
}
return null ;
};
@app.route ( '/api/pelicula/<slug>' , methods = [ 'GET' ])
def api_ver_pelicula ( slug ):
# Search in Movies section
url = f " { movies_url } / { slug } "
player = extraer_iframe_reproductor(url)
if not player:
# Fallback to Anime Movies section
url = f " { anime_movies_url } / { slug } "
player = extraer_iframe_reproductor(url)
if not player:
return jsonify({ "error" : "Película no encontrada" }), 404
return jsonify({
"slug" : slug,
"player" : {
"player_url" : player.get( "player_url" ),
"source" : player.get( "fuente" ),
"format" : player.get( "formato" )
}
})
@app.route ( '/api/serie/<slug>' , methods = [ 'GET' ])
def api_ver_serie ( slug ):
url = f " { series_url } / { slug } "
result = extraer_episodios_serie(url)
episodios = result.get( "episodios" , [])
if not episodios:
# Fallback to anime section
url = f " { anime_url } / { slug } "
result = extraer_episodios_serie(url)
episodios = result.get( "episodios" , [])
# Group episodes by season
temporadas = {}
for ep in episodios:
t = ep[ 'temporada' ]
if t not in temporadas:
temporadas[t] = []
temporadas[t].append(ep)
return jsonify({ "temporadas" : temporadas, "info" : result.get( "info" , {})})
Fetch HTML
Download page HTML using Cloudflare bypass
Clean Ads
Remove ad elements before parsing
Parse DOM
Use BeautifulSoup to find player iframe
Extract URL
Get iframe src attribute
Return Metadata
Include player URL, source domain, and format
def extraer_iframe_reproductor ( url ):
html = fetch_html(url)
if not html:
return None
html_limpio = clean_html_ads(html)
soup = BeautifulSoup(html_limpio, 'html.parser' )
iframe = soup.select_one( '.dooplay_player iframe' )
if iframe and iframe.get( 'src' ):
url_reproductor = iframe[ 'src' ]
return {
"player_url" : url_reproductor,
"fuente" : url_reproductor.split( '/' )[ 2 ],
"formato" : "iframe"
}
return None
Loading and Error States
Loading Display
if ( isLoading ) {
return (
< div className = "min-h-screen bg-space-black flex items-center justify-center" >
< LoadingSpinner size = "lg" />
</ div >
);
}
Error Handling
if ( error || ! data ) {
return (
< div className = "min-h-screen bg-space-black flex items-center justify-center" >
< div className = "text-center" >
< h2 className = "text-2xl font-bold text-white mb-2" >
Contenido no encontrado
</ h2 >
< p className = "text-gray-400 mb-4" >
El contenido que buscas no existe o ha sido eliminado.
</ p >
< Link to = "/" className = "bg-blue-600 px-6 py-2 rounded-md" >
Volver al catálogo
</ Link >
</ div >
</ div >
);
}
Best Practices
Show loading states during player initialization
Provide clear error messages with recovery options
Support keyboard navigation (Escape to exit)
Use theme colors for consistent branding
Detect orientation and adjust layout
Hide sidebars in portrait mode
Use full-screen player on landscape
Optimize touch targets (48px minimum)
Catalog Browsing Navigate to player from catalog
Web Scraping Learn how player URLs are extracted