Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions websites/S/SoloLatino/iframe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
const iframe = new iFrame()

iframe.on('UpdateData', async () => {
const video = document.querySelector('video')

if (video && !Number.isNaN(video.duration)) {
iframe.send({
duration: video.duration,
currentTime: video.currentTime,
paused: video.paused,
})
}
})
78 changes: 78 additions & 0 deletions websites/S/SoloLatino/metadata.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
{
"$schema": "https://schemas.premid.app/metadata/1.16",
"apiVersion": 1,
"author": {
"name": "0immagic0",
"id": "1545854937341239306"
},
"contributors": [],
"service": "SoloLatino",
"description": {
"en": "SoloLatino.Net is a free streaming website where you can watch movies, series, animes and doramas dubbed in Latin Spanish.",
"es": "SoloLatino.Net es un sitio de streaming gratuito donde puedes ver películas, series, animes y doramas doblados al español latino."
},
"url": "sololatino.net",
"regExp": "^https?[:][/][/]sololatino[.]net[/]",
"version": "1.0.0",
"logo": "https://i.imgur.com/WTlbzGb.png",
"thumbnail": "https://i.imgur.com/WTlbzGb.png",
"color": "#e50914",
"category": "videos",
"tags": [
"streaming",
"peliculas",
"series",
"anime",
"doramas",
"latino",
"espanol",
"movies",
"tv"
],
"iframe": true,
"iFrameRegExp": "player[.]pelisserieshoy[.]com|embed69[.]org|xupalace[.]org|drive[.]google[.]com|.*vidhide.*|.*streamhide.*",
"settings": [
{
"id": "brand",
"title": "Mostrar SoloLatino en el estado",
"icon": "fas fa-signature",
"value": true,
"description": "Muestra \"Viendo SoloLatino\" como título de la actividad. Si lo desactivas, se muestra \"Viendo\" seguido únicamente del nombre de la serie o película."
},
{
"id": "showTempEp",
"title": "Mostrar temporada y episodio",
"icon": "fas fa-list-ol",
"value": true,
"description": "Muestra la temporada y el episodio actual mientras ves una serie."
},
{
"id": "showTime",
"title": "Mostrar tiempo restante",
"icon": "fas fa-stopwatch",
"value": true,
"description": "Muestra el minuto en el que vas, con un contador sincronizado al reproductor."
},
{
"id": "showCover",
"title": "Mostrar portada",
"icon": "fas fa-image",
"value": true,
"description": "Usa la portada de la serie o película como imagen grande en lugar del logo."
},
{
"id": "showPlayState",
"title": "Mostrar estado de reproducción",
"icon": "fas fa-play-circle",
"value": true,
"description": "Muestra un pequeño icono que indica si el video está en reproducción o en pausa."
},
{
"id": "showButton",
"title": "Mostrar botón Ver ahora",
"icon": "fas fa-external-link-alt",
"value": true,
"description": "Añade un botón que abre directamente la página de la serie, episodio o película que estás viendo."
}
]
}
258 changes: 258 additions & 0 deletions websites/S/SoloLatino/presence.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,258 @@
import { ActivityType, Assets, getTimestamps } from 'premid'

const presence = new Presence({
// Crea tu aplicación en https://discord.com/developers/applications y pega aquí su Client ID
clientId: '1545854937341239306',
})

enum ActivityAssets {
Logo = 'https://i.imgur.com/WTlbzGb.png',
}

interface VideoData {
duration: number
currentTime: number
paused: boolean
}

interface SchemaNode {
'@type'?: string | string[]
'name'?: string
'image'?: string
'partOfSeason'?: { seasonNumber?: number }
'partOfSeries'?: { name?: string }
}

let video: VideoData = { duration: 0, currentTime: 0, paused: true }
let lastIFrameUpdate = 0
const iframeCacheDuration = 5000
const browsingTimestamp = Math.floor(Date.now() / 1000)

const strings = presence.getStrings({
play: 'general.playing',
pause: 'general.paused',
browse: 'general.browsing',
})

presence.on('iFrameData', (data: unknown) => {
video = data as VideoData
lastIFrameUpdate = Date.now()
})

function getSchemaNode(): SchemaNode | null {
for (const script of document.querySelectorAll('script[type="application/ld+json"]')) {
try {
const parsed = JSON.parse(script.textContent ?? '') as SchemaNode & { '@graph'?: SchemaNode[] }
const graph = parsed['@graph']
const nodes = Array.isArray(graph) ? graph : [parsed]
for (const node of nodes) {
const type = Array.isArray(node['@type']) ? node['@type'][0] : node['@type']
if (type === 'TVEpisode' || type === 'Movie' || type === 'TVSeries')
return node
}
}
catch {
continue
}
}
return null
}

function cleanTitle(raw: string): string {
return raw
.replace(/^\s*Ver\s+/i, '')
.replace(/\s*\(\d{4}\)\s*Online.*$/i, '')
.replace(/\s+Online.*$/i, '')
.replace(/\s*\|\s*SoloLatino\.Net\s*$/i, '')
.trim()
}

function getOgTitle(): string {
return document.querySelector<HTMLMetaElement>('meta[property="og:title"]')?.content ?? ''
}

function getOgImage(): string {
return document.querySelector<HTMLMetaElement>('meta[property="og:image"]')?.content ?? ''
}

function getYear(): string {
return getOgTitle().match(/\((\d{4})\)/)?.[1] ?? ''
}

function prettify(value: string): string {
return value
.split('-')
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
.join(' ')
}

function truncate(text: string, maxLength: number): string {
if (text.length <= maxLength)
return text
return `${text.slice(0, maxLength - 1).trimEnd()}…`
}

function getParentVideo(): VideoData | null {
const el = document.querySelector<HTMLVideoElement>('#player-frame video')
if (!el || Number.isNaN(el.duration) || el.duration <= 0 || Number.isNaN(el.currentTime))
return null
return { duration: el.duration, currentTime: el.currentTime, paused: el.paused }
}

function getVideoData(): VideoData | null {
if (Date.now() - lastIFrameUpdate < iframeCacheDuration && video.duration > 0)
return video
const parentVideo = getParentVideo()
return parentVideo || (video.duration > 0 ? video : null)
}

function applyVideoState(
presenceData: PresenceData,
showTime: boolean,
showPlayState: boolean,
playLabel: string,
pauseLabel: string,
): void {
const currentVideo = getVideoData()
if (!currentVideo || currentVideo.duration <= 0 || Number.isNaN(currentVideo.duration) || Number.isNaN(currentVideo.currentTime))
return

if (showTime && !currentVideo.paused) {
[presenceData.startTimestamp, presenceData.endTimestamp] = getTimestamps(
Math.floor(currentVideo.currentTime),
Math.floor(currentVideo.duration),
)
}

if (showPlayState) {
presenceData.smallImageKey = currentVideo.paused ? Assets.Pause : Assets.Play
presenceData.smallImageText = currentVideo.paused ? pauseLabel : playLabel
}
}

function applyButton(presenceData: PresenceData, showButton: boolean, url: string): void {
if (showButton)
presenceData.buttons = [{ label: 'Ver ahora', url }]
}

presence.on('UpdateData', async () => {
const s = await strings
const brand = await presence.getSetting<boolean>('brand')
const showTempEp = await presence.getSetting<boolean>('showTempEp')
const showTime = await presence.getSetting<boolean>('showTime')
const showCover = await presence.getSetting<boolean>('showCover')
const showPlayState = await presence.getSetting<boolean>('showPlayState')
const showButton = await presence.getSetting<boolean>('showButton')

const { pathname, href, search } = document.location
const schema = getSchemaNode()

const presenceData: PresenceData = {
type: ActivityType.Watching,
name: 'SoloLatino',
largeImageKey: ActivityAssets.Logo,
}

const episodeMatch = pathname.match(/^\/serie\/[^/]+\/temporada-(\d+)\/episodio-(\d+)/)
const seriesMatch = pathname.match(/^\/serie\/[^/]+\/?$/)
const movieMatch = pathname.match(/^\/pelicula\/[^/]+\/?$/)

if (episodeMatch) {
const seasonNumber = episodeMatch[1]
const episodeNumber = episodeMatch[2]
const seriesName = schema?.partOfSeries?.name || cleanTitle(getOgTitle()) || 'Serie'
const episodeTitle = schema?.name || document.querySelector('h1')?.textContent?.trim() || ''
const cover = getOgImage()
const tempEp = `Temporada ${seasonNumber} · Episodio ${episodeNumber}`

if (brand) {
presenceData.details = `Viendo ${seriesName}`
if (showTempEp)
presenceData.state = episodeTitle ? truncate(`${tempEp} · ${episodeTitle}`, 120) : tempEp
else if (episodeTitle)
presenceData.state = truncate(episodeTitle, 120)
}
else {
presenceData.name = seriesName
if (showTempEp)
presenceData.details = tempEp
if (episodeTitle)
presenceData.state = truncate(episodeTitle, 120)
}

presenceData.largeImageKey = showCover && cover ? cover : ActivityAssets.Logo
presenceData.largeImageText = seriesName
applyVideoState(presenceData, showTime, showPlayState, s.play, s.pause)
applyButton(presenceData, showButton, href)
return presence.setActivity(presenceData)
}

if (movieMatch) {
const movieName = schema?.name || cleanTitle(getOgTitle()) || 'Película'
const cover = getOgImage()
const year = getYear()
const movieLabel = year ? `Película · ${year}` : 'Película'

if (brand) {
presenceData.details = `Viendo ${movieName}`
presenceData.state = movieLabel
}
else {
presenceData.name = movieName
presenceData.details = movieLabel
}

presenceData.largeImageKey = showCover && cover ? cover : ActivityAssets.Logo
presenceData.largeImageText = movieName
applyVideoState(presenceData, showTime, showPlayState, s.play, s.pause)
applyButton(presenceData, showButton, href)
return presence.setActivity(presenceData)
}

if (seriesMatch) {
const seriesName = schema?.name || cleanTitle(getOgTitle()) || 'Serie'
const cover = schema?.image || getOgImage()

if (brand) {
presenceData.details = `Explorando ${seriesName}`
presenceData.state = 'Serie'
}
else {
presenceData.name = seriesName
presenceData.details = 'Serie'
}

presenceData.largeImageKey = showCover && cover ? cover : ActivityAssets.Logo
presenceData.largeImageText = seriesName
presenceData.startTimestamp = browsingTimestamp
presenceData.smallImageKey = Assets.Search
presenceData.smallImageText = s.browse
applyButton(presenceData, showButton, href)
return presence.setActivity(presenceData)
}

presenceData.details = 'Explorando SoloLatino'
presenceData.state = 'Navegando'
presenceData.startTimestamp = browsingTimestamp
presenceData.smallImageKey = Assets.Search
presenceData.smallImageText = s.browse

if (pathname === '/' || pathname === '') {
presenceData.state = 'Página principal'
}
else if (/^\/(?:series|peliculas|animes|doramas)\/?$/.test(pathname)) {
presenceData.state = prettify(pathname.replace(/\//g, ''))
}
else if (/^\/genero\/[^/]+/.test(pathname)) {
presenceData.state = `Género: ${prettify(pathname.split('/')[2] ?? '')}`
}
else if (/^\/red\/[^/]+/.test(pathname)) {
presenceData.state = `Plataforma: ${prettify(pathname.split('/')[2] ?? '')}`
}
else if (pathname.startsWith('/buscar')) {
const query = new URLSearchParams(search).get('q')
presenceData.state = query ? truncate(`Buscando: ${query}`, 80) : 'Buscando contenido'
}

return presence.setActivity(presenceData)
})
Loading