commit b6eb41dfdcadb8199f983a1a4c19663dc31de040 Author: alwaysmesmerizingyou Date: Tue Oct 6 21:47:41 2026 -0700 Initial documentation diff --git a/ai-catalog.json b/ai-catalog.json new file mode 100644 index 0000000..c9e6751 --- /dev/null +++ b/ai-catalog.json @@ -0,0 +1,177 @@ +{ + "name": "Vyla Documentation", + "description": "Comprehensive documentation for Vyla - a community-built, self-hosted media project focused on freedom, privacy, and user control. Includes SDK reference, Stream API documentation, and guides for building custom media players.", + "url": "https://vyla.cc", + "type": "documentation", + "category": "software", + "tags": [ + "media streaming", + "self-hosted", + "privacy", + "open source", + "SDK", + "API", + "Node.js", + "TypeScript", + "Android", + "Android TV", + "desktop", + "streaming", + "video player", + "HLS", + "SSE" + ], + "language": "en", + "version": "1.0.0", + "lastUpdated": "2026-10-06", + "maintainer": { + "name": "Vyla Entertainment", + "url": "https://gitlab.com/vyla-entertainment/" + }, + "documentation": { + "main": "https://vyla.cc", + "llms": "https://vyla.cc/llms.txt", + "repository": "https://gitlab.com/vyla-entertainment/" + }, + "topics": [ + { + "name": "Getting Started", + "pages": [ + { + "title": "Introduction", + "url": "/" + }, + { + "title": "Architecture", + "url": "/architecture" + } + ] + }, + { + "name": "Products", + "pages": [ + { + "title": "Vyla Home", + "url": "/home" + }, + { + "title": "Vyla Go", + "url": "/go" + }, + { + "title": "Vyla TV", + "url": "/tv" + }, + { + "title": "Build a Player", + "url": "/player" + } + ] + }, + { + "name": "SDK", + "pages": [ + { + "title": "SDK Reference", + "url": "/sdk" + }, + { + "title": "SDK Quickstart", + "url": "/sdk-quickstart" + } + ] + }, + { + "name": "Stream API", + "pages": [ + { + "title": "Overview", + "url": "/stream-api/overview" + }, + { + "title": "Paths and Events", + "url": "/stream-api/paths" + }, + { + "title": "Subtitles and Downloads", + "url": "/stream-api/media-data" + }, + { + "title": "Health and Operations", + "url": "/stream-api/operations" + } + ] + }, + { + "name": "Deployment", + "pages": [ + { + "title": "Self-hosting", + "url": "/self-hosting" + } + ] + }, + { + "name": "Concepts", + "pages": [ + { + "title": "Providers", + "url": "/concepts/providers" + } + ] + }, + { + "name": "Guides", + "pages": [ + { + "title": "Client Integration", + "url": "/guides/integrations" + }, + { + "title": "Error Handling", + "url": "/guides/error-handling" + } + ] + }, + { + "name": "Miscellaneous", + "pages": [ + { + "title": "Credits", + "url": "/misc/credit" + }, + { + "title": "DMCA Policy", + "url": "/misc/dmca" + } + ] + } + ], + "apis": [ + { + "name": "Vyla SDK", + "type": "npm", + "package": "@vyla-entertainment/sdk", + "description": "Node.js/TypeScript SDK for direct provider resolution", + "language": "TypeScript" + }, + { + "name": "Stream API", + "type": "http", + "description": "Self-hosted HTTP/SSE API for browser and mobile clients", + "protocol": "SSE" + } + ], + "platforms": [ + "Windows", + "macOS", + "Linux", + "Android", + "Android TV" + ], + "license": "ISC", + "community": { + "discord": "https://discord.gg/5eqRhKeywG", + "gitlab": "https://gitlab.com/vyla-entertainment/" + } +} \ No newline at end of file diff --git a/architecture/index.html b/architecture/index.html new file mode 100644 index 0000000..38494d1 --- /dev/null +++ b/architecture/index.html @@ -0,0 +1,190 @@ + + + + + + + Our Architecture — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Our Architecture

+

How Vyla Home, SDK, and Stream API relate, and where each responsibility belongs. +

+

The Vyla stack

+

Vyla separates local application management, provider resolution, and client-facing delivery. That separation + lets each layer be used on its own while keeping the integration points clear.

+

Vyla Home

+

Home is responsible for a desktop session: local configuration, encrypted local credentials, update + coordination, managed processes, and the embedded interface. It is the right layer for launching and + supervising a local experience.

+

Vyla SDK

+

The SDK contains the provider-facing work: source configuration, stream resolution, subtitle retrieval, + download lookup, and health probes. It is the right layer when your Node.js application needs to decide how + requests are authorized, cached, selected, and returned.

+

Stream API

+

Stream API wraps the SDK in a self-hosted HTTP service. Its movie and TV endpoints use Server-Sent Events so + metadata can arrive first and usable sources can be delivered progressively. It also provides subtitle, + download, health, test, and optional debug routes.

+

Common deployment patterns

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PatternBest forKey decision
Home onlya managed local desktop experienceHome owns the lifecycle of bundled local services
App + SDKa Node.js backend with a custom product APIyour app owns authentication, policy, and response design
Player + Stream APIbrowser or mobile clients that need a stable HTTP contractdeploy Stream API and place it behind TLS when exposed remotely
Home + Stream APIa local player backed by a local servicekeep the service bound and access-controlled for the local environment
+

Data flow for playback

+
    +
  1. A client provides a TMDB media ID (plus season and episode for TV).
  2. +
  3. Stream API, or your own SDK-based backend, asks active providers for candidates.
  4. +
  5. Candidates are checked and successful sources are returned progressively.
  6. +
  7. The player begins with the first compatible candidate and keeps later candidates as fallbacks.
  8. +
  9. Subtitle and download lookups are separate operations; neither should block first playback.
  10. +
+

Provider availability varies by source and network environment. Build the player experience around fallbacks + and empty-result states, as described in Error handling.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/architecture/index.html.backup b/architecture/index.html.backup new file mode 100644 index 0000000..bfbcaca --- /dev/null +++ b/architecture/index.html.backup @@ -0,0 +1,177 @@ + + + + + + + Our Architecture — Vyla + + + + + + + + + + + + + + + + + + + + + + + content="How Vyla Home, SDK, and Stream API relate, and where each responsibility belongs." /> + + + + + + + + + + + + +
+ +
+
+

Our Architecture

+

How Vyla Home, SDK, and Stream API relate, and where each responsibility belongs. +

+

The Vyla stack

+

Vyla separates local application management, provider resolution, and client-facing delivery. That separation + lets each layer be used on its own while keeping the integration points clear.

+

Vyla Home

+

Home is responsible for a desktop session: local configuration, encrypted local credentials, update + coordination, managed processes, and the embedded interface. It is the right layer for launching and + supervising a local experience.

+

Vyla SDK

+

The SDK contains the provider-facing work: source configuration, stream resolution, subtitle retrieval, + download lookup, and health probes. It is the right layer when your Node.js application needs to decide how + requests are authorized, cached, selected, and returned.

+

Stream API

+

Stream API wraps the SDK in a self-hosted HTTP service. Its movie and TV endpoints use Server-Sent Events so + metadata can arrive first and usable sources can be delivered progressively. It also provides subtitle, + download, health, test, and optional debug routes.

+

Common deployment patterns

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PatternBest forKey decision
Home onlya managed local desktop experienceHome owns the lifecycle of bundled local services
App + SDKa Node.js backend with a custom product APIyour app owns authentication, policy, and response design
Player + Stream APIbrowser or mobile clients that need a stable HTTP contractdeploy Stream API and place it behind TLS when exposed remotely
Home + Stream APIa local player backed by a local servicekeep the service bound and access-controlled for the local environment
+

Data flow for playback

+
    +
  1. A client provides a TMDB media ID (plus season and episode for TV).
  2. +
  3. Stream API, or your own SDK-based backend, asks active providers for candidates.
  4. +
  5. Candidates are checked and successful sources are returned progressively.
  6. +
  7. The player begins with the first compatible candidate and keeps later candidates as fallbacks.
  8. +
  9. Subtitle and download lookups are separate operations; neither should block first playback.
  10. +
+

Provider availability varies by source and network environment. Build the player experience around fallbacks + and empty-result states, as described in Error handling.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/assets/app.js b/assets/app.js new file mode 100644 index 0000000..ba33526 --- /dev/null +++ b/assets/app.js @@ -0,0 +1,653 @@ +var THEME_KEY = "vyla-theme"; +var ICONS = { + system: '', + light: '', + dark: '', + search: '', + menu: '', + doc: '', + hash: '', + copy: '', + check: '', + chevron: '' +}; +var searchData = null; +var searchResults = []; +var searchActive = 0; + +function icon(name) { + return '' + ICONS[name] + ""; +} + +function esc(s) { + return String(s).replace(/[&<>"]/g, function (c) { + return { "&": "&", "<": "<", ">": ">", '"': """ }[c]; + }); +} + +function getThemeMode() { + var v = localStorage.getItem(THEME_KEY); + return v === "light" || v === "dark" ? v : "system"; +} + +function applyTheme() { + var mode = getThemeMode(); + var dark = mode === "dark" || (mode === "system" && window.matchMedia("(prefers-color-scheme: dark)").matches); + document.documentElement.classList.toggle("dark", dark); + var logo = document.getElementById("brand-logo"); + if (logo) logo.src = dark ? "/assets/images/logo.png" : "/assets/images/dark_logo.png"; +} + +function setTheme(mode) { + if (mode === "system") localStorage.removeItem(THEME_KEY); + else localStorage.setItem(THEME_KEY, mode); + applyTheme(); + refreshThemeUI(); + var menu = document.getElementById("theme-menu"); + if (menu) menu.classList.remove("open"); +} + +function toggleThemeMenu(e) { + e.stopPropagation(); + document.getElementById("theme-menu").classList.toggle("open"); +} + +function refreshThemeUI() { + var mode = getThemeMode(); + var btn = document.getElementById("theme-toggle"); + if (btn) btn.innerHTML = icon(mode); + document.querySelectorAll("#theme-menu button, #theme-seg button").forEach(function (b) { + b.classList.toggle("on", b.getAttribute("data-mode") === mode); + }); +} + +function buildThemeControls() { + var btn = document.getElementById("theme-toggle"); + var links = document.querySelector("#navbar .links"); + if (!btn || !links) return; + var wrap = document.createElement("div"); + wrap.className = "theme-wrap"; + btn.parentNode.insertBefore(wrap, btn); + wrap.appendChild(btn); + var modes = ["system", "light", "dark"]; + var menu = document.createElement("div"); + menu.id = "theme-menu"; + menu.innerHTML = modes.map(function (m) { + return ''; + }).join(""); + wrap.appendChild(menu); + var seg = document.createElement("div"); + seg.id = "theme-seg"; + seg.innerHTML = modes.map(function (m) { + return '"; + }).join(""); + links.appendChild(seg); + [menu, seg].forEach(function (el) { + el.addEventListener("click", function (e) { + var b = e.target.closest("button"); + if (b) setTheme(b.getAttribute("data-mode")); + }); + }); + refreshThemeUI(); +} + +function toggleSidebar() { + var sidebar = document.getElementById("sidebar"); + var nav = document.getElementById("navbar"); + var top = Math.max(0, Math.round(nav.getBoundingClientRect().bottom)); + sidebar.style.setProperty("--sidebar-top", top + "px"); + var open = sidebar.classList.toggle("open"); + document.body.classList.toggle("nav-open", open); +} + +function findActiveTab(root) { + var hrefs = {}; + root.querySelectorAll("#sidebar .group li a").forEach(function (a) { + hrefs[a.getAttribute("href")] = true; + }); + var tabs = root.querySelectorAll("#navbar .tabs a"); + for (var i = 0; i < tabs.length; i++) { + if (hrefs[tabs[i].getAttribute("href")]) return tabs[i]; + } + return root.querySelector("#navbar .tabs a.active"); +} + +function fixActiveTab() { + var current = findActiveTab(document); + if (!current) return; + document.querySelectorAll("#navbar .tabs a").forEach(function (a) { + a.classList.toggle("active", a === current); + }); +} + +function activeLink() { + return document.querySelector("#sidebar .group:not(.mobile-tabs) li a.active"); +} + +function activeGroupTitle() { + var a = activeLink(); + var g = a && a.closest(".group"); + var t = g && g.querySelector(".group-title"); + return t ? t.textContent.trim() : ""; +} + +function buildMobileBar() { + var nav = document.getElementById("navbar"); + var sidebar = document.getElementById("sidebar"); + if (!nav) return; + var a = activeLink(); + var group = activeGroupTitle(); + var bar = document.createElement("div"); + bar.id = "mobile-bar"; + bar.innerHTML = + '" + + (group ? "" + esc(group) + '›' : "") + + '' + esc(a ? a.textContent.trim() : document.title) + ""; + nav.parentNode.insertBefore(bar, nav.nextSibling); + var tabs = document.querySelectorAll("#navbar .tabs a"); + if (sidebar && tabs.length) { + var currentTab = document.querySelector("#navbar .tabs a.active") || tabs[0]; + var g = document.createElement("div"); + g.className = "mobile-tabs"; + g.innerHTML = + '" + + '
' + Array.prototype.map.call(tabs, function (t) { + return '" + esc(t.textContent.trim()) + icon("check") + ""; + }).join("") + "
"; + sidebar.insertBefore(g, sidebar.firstChild); + g.querySelector(".mt-btn").addEventListener("click", function () { + g.classList.toggle("open"); + }); + document.addEventListener("click", function (e) { + if (!g.contains(e.target)) g.classList.remove("open"); + }); + g.querySelectorAll(".mt-menu a").forEach(function (a) { + a.addEventListener("click", function (e) { + e.preventDefault(); + e.stopPropagation(); + var href = a.getAttribute("href"); + g.querySelectorAll(".mt-menu a").forEach(function (link) { + link.classList.remove("on"); + }); + a.classList.add("on"); + g.querySelector(".mt-btn span").textContent = a.textContent.trim(); + g.classList.remove("open"); + var sidebar = document.getElementById("sidebar"); + if (sidebar) { + var mobileTabs = sidebar.querySelector(".mobile-tabs"); + var nav = document.getElementById("navbar"); + var top = Math.max(0, Math.round(nav.getBoundingClientRect().bottom)); + sidebar.style.setProperty("--sidebar-top", top + "px"); + fetch(href) + .then(function (r) { return r.text(); }) + .then(function (html) { + var doc = new DOMParser().parseFromString(html, "text/html"); + var newSidebar = doc.querySelector("#sidebar"); + if (newSidebar) { + var groups = newSidebar.querySelectorAll(".group"); + sidebar.innerHTML = ""; + if (mobileTabs) sidebar.appendChild(mobileTabs); + groups.forEach(function (group) { + sidebar.appendChild(group.cloneNode(true)); + }); + addEyebrow(); + } + }) + .catch(function () { }); + if (!sidebar.classList.contains("open")) { + sidebar.classList.add("open"); + document.body.classList.add("nav-open"); + } + } + }); + }); + } +} + +function addEyebrow() { + var h1 = document.querySelector("main.prose h1"); + var group = activeGroupTitle(); + if (!h1 || !group) return; + var el = document.createElement("div"); + el.className = "eyebrow"; + el.textContent = group; + h1.parentNode.insertBefore(el, h1); +} + +function addCopyButtons() { + document.querySelectorAll(".code-group").forEach(function (cg) { + if (cg.querySelector(".cg-copy-btn")) return; + var b = document.createElement("button"); + b.type = "button"; + b.className = "cg-copy-btn"; + b.setAttribute("aria-label", "Copy"); + b.innerHTML = icon("copy"); + b.onclick = function () { + var activePre = cg.querySelector(".code-group-panel.active pre"); + if (!activePre) return; + navigator.clipboard.writeText(activePre.innerText).then(function () { + b.innerHTML = icon("check"); + setTimeout(function () { b.innerHTML = icon("copy"); }, 1500); + }); + }; + cg.appendChild(b); + }); + + document.querySelectorAll("main.prose pre").forEach(function (pre) { + if (pre.closest(".code-group")) return; + if (pre.parentNode.classList.contains("code-wrap")) return; + var wrap = document.createElement("div"); + wrap.className = "code-wrap"; + pre.parentNode.insertBefore(wrap, pre); + wrap.appendChild(pre); + var b = document.createElement("button"); + b.type = "button"; + b.className = "copy-btn"; + b.setAttribute("aria-label", "Copy"); + b.innerHTML = icon("copy"); + b.onclick = function () { + navigator.clipboard.writeText(pre.innerText).then(function () { + b.innerHTML = icon("check"); + setTimeout(function () { b.innerHTML = icon("copy"); }, 1500); + }); + }; + wrap.appendChild(b); + }); +} + +function bindScrollFade(box, getScroller) { + function update() { + var el = getScroller(); + var max = el.scrollWidth - el.clientWidth; + var left = 0; + var right = 0; + if (max > 1) { + left = Math.min(el.scrollLeft, 56); + right = Math.min(Math.max(Math.round(max - el.scrollLeft), 0), 56); + } + box.style.setProperty("--fade-l", left + "px"); + box.style.setProperty("--fade-r", right + "px"); + } + box.addEventListener("scroll", update, true); + window.addEventListener("resize", update); + window.addEventListener("load", update); + update(); +} + +function initCodeFades() { + document.querySelectorAll("main.prose pre").forEach(function (pre) { + var code = pre.querySelector("code"); + bindScrollFade(pre, function () { + return code && code.scrollWidth > code.clientWidth + 1 ? code : pre; + }); + }); +} + +function wrapTables() { + document.querySelectorAll("main.prose table").forEach(function (table) { + if (table.parentNode.classList.contains("table-wrap")) return; + var wrap = document.createElement("div"); + wrap.className = "table-wrap"; + table.parentNode.insertBefore(wrap, table); + wrap.appendChild(table); + bindScrollFade(wrap, function () { + return wrap; + }); + }); +} + +function initTocSpy() { + var links = Array.prototype.slice.call(document.querySelectorAll("aside#toc a[href^='#']")); + if (!links.length) return; + var heads = links.map(function (a) { + return document.getElementById(decodeURIComponent(a.getAttribute("href").slice(1))); + }); + function update() { + var idx = 0; + heads.forEach(function (h, i) { + if (h && h.getBoundingClientRect().top < 140) idx = i; + }); + links.forEach(function (a, i) { a.classList.toggle("active", i === idx); }); + } + window.addEventListener("scroll", update, { passive: true }); + update(); +} + +var SEARCH_KEY = "vyla-search-v1"; +var searchPromise = null; + +function getRecent() { + try { return JSON.parse(localStorage.getItem("vyla-recent-v2")) || []; } catch (e) { return []; } +} + +function saveRecent(item) { + if (!item) return; + var slim = { title: item.title, crumbs: item.crumbs, href: item.href, type: item.type, text: "" }; + var list = getRecent().filter(function (r) { return r.href !== slim.href; }); + list.unshift(slim); + localStorage.setItem("vyla-recent-v2", JSON.stringify(list.slice(0, 5))); +} + +function cleanText(s) { + return s.replace(/\s+/g, " ").trim(); +} + +function fetchDoc(href) { + return fetch(href) + .then(function (r) { + if (!r.ok) throw new Error(href); + return r.text(); + }) + .then(function (html) { + return { href: href, doc: new DOMParser().parseFromString(html, "text/html") }; + }) + .catch(function () { + return null; + }); +} + +function indexDoc(res) { + var doc = res.doc; + var href = res.href.split("#")[0]; + var prose = doc.querySelector("main.prose"); + if (!prose) return []; + var tabEl = findActiveTab(doc); + var activeEl = doc.querySelector("#sidebar li a.active"); + var groupEl = activeEl && activeEl.closest(".group"); + var groupTitleEl = groupEl && groupEl.querySelector(".group-title"); + var h1 = prose.querySelector("h1"); + var pageTitle = h1 ? cleanText(h1.textContent) : activeEl ? cleanText(activeEl.textContent) : href; + var base = []; + if (tabEl) base.push(cleanText(tabEl.textContent)); + if (groupTitleEl) base.push(cleanText(groupTitleEl.textContent)); + var current = { title: pageTitle, id: "", type: "page", crumbs: base.slice(), text: [] }; + var entries = [current]; + Array.prototype.forEach.call(prose.children, function (el) { + if (el.tagName === "H1") return; + if (el.tagName === "H2" || el.tagName === "H3") { + var title = cleanText(el.textContent); + current = { title: title, id: el.id, type: "heading", crumbs: base.concat([pageTitle, title]), text: [] }; + entries.push(current); + return; + } + current.text.push(cleanText(el.textContent)); + }); + return entries.map(function (e) { + return { + title: e.title, + crumbs: e.crumbs, + href: e.id ? href + "#" + e.id : href, + type: e.type, + text: e.text.join(" ").slice(0, 4000) + }; + }); +} + +function crawlSite() { + var seen = {}; + var all = []; + function visit(hrefs) { + var fresh = hrefs.map(function (h) { + return new URL(h, window.location.href); + }).filter(function (u) { + if (u.origin !== window.location.origin || seen[u.pathname]) return false; + seen[u.pathname] = true; + return true; + }).map(function (u) { + return u.pathname; + }); + return Promise.all(fresh.map(fetchDoc)).then(function (results) { + var next = []; + results.forEach(function (res) { + if (!res) return; + all = all.concat(indexDoc(res)); + res.doc.querySelectorAll("#sidebar li a").forEach(function (a) { + var h = a.getAttribute("href"); + if (h && h.charAt(0) !== "#") next.push(h); + }); + }); + return next.length ? visit(next) : all; + }); + } + var start = Array.prototype.map.call(document.querySelectorAll("#navbar .tabs a"), function (a) { + return a.getAttribute("href"); + }); + start.push(window.location.pathname); + return visit(start).then(function () { + return all; + }); +} + +function loadSearchData() { + if (searchData) return Promise.resolve(searchData); + if (searchPromise) return searchPromise; + try { + var cached = JSON.parse(sessionStorage.getItem(SEARCH_KEY)); + if (cached && cached.data && cached.data.length && Date.now() - cached.time < 600000) { + searchData = cached.data; + return Promise.resolve(searchData); + } + } catch (e) { } + searchPromise = crawlSite().then(function (all) { + searchData = all; + try { sessionStorage.setItem(SEARCH_KEY, JSON.stringify({ time: Date.now(), data: all })); } catch (e) { } + return all; + }); + return searchPromise; +} + +function escRe(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function hl(text, tokens) { + if (!tokens.length) return esc(text); + var re = new RegExp("(" + tokens.map(escRe).join("|") + ")", "ig"); + return text.split(re).map(function (part, i) { + return i % 2 ? '' + esc(part) + "" : esc(part); + }).join(""); +} + +function lower(it) { + if (!it._l) it._l = { t: it.title.toLowerCase(), c: it.crumbs.join(" ").toLowerCase(), x: (it.text || "").toLowerCase() }; + return it._l; +} + +function scoreEntry(it, tokens, q) { + var low = lower(it); + var score = 0; + for (var i = 0; i < tokens.length; i++) { + var t = tokens[i]; + var inTitle = low.t.indexOf(t) !== -1; + var inCrumbs = low.c.indexOf(t) !== -1; + var inText = low.x.indexOf(t) !== -1; + if (!inTitle && !inCrumbs && !inText) return 0; + score += (inTitle ? 20 : 0) + (inCrumbs ? 4 : 0) + (inText ? 1 : 0); + } + if (low.t === q) score += 100; + else if (low.t.indexOf(q) === 0) score += 50; + else if (low.t.indexOf(q) !== -1) score += 25; + if (it.type === "page") score += 5; + return score; +} + +function makeSnippet(it, tokens) { + var low = lower(it); + var inTitle = tokens.every(function (t) { return low.t.indexOf(t) !== -1; }); + if (inTitle || !it.text) return ""; + var hit = -1; + tokens.forEach(function (t) { + var k = low.x.indexOf(t); + if (k !== -1 && (hit === -1 || k < hit)) hit = k; + }); + if (hit === -1) return ""; + var start = Math.max(0, hit - 50); + var end = hit + 120; + return (start > 0 ? "…" : "") + it.text.slice(start, end) + (end < it.text.length ? "…" : ""); +} + +function renderSearch(keepActive) { + var q = document.getElementById("search-input").value.trim().toLowerCase(); + var tokens = q.split(/\s+/).filter(Boolean); + var box = document.getElementById("search-results"); + if (!keepActive) searchActive = 0; + if (tokens.length) { + if (!searchData) { + searchResults = []; + box.innerHTML = '
Indexing pages...
'; + return; + } + searchResults = searchData + .map(function (it) { return { it: it, s: scoreEntry(it, tokens, q) }; }) + .filter(function (r) { return r.s > 0; }) + .sort(function (a, b) { return b.s - a.s; }) + .slice(0, 12) + .map(function (r) { return r.it; }); + } else { + searchResults = getRecent(); + } + if (!searchResults.length) { + box.innerHTML = '
' + (tokens.length ? "No results found" : "No recent searches") + "
"; + return; + } + box.innerHTML = (tokens.length ? "" : '
Recent searches
') + searchResults.map(function (it, i) { + var snippet = tokens.length ? makeSnippet(it, tokens) : ""; + var crumbs = it.crumbs.map(function (c) { return hl(c, tokens); }).join('›'); + return '' + + icon(it.type === "heading" ? "hash" : "doc") + + '
' + esc(it.title) + "
" + + (snippet ? '
' + hl(snippet, tokens) + "
" : "") + + '
' + crumbs + "
"; + }).join(""); + if (keepActive) { + var on = box.querySelector(".sr.on"); + if (on) on.scrollIntoView({ block: "nearest" }); + } +} + +function openSearch() { + var input = document.getElementById("search-input"); + document.getElementById("search-modal").classList.add("open"); + document.body.style.overflow = "hidden"; + input.value = ""; + renderSearch(false); + loadSearchData().then(function () { renderSearch(false); }); + input.focus(); +} + +function closeSearch() { + document.getElementById("search-modal").classList.remove("open"); + document.body.style.overflow = ""; +} + +function onSearchKey(e) { + if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "k") { + e.preventDefault(); + openSearch(); + return; + } + var modal = document.getElementById("search-modal"); + if (!modal || !modal.classList.contains("open")) return; + if (e.key === "Escape") { + closeSearch(); + } else if (e.key === "ArrowDown" || e.key === "ArrowUp") { + e.preventDefault(); + var n = searchResults.length || 1; + searchActive = (searchActive + (e.key === "ArrowDown" ? 1 : -1) + n) % n; + renderSearch(true); + } else if (e.key === "Enter" && searchResults[searchActive]) { + var item = searchResults[searchActive]; + saveRecent(item); + closeSearch(); + window.location.href = item.href; + } +} + +function buildSearch() { + var trigger = document.getElementById("search-trigger"); + if (trigger) trigger.innerHTML = icon("search") + "Search...Ctrl K"; + var modal = document.createElement("div"); + modal.id = "search-modal"; + modal.innerHTML = + '
' + icon("search") + + '
' + + '
'; + document.body.appendChild(modal); + modal.querySelector(".search-backdrop").addEventListener("click", closeSearch); + document.getElementById("search-input").addEventListener("input", function () { renderSearch(false); }); + document.getElementById("search-results").addEventListener("click", function (e) { + var a = e.target.closest(".sr"); + if (a) { + saveRecent(searchResults[Number(a.getAttribute("data-i"))]); + closeSearch(); + } + }); + document.addEventListener("keydown", onSearchKey); +} + +applyTheme(); +window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", applyTheme); + +function dismissBanner() { + var banner = document.getElementById("banner"); + if (banner) banner.setAttribute("data-dismissed", "true"); + document.body.classList.add("banner-dismissed"); + localStorage.setItem("vyla-banner-dismissed", "true"); +} + +function cgSwitch(btn) { + var group = btn.closest(".code-group"); + var index = btn.getAttribute("data-tab-index"); + group.querySelectorAll(".code-group-tab").forEach(function (t) { + t.classList.toggle("active", t === btn); + }); + group.querySelectorAll(".code-group-panel").forEach(function (p, i) { + p.classList.toggle("active", String(i) === index); + }); + window.dispatchEvent(new Event("resize")); +} + +document.addEventListener("DOMContentLoaded", function () { + if (localStorage.getItem("vyla-banner-dismissed") === "true") { + var banner = document.getElementById("banner"); + if (banner) banner.setAttribute("data-dismissed", "true"); + document.body.classList.add("banner-dismissed"); + } + applyTheme(); + fixActiveTab(); + buildMobileBar(); + addEyebrow(); + addCopyButtons(); + initCodeFades(); + wrapTables(); + buildThemeControls(); + buildSearch(); + setTimeout(loadSearchData, 1500); + initTocSpy(); + document.addEventListener("click", function (e) { + var sidebar = document.getElementById("sidebar"); + var menu = document.getElementById("theme-menu"); + if (menu && !e.target.closest(".theme-wrap")) menu.classList.remove("open"); + if ( + sidebar && + sidebar.classList.contains("open") && + !sidebar.contains(e.target) && + !e.target.closest("#mobile-toggle") && + !e.target.closest(".theme-wrap") && + !e.target.closest("#theme-seg") + ) { + sidebar.classList.remove("open"); + document.body.classList.remove("nav-open"); + } + }); + var sidebarLinks = document.querySelectorAll("#sidebar .group:not(.mobile-tabs) li a"); + sidebarLinks.forEach(function (a) { + a.addEventListener("click", function () { + var sidebar = document.getElementById("sidebar"); + if (sidebar && sidebar.classList.contains("open")) { + sidebar.classList.remove("open"); + document.body.classList.remove("nav-open"); + } + }); + }); +}); \ No newline at end of file diff --git a/assets/base.css b/assets/base.css new file mode 100644 index 0000000..c277b1b --- /dev/null +++ b/assets/base.css @@ -0,0 +1,1501 @@ +@import url("https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"); + +:root { + --primary: #000000; + --bg-light: #ffffff; + --bg-dark: #000000; + --text-light: #0b0b0c; + --text-dark: #f3f3f3; + --muted-light: #6b7280; + --muted-dark: #a3a3a3; + --sidebar-width: 256px; + --toc-width: 240px; + --header-height: 109px; + --font: Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; + --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; + --border-light: #e5e7eb; + --border-dark: #2b2b2b98; +} + +* { + box-sizing: border-box; +} + +html { + height: 100%; +} + +body { + min-height: 100%; + margin: 0; + font-family: var(--font); + background: var(--bg-light); + color: var(--text-light); + line-height: 1.65; + overflow-x: hidden; +} + +html.dark body { + background: var(--bg-dark); + color: var(--text-dark); +} + +a { + color: inherit; + text-decoration: none; +} + +#banner { + position: relative; + display: block; + padding: 10px 48px; + font-size: 13px; + line-height: 1.45; + text-align: center; + z-index: 100; + background: var(--bg-light); +} + +html.dark #banner { + background: var(--bg-dark); +} + +#banner[data-dismissed="true"] { + display: none; +} + +#banner-close { + position: absolute; + top: 50%; + right: 8px; + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + padding: 0; + transform: translateY(-50%); + background: none; + border: none; + border-radius: 8px; + cursor: pointer; + font-size: 18px; + line-height: 1; + color: inherit; + opacity: 0.6; +} + +#banner-close:hover { + opacity: 1; + background: rgba(0, 0, 0, 0.06); +} + +html.dark #banner-close:hover { + background: rgba(255, 255, 255, 0.1); +} + +#navbar { + position: sticky; + top: 0; + z-index: 40; + background: var(--bg-light); +} + +#navbar::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 40px; + background: linear-gradient(to bottom, + rgba(255, 255, 255), + transparent); + pointer-events: none; +} + +html.dark #navbar::after { + background: linear-gradient(to bottom, + rgba(0, 0, 0), + transparent); +} + +html.dark #navbar { + background: var(--bg-dark); + border-color: transparent; +} + +#navbar .nav-top { + position: relative; + display: flex; + align-items: center; + height: 60px; + max-width: 1400px; + margin: 0 auto; + padding: 0 16px; +} + +#navbar .nav-top::after { + content: ""; + position: absolute; + left: 16px; + right: 16px; + bottom: 0; +} + +html.dark #navbar .nav-top::after { + border-color: transparent; +} + +#navbar .brand { + display: flex; + align-items: center; +} + +#navbar .brand img { + height: 28px; + width: 28px; + border-radius: 50%; + display: block; +} + +#navbar .links { + display: flex; + align-items: center; + gap: 10px; + margin-left: auto; +} + +#navbar .tabs { + display: flex; + align-items: center; + gap: 4px; + height: 48px; + max-width: 1400px; + margin: 0 auto; + padding: 0 16px; + overflow-x: auto; +} + +#navbar .tabs a { + padding: 6px 14px; + font-size: 14px; + font-weight: 500; + color: var(--muted-light); + white-space: nowrap; +} + +html.dark #navbar .tabs a { + color: var(--muted-dark); +} + +#navbar .tabs a:hover { + color: var(--text-light); +} + +html.dark #navbar .tabs a:hover { + color: var(--text-dark); +} + +#navbar .tabs a.active { + color: var(--text-light); + font-weight: 600; +} + +html.dark #navbar .tabs a.active { + color: var(--text-dark); +} + +#layout { + display: flex; + max-width: 1400px; + margin: 0 auto; +} + +#sidebar { + width: var(--sidebar-width); + flex-shrink: 0; + padding: 28px 20px; + position: sticky; + top: var(--header-height); + height: calc(100vh - var(--header-height)); + overflow-y: auto; +} + +#sidebar .group { + margin-bottom: 22px; +} + +#sidebar .group-title { + font-size: 12px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted-light); + margin: 0 0 8px 10px; +} + +html.dark #sidebar .group-title { + color: var(--muted-dark); +} + +#sidebar ul { + list-style: none; + margin: 0; + padding: 0; +} + +#sidebar li a { + display: block; + padding: 7px 10px; + border-radius: 8px; + font-size: 13.5px; + color: var(--muted-light); +} + +html.dark #sidebar li a { + color: var(--muted-dark); +} + +#sidebar li a:hover { + background: rgba(0, 0, 0, 0.05); + color: var(--text-light); +} + +html.dark #sidebar li a:hover { + background: rgba(255, 255, 255, 0.08); + color: var(--text-dark); +} + +#content-col { + flex: 1; + min-width: 0; + padding: 40px 48px 100px; + display: flex; + gap: 48px; +} + +main.prose { + flex: 1; + min-width: 0; + max-width: 760px; +} + +main.prose h1 { + font-size: 32px; + font-weight: 800; + margin: 0 0 6px; +} + +main.prose .page-description { + color: var(--muted-light); + font-size: 16px; + margin: 0 0 32px; +} + +html.dark main.prose .page-description { + color: var(--muted-dark); +} + +main.prose h2 { + font-size: 22px; + font-weight: 700; + margin: 40px 0 14px; + padding-top: 6px; +} + +main.prose h2:first-child { + border-top: none; + padding-top: 0; +} + +main.prose h3 { + font-size: 17px; + font-weight: 700; + margin: 26px 0 10px; +} + +main.prose p { + margin: 14px 0; +} + +main.prose ul, +main.prose ol { + margin: 14px 0; + padding-left: 22px; +} + +main.prose li { + margin: 4px 0; +} + +main.prose img { + max-width: 100%; + border-radius: 16px; + display: block; + margin: 20px 0; +} + +main.prose img.vyla-banner { + width: 100%; +} + +main.prose hr { + border: none; + margin: 32px 0; +} + +main.prose code { + font-family: var(--mono); + font-size: 13px; + background: rgba(0, 0, 0, 0.06); + padding: 2px 6px; + border-radius: 5px; +} + +html.dark main.prose code { + background: rgba(255, 255, 255, 0.1); + position: relative; + overflow: hidden; +} + +main.prose pre { + margin: 16px 0; + padding: 16px 18px; + border-radius: 12px; + overflow-x: auto; + background: rgba(250, 250, 250); +} + +html.dark main.prose pre { + background: rgba(5, 5, 5); + border-color: transparent; +} + +main.prose pre code { + background: none; + padding: 0; + font-size: 13px; +} + +.table-wrap { + --fade-l: 0px; + --fade-r: 0px; + max-width: 100%; + margin: 18px 0; + overflow-x: auto; + scrollbar-width: thin; + -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent 100%); + mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent 100%); +} + +.table-wrap table { + margin: 0; +} + +table { + width: 100%; + border-collapse: collapse; + margin: 18px 0; + font-size: 14px; +} + +th, +td { + text-align: left; + padding: 10px 14px; +} + +th { + font-weight: 700; +} + +.callout { + display: flex; + gap: 12px; + padding: 14px 16px; + border-radius: 12px; + margin: 18px 0; + font-size: 14px; +} + +.callout-content { + flex: 1; +} + +.callout-content> :first-child { + margin-top: 0; +} + +.callout-content> :last-child { + margin-bottom: 0; +} + +.callout-note { + background: rgba(5, 5, 5, 0.08); +} + +.callout-info { + background: rgba(39, 39, 39, 0.08); +} + +.callout-warning { + background: rgba(245, 158, 11, 0.1); +} + +.card { + display: block; + border-radius: 14px; + padding: 16px 18px; + margin: 18px 0; +} + +a.card:hover { + border-color: rgba(0, 0, 0, 0.3); +} + +html.dark a.card:hover { + border-color: rgba(255, 255, 255, 0.3); +} + +.card-title { + font-weight: 700; + font-size: 15px; + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 6px; +} + +.card-body { + font-size: 14px; + color: var(--muted-light); +} + +html.dark .card-body { + color: var(--muted-dark); +} + +.card-body> :first-child { + margin-top: 0; +} + +.card-body> :last-child { + margin-bottom: 0; +} + +.code-group { + position: relative; + margin: 16px 0; + border-radius: 20px; + background: #f4f4f5; + padding: 6px 4px 4px; +} + +html.dark .code-group { + background: #0d0d0d; + border: none; +} + +.code-group-tabs { + display: flex; + align-items: center; + gap: 16px; + padding: 4px 6px 6px; +} + +.code-group-tab { + position: relative; + padding: 6px 2px 8px; + font-family: var(--font); + font-size: 13px; + font-weight: 600; + line-height: 16px; + border: none; + background: none; + cursor: pointer; + color: #71717a; +} + +.code-group-tab.active { + color: var(--text-light); +} + +html.dark .code-group-tab.active { + color: #ffffff; +} + +.code-group-tab.active::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: 0; + height: 2px; + background: var(--text-light); + border-radius: 2px; +} + +html.dark .code-group-tab.active::after { + background: #ffffff; +} + +.code-group-panels { + background: #ffffff; + border-radius: 14px; + overflow: hidden; +} + +html.dark .code-group-panels { + background: #000000; + border-color: rgba(255, 255, 255, 0.06); +} + +.cg-copy-btn { + position: absolute; + top: 10px; + right: 14px; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + border-radius: 6px; + background: none; + color: #71717a; + cursor: pointer; +} + +.cg-copy-btn:hover { + color: var(--text-light); +} + +html.dark .cg-copy-btn:hover { + color: #ffffff; +} + +.cg-copy-btn { + position: absolute; + top: 10px; + right: 14px; + display: flex; + align-items: center; + justify-content: center; + padding: 4px; + border: none; + border-radius: 6px; + background: none; + color: #71717a; + cursor: pointer; +} + +.cg-copy-btn:hover { + color: #ffffff; +} + +.code-group-panel { + display: none; +} + +.code-group-panel.active { + display: block; +} + +main.prose .code-group pre { + margin: 0; + padding: 16px 20px; + border: none; + border-radius: 0; + background: transparent; + color: #e6edf3; +} + +main.prose .code-group pre code { + color: inherit; +} + +.hljs-built_in, +.hljs-builtin-name { + color: #d19a66 !important; +} + +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-number, +.hljs-operator, +.hljs-variable, +.hljs-template-variable { + color: #e5c07b !important; +} + +main.prose a:not(.card) { + color: #2563eb; + text-decoration: underline; + text-underline-offset: 2px; +} + +html.dark main.prose a:not(.card) { + color: #60a5fa; +} + +aside#toc { + width: var(--toc-width); + flex-shrink: 0; + display: none; +} + +@media (min-width: 1100px) { + aside#toc { + display: block; + } +} + +aside#toc .toc-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + font-weight: 600; + line-height: 20px; + color: var(--text-light); + margin-bottom: 2px; +} + +aside#toc .toc-title::before { + content: ""; + width: 16px; + height: 16px; + background: currentColor; + -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 6h16M4 12h10M4 18h14'/%3E%3C/svg%3E") center / contain no-repeat; + mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round'%3E%3Cpath d='M4 6h16M4 12h10M4 18h14'/%3E%3C/svg%3E") center / contain no-repeat; +} + +html.dark aside#toc .toc-title { + color: var(--text-dark); +} + +aside#toc ul { + list-style: none; + margin: 0; + padding: 0; +} + +aside#toc li a { + display: block; + padding: 5px 0 5px 14px; + font-size: 13px; + color: var(--muted-light); +} + +html.dark aside#toc li a { + color: var(--muted-dark); +} + +aside#toc li a:hover { + color: var(--text-light); +} + +html.dark aside#toc li a:hover { + color: var(--text-dark); +} + +aside#toc { + position: sticky; + top: calc(var(--header-height) + 40px); + max-height: calc(100vh - var(--header-height) - 56px); + overflow-y: auto; + padding-top: 0; +} + +footer#site-footer { + padding: 28px 48px; + display: flex; + justify-content: center; + gap: 18px; + font-size: 13px; + color: var(--muted-light); +} + +html.dark footer#site-footer { + + color: var(--muted-dark); +} + +.hljs { + background: none !important; +} + +.ico { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +#search-trigger { + position: absolute; + left: 50%; + transform: translateX(-50%); + display: flex; + align-items: center; + gap: 10px; + width: min(440px, 40vw); + height: 36px; + padding: 0 12px; + border: none; + border-radius: 100px; + background: none; + color: var(--muted-light); + font: inherit; + font-size: 14px; + cursor: pointer; +} + +#search-trigger:hover { + background: rgba(0, 0, 0, 0.05); +} + +html.dark #search-trigger { + color: var(--muted-dark); +} + +html.dark #search-trigger:hover { + background: rgba(255, 255, 255, 0.08); +} + +#search-trigger kbd { + margin-left: auto; + font: 600 11px var(--font); + color: inherit; +} + +.theme-wrap { + position: relative; +} + +#theme-toggle { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border: none; + border-radius: 10px; + background: none; + color: var(--muted-light); + cursor: pointer; +} + +#theme-toggle .ico { + width: 18px; + height: 18px; +} + +#theme-toggle:hover { + background: rgba(0, 0, 0, 0.05); +} + +html.dark #theme-toggle { + color: var(--muted-dark); +} + +html.dark #theme-toggle:hover { + background: rgba(255, 255, 255, 0.08); +} + +#theme-menu { + display: none; + position: absolute; + right: 0; + top: 46px; + width: 180px; + padding: 8px; + border-radius: 16px; + background: var(--bg-light); + box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12); + z-index: 50; +} + +html.dark #theme-menu { + background: var(--bg-dark); +} + +#theme-menu.open { + display: block; +} + +#theme-menu button { + display: flex; + align-items: center; + gap: 10px; + width: 100%; + padding: 8px 10px; + border: none; + border-radius: 10px; + background: none; + color: inherit; + font: inherit; + font-size: 14px; + cursor: pointer; +} + +#theme-menu button:hover { + background: rgba(0, 0, 0, 0.05); +} + +html.dark #theme-menu button:hover { + background: rgba(255, 255, 255, 0.08); +} + +#theme-menu .check { + margin-left: auto; + font-style: normal; + visibility: hidden; +} + +#theme-menu button.on { + font-weight: 600; +} + +#theme-menu button.on .check { + visibility: visible; +} + +#theme-seg { + display: none; + gap: 4px; +} + +#theme-seg button { + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + border: none; + border-radius: 50%; + background: none; + color: var(--muted-light); + cursor: pointer; +} + +html.dark #theme-seg button { + color: var(--muted-dark); +} + +#theme-seg button.on { + background: rgba(0, 0, 0, 0.07); + color: var(--text-light); +} + +html.dark #theme-seg button.on { + background: rgba(255, 255, 255, 0.1); + color: var(--text-dark); +} + +#mobile-bar { + display: none; + position: sticky; + top: 60px; + z-index: 35; + background: var(--bg-light); + align-items: center; + gap: 8px; + padding: 14px 20px; + font-size: 14px; + color: var(--muted-light); +} + +html.dark #mobile-bar { + background: var(--bg-dark); +} + +html.dark #mobile-bar { + color: var(--muted-dark); +} + +#mobile-bar::after { + content: ""; + position: absolute; + top: 100%; + left: 0; + width: 100%; + height: 40px; + background: linear-gradient(to bottom, + rgba(255, 255, 255), + transparent); + pointer-events: none; +} + +html.dark #mobile-bar::after { + background: linear-gradient(to bottom, + rgba(0, 0, 0), + transparent); +} + +#mobile-bar button { + display: flex; + padding: 0; + margin-right: 8px; + border: none; + background: none; + color: inherit; + cursor: pointer; +} + +#mobile-bar .crumb-current { + color: var(--text-light); + font-weight: 600; +} + +html.dark #mobile-bar .crumb-current { + color: var(--text-dark); +} + +.mobile-tabs { + display: none; + position: relative; + margin-bottom: 20px; +} + +.mt-btn { + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; + padding: 8px 12px; + border: none; + background: rgba(25, 25, 25, 0.1); + border-radius: 100px; + color: var(--text-light); + font: inherit; + font-size: 15px; + cursor: pointer; +} + +html.dark .mt-btn { + color: var(--text-dark); +} + +.mt-btn .ico { + color: var(--muted-light); + transition: transform 0.15s ease; +} + +html.dark .mt-btn .ico { + color: var(--muted-dark); +} + +.mobile-tabs.open .mt-btn .ico { + transform: rotate(180deg); +} + +.mt-menu { + display: none; + position: absolute; + top: 100%; + left: 0; + right: 0; + z-index: 5; + padding: 8px; + border-radius: 16px; + background: var(--bg-light); + border: 1px solid var(--border-light); +} + +html.dark .mt-menu { + background: var(--bg-dark); + border: 1px solid var(--border-dark); + +} + +.mobile-tabs.open .mt-menu { + display: block; +} + +.mt-menu a { + display: flex; + align-items: center; + justify-content: space-between; + padding: 8px 12px; + border-radius: 10px; + font-size: 13px; + font-weight: 600; + color: var(--text-light); +} + +html.dark .mt-menu a { + color: var(--text-dark); +} + +.mt-menu a .ico { + visibility: hidden; +} + +.mt-menu a.on .ico { + visibility: visible; +} + +#sidebar { + padding: 36px 0 24px; +} + +#sidebar .group { + margin-bottom: 32px; +} + +#sidebar .group-title { + font-size: 14px; + font-weight: 600; + line-height: 23px; + text-transform: none; + letter-spacing: 0; + color: var(--text-light); + margin: 0 0 10px 16px; +} + +html.dark #sidebar .group-title { + color: var(--text-dark); +} + +#sidebar li a { + padding: 8px 16px; + border-radius: 12px; + font-size: 14px; + line-height: 21px; +} + +#content-col { + padding: 40px 32px 100px 72px; + gap: 92px; +} + +main.prose { + max-width: 720px; +} + +main.prose h2, +main.prose h3 { + scroll-margin-top: 130px; +} + +main.prose .eyebrow { + font-size: 14px; + font-weight: 600; + margin: 0 0 8px; + color: var(--text-light); +} + +html.dark main.prose .eyebrow { + color: var(--text-dark); +} + +main.prose h1 { + font-size: 30px; + font-weight: 700; + letter-spacing: 0; +} + +main.prose .page-description { + font-size: 18px; +} + +main.prose h2 { + font-size: 24px; + font-weight: 600; + border-top: none; + padding-top: 0; + margin: 40px 0 14px; +} + +main.prose h1, +main.prose h2, +main.prose h3 { + color: #111827; +} + +html.dark main.prose h1, +html.dark main.prose h2, +html.dark main.prose h3 { + color: var(--text-dark); +} + +main.prose p, +main.prose li { + color: #4b5563; +} + +html.dark main.prose p, +html.dark main.prose li { + color: #a1a1aa; +} + +main.prose strong { + color: var(--text-light); +} + +html.dark main.prose strong { + color: var(--text-dark); +} + +main.prose img { + border-radius: 30px; +} + +main.prose a:not(.card) { + color: var(--text-light); + font-weight: 600; + text-decoration: underline; +} + +html.dark main.prose a:not(.card) { + color: var(--text-dark); +} + +main.prose pre { + border-radius: 16px; + border-color: transparent; + padding: 16px 20px; + --fade-l: 0px; + --fade-r: 0px; + -webkit-mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent 100%); + mask-image: linear-gradient(to right, transparent 0, #000 var(--fade-l), #000 calc(100% - var(--fade-r)), transparent 100%); +} + +.code-wrap { + position: relative; +} + +.code-wrap .copy-btn { + position: absolute; + top: 12px; + right: 12px; + display: flex; + padding: 6px; + border: none; + border-radius: 8px; + background: none; + color: var(--muted-light); + cursor: pointer; +} + +.code-wrap .copy-btn:hover { + background: rgba(0, 0, 0, 0.06); +} + +html.dark .code-wrap .copy-btn { + color: var(--muted-dark); +} + +html.dark .code-wrap .copy-btn:hover { + background: rgba(255, 255, 255, 0.1); +} + +aside#toc ul { + border-left: none; +} + +aside#toc li a { + padding: 6px 0; + font-size: 14px; + line-height: 20px; +} + +aside#toc li a.active { + color: var(--text-light); + font-weight: 600; +} + +html.dark aside#toc li a.active { + color: var(--text-dark); +} + +#search-modal { + position: fixed; + inset: 0; + z-index: 100; + display: none; +} + +#search-modal.open { + display: block; +} + +.search-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.5); +} + +.search-panel { + position: relative; + width: min(640px, calc(100vw - 24px)); + margin: 52px auto 0; + border-radius: 20px; + overflow: hidden; + background: var(--bg-light); + box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3); +} + +html.dark .search-panel { + background: var(--bg-dark); +} + +.search-input-row { + display: flex; + align-items: center; + gap: 12px; + padding: 18px 20px; +} + +.search-input-row .ico { + width: 18px; + height: 18px; + color: var(--muted-light); +} + +#search-input { + flex: 1; + border: none; + outline: none; + background: transparent; + color: inherit; + font: inherit; + font-size: 18px; +} + +#search-results { + max-height: 60vh; + overflow-y: auto; + padding: 0 6px 8px; +} + +.search-label { + padding: 8px 14px; + font-size: 12px; + color: var(--muted-light); +} + +html.dark .search-label { + color: var(--muted-dark); +} + +.sr { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 8px 12px; + border-radius: 12px; +} + +.sr>.ico { + margin-top: 3px; +} + +.sr.on { + background: rgba(0, 0, 0, 0.06); +} + +.sr-body { + min-width: 0; +} + +.sr .t { + font-size: 14px; + font-weight: 600; + line-height: 1.35; +} + +.sr .s { + display: -webkit-box; + margin-top: 2px; + font-size: 12.5px; + line-height: 1.4; + color: var(--muted-light); + overflow: hidden; + -webkit-line-clamp: 2; + line-clamp: 2; + -webkit-box-orient: vertical; +} + +.sr .p { + margin-top: 2px; + font-size: 12px; + line-height: 1.35; + color: var(--muted-light); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +html.dark .sr .s, +html.dark .sr .p { + color: var(--muted-dark); +} + +.sr .sep { + margin: 0 6px; + opacity: 0.7; +} + +.sr .hit { + font-weight: 700; + color: var(--text-light); +} + +html.dark .sr .hit { + color: var(--text-dark); +} + +.search-footer { + display: flex; + gap: 18px; + padding: 12px 20px; + font-size: 12px; + color: var(--muted-light); +} + +html.dark .search-footer { + + color: var(--muted-dark); +} + +.search-footer .close-hint { + margin-left: auto; +} + +.search-footer kbd { + margin-right: 4px; + padding: 1px 5px; + border-radius: 4px; + font: 11px var(--font); +} + +@media (max-width: 900px) { + :root { + --header-height: 60px; + } + + #navbar::after { + display: none; + } + + #navbar .tabs, + #theme-toggle, + #navbar .nav-top::after { + display: none; + } + + #navbar .nav-top { + padding-right: 20px; + padding-left: 0px; + } + + #navbar .links { + order: 1; + margin-left: auto; + margin-right: 8px; + } + + #search-trigger { + order: 2; + position: static; + transform: none; + width: auto; + padding: 8px; + } + + #search-trigger span, + #search-trigger kbd { + display: none; + } + + #mobile-bar { + display: flex; + } + + .mobile-tabs { + display: block; + margin-top: 50px; + } + + body.nav-open #theme-seg { + display: flex; + background: rgba(25, 25, 25, 0.1); + border-radius: 50px; + padding-left: 5px; + padding-right: 5px; + padding-top: 1px; + padding-bottom: 1px; + } + + #theme-seg button.on { + background: transparent; + } + + #banner { + padding: 10px 40px; + font-size: 12.5px; + } + + body.nav-open { + overflow: hidden; + } + + #sidebar { + position: fixed; + left: 0; + top: var(--sidebar-top, var(--header-height)); + width: 80%; + max-width: 340px; + height: calc(100vh - var(--sidebar-top, var(--header-height))); + height: calc(100dvh - var(--sidebar-top, var(--header-height))); + padding: 20px 16px; + background: var(--bg-light); + transform: translateX(-100%); + transition: transform 0.2s ease; + z-index: 30; + } + + html.dark #sidebar { + background: var(--bg-dark); + } + + #sidebar.open { + transform: translateX(0); + } + + #content-col { + padding: 16px 20px 80px; + } + + aside#toc { + display: none; + } + + .search-panel { + margin-top: 50px; + border-radius: 30px; + } + + .search-footer { + display: none; + } +} + +@media (max-width: 900px) { + + #sidebar li a, + #sidebar .group-title { + font-size: 15px; + } +} + +main.prose p { + margin: 20px 0; +} + +main.prose p, +main.prose li { + line-height: 1.75; +} \ No newline at end of file diff --git a/assets/highlight.css b/assets/highlight.css new file mode 100644 index 0000000..68923ec --- /dev/null +++ b/assets/highlight.css @@ -0,0 +1,107 @@ +pre code.hljs { + display: block; + overflow-x: auto; + padding: 1em; + scrollbar-width: none; + -ms-overflow-style: none; + + ::-webkit-scrollbar { + width: 0; + } +} + +code.hljs { + padding: 3px 5px; +} + +.hljs { + color: #5e5e5e; + background: #0d1117 +} + +.hljs-doctag, +.hljs-keyword, +.hljs-meta .hljs-keyword, +.hljs-template-tag, +.hljs-template-variable, +.hljs-type, +.hljs-variable.language_ { + color: #ff7b72 +} + +.hljs-title, +.hljs-title.class_, +.hljs-title.class_.inherited__, +.hljs-title.function_ { + color: #d2a8ff +} + +.hljs-attr, +.hljs-attribute, +.hljs-literal, +.hljs-meta, +.hljs-number, +.hljs-operator, +.hljs-variable, +.hljs-selector-attr, +.hljs-selector-class, +.hljs-selector-id { + color: #79c0ff +} + +.hljs-regexp, +.hljs-string, +.hljs-meta .hljs-string { + color: #a5d6ff +} + +.hljs-built_in, +.hljs-symbol { + color: #ffa657 +} + +.hljs-comment, +.hljs-code, +.hljs-formula { + color: #8b949e +} + +.hljs-name, +.hljs-quote, +.hljs-selector-tag, +.hljs-selector-pseudo { + color: #7ee787 +} + +.hljs-subst { + color: #5e5e5e +} + +.hljs-section { + color: #1f6feb; + font-weight: bold +} + +.hljs-bullet { + color: #f2cc60 +} + +.hljs-emphasis { + color: #5e5e5e; + font-style: italic +} + +.hljs-strong { + color: #5e5e5e; + font-weight: bold +} + +.hljs-addition { + color: #aff5b4; + background-color: #033a16 +} + +.hljs-deletion { + color: #ffdcd7; + background-color: #67060c +} \ No newline at end of file diff --git a/assets/images/android-chrome-192x192.png b/assets/images/android-chrome-192x192.png new file mode 100644 index 0000000..008e249 Binary files /dev/null and b/assets/images/android-chrome-192x192.png differ diff --git a/assets/images/android-chrome-512x512.png b/assets/images/android-chrome-512x512.png new file mode 100644 index 0000000..fde4038 Binary files /dev/null and b/assets/images/android-chrome-512x512.png differ diff --git a/assets/images/android-tv/site-preview.png b/assets/images/android-tv/site-preview.png new file mode 100644 index 0000000..ef58b3f Binary files /dev/null and b/assets/images/android-tv/site-preview.png differ diff --git a/assets/images/android/site_preview.png b/assets/images/android/site_preview.png new file mode 100644 index 0000000..74b1163 Binary files /dev/null and b/assets/images/android/site_preview.png differ diff --git a/assets/images/apple-touch-icon.png b/assets/images/apple-touch-icon.png new file mode 100644 index 0000000..bae7588 Binary files /dev/null and b/assets/images/apple-touch-icon.png differ diff --git a/assets/images/dark_logo.png b/assets/images/dark_logo.png new file mode 100644 index 0000000..59fc568 Binary files /dev/null and b/assets/images/dark_logo.png differ diff --git a/assets/images/desktop-preview.png b/assets/images/desktop-preview.png new file mode 100644 index 0000000..ffefd16 Binary files /dev/null and b/assets/images/desktop-preview.png differ diff --git a/assets/images/desktop/site_preview.png b/assets/images/desktop/site_preview.png new file mode 100644 index 0000000..29ce40e Binary files /dev/null and b/assets/images/desktop/site_preview.png differ diff --git a/assets/images/devices.png b/assets/images/devices.png new file mode 100644 index 0000000..ee3c308 Binary files /dev/null and b/assets/images/devices.png differ diff --git a/assets/images/favicon-16x16.png b/assets/images/favicon-16x16.png new file mode 100644 index 0000000..e95206d Binary files /dev/null and b/assets/images/favicon-16x16.png differ diff --git a/assets/images/favicon-32x32.png b/assets/images/favicon-32x32.png new file mode 100644 index 0000000..60c276c Binary files /dev/null and b/assets/images/favicon-32x32.png differ diff --git a/assets/images/favicon-96x96.png b/assets/images/favicon-96x96.png new file mode 100644 index 0000000..e878146 Binary files /dev/null and b/assets/images/favicon-96x96.png differ diff --git a/assets/images/favicon.ico b/assets/images/favicon.ico new file mode 100644 index 0000000..9fdbb82 Binary files /dev/null and b/assets/images/favicon.ico differ diff --git a/assets/images/header_logo.png b/assets/images/header_logo.png new file mode 100644 index 0000000..a80a11d Binary files /dev/null and b/assets/images/header_logo.png differ diff --git a/assets/images/logo.png b/assets/images/logo.png new file mode 100644 index 0000000..9fdbb82 Binary files /dev/null and b/assets/images/logo.png differ diff --git a/assets/images/partners/fmhy.png b/assets/images/partners/fmhy.png new file mode 100644 index 0000000..eb21dc4 Binary files /dev/null and b/assets/images/partners/fmhy.png differ diff --git a/assets/images/partners/piracy.png b/assets/images/partners/piracy.png new file mode 100644 index 0000000..abbb555 Binary files /dev/null and b/assets/images/partners/piracy.png differ diff --git a/assets/images/rounded-banner.png b/assets/images/rounded-banner.png new file mode 100644 index 0000000..8341957 Binary files /dev/null and b/assets/images/rounded-banner.png differ diff --git a/assets/images/thumbnail.png b/assets/images/thumbnail.png new file mode 100644 index 0000000..22d4d05 Binary files /dev/null and b/assets/images/thumbnail.png differ diff --git a/assets/mintlify-overrides.css b/assets/mintlify-overrides.css new file mode 100644 index 0000000..14b277a --- /dev/null +++ b/assets/mintlify-overrides.css @@ -0,0 +1,121 @@ +:root { + --table-header-text: #000000; + --vyla-black: #000000; + --vyla-white: #ffffff; + --vyla-border-light: rgba(0, 0, 0, 0.12); + --vyla-border-dark: rgba(255, 255, 255, 0.16); + --banner-bg: #ffffff; + --banner-text: #000000; +} + +.dark { + --table-header-text: #ffffff; + --banner-bg: #000000; + --banner-text: #ffffff; +} + +table th { + color: var(--table-header-text) !important; +} + +#banner, +#banner *, +#banner p, +#banner a { + color: var(--banner-text) !important; +} + +.data-\[is-opaque\=true\]\:bg-background-light[data-is-opaque=true] { + background-color: rgba(255, 255, 255, 0.2) !important; +} + +.data-\[is-opaque\=true\]\:dark\:bg-background-dark\/75:is(.dark *)[data-is-opaque=true] { + background-color: rgba(0, 0, 0, 0.2) !important; +} + +#banner a { + text-decoration: underline !important; + text-underline-offset: 3px; +} + +#banner button { + color: var(--banner-text) !important; +} + +#navbar a[href*="gitlab"], +#navbar a[href*="discord"], +#navbar a[href="/"] { + border-radius: 999px !important; +} + +#navbar a[href="/"] { + padding-left: 16px !important; + padding-right: 16px !important; +} + +.dark\:border-gray-300\/\[0\.06\]:is(.dark *) { + border-color: transparent !important; +} + +.dark\:ring-0:is(.dark *), +.dark\:ring-1:is(.dark *), +.ring-1, +.ring-2 { + box-shadow: none !important; +} + +.dark\:hover\:\!border-primary-light:hover:is(.dark *) { + background-color: transparent !important; + border-color: transparent !important; +} + +.hover\:\!border-primary:hover { + background-color: transparent !important; + border-color: transparent !important; +} + +.dark\:border-white\/10:is(.dark *) { + border-color: transparent !important; +} + +.dark\:bg-white\/10:is(.dark *) { + background-color: rgba(25, 25, 25, 0.1) !important; +} + +html.dark .code-block-background { + background-color: rgba(5, 5, 5) !important; + border-color: transparent !important; +} + +html .code-block-background { + background-color: rgba(250, 250, 250) !important; + border-color: transparent !important; +} + +[data-fade-overlay] { + background-color: transparent !important; +} + +.border { + border-color: transparent !important; +} + +.h-\[1\.5px\] { + height: 0px !important; +} + +.prose :where(img):not(:where([class~=not-prose], [class~=not-prose] *)) { + border-radius: 30px; +} + +html.dark [data-fade-overlay] { + background: transparent !important; +} + +.dark\:text-gray-500:is(.dark *) { + display: none !important; +} + +.px-6 { + padding-left: 0px !important; +} \ No newline at end of file diff --git a/concepts/providers/index.html b/concepts/providers/index.html new file mode 100644 index 0000000..fae5cc3 --- /dev/null +++ b/concepts/providers/index.html @@ -0,0 +1,293 @@ + + + + + + + Providers — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Providers

+

How Vyla SDK sources are configured, selected, and verified.

+

Vyla SDK aggregates multiple independent providers. Each has different internal mechanisms, reliability + profiles, and timeout characteristics. This page explains the source layer that powers both direct SDK use and + the optional Stream API.

+
+
+

Provider availability and performance can vary based on your network environment. When self-hosting, + providers may work differently than on residential connections due to IP-based blocking from upstream + sources.

+
+
+
+

How Providers Are Used

+

When your app calls getStream()—directly or through Stream API:

+
    +
  1. All active (non-disabled) providers are queried simultaneously with per-source random + jitter to stagger upstream requests
  2. +
  3. Each provider has its own timeout, retry count, and backoff + delay
  4. +
  5. Every returned URL is verified — first via a HEAD request to the raw upstream URL, then + via a proxied HLS playability check
  6. +
  7. Only URLs that pass both verification steps appear in the response as source events
  8. +
+

Because the endpoint is SSE, verified sources are emitted immediately as each provider + resolves — your player receives them progressively. The stream closes once every provider has + either resolved or timed out.

+
+
+

Call sdk.getSources(true) to inspect enabled providers and + sdk.probeAllSources() for a live availability signal. If you use Stream API, its /health endpoint exposes probes over HTTP. +

+
+
+
+

Provider Configuration

+

Each provider is defined with the following properties:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldDescription
keyInternal identifier used in API responses and the source field
labelHuman-readable name shown in the label field
proxyParamShort query parameter used to route proxied requests
timeoutMaximum time (ms) to wait for a stream URL
sourcesTimeoutOptional secondary timeout (ms) for an internal source-listing step, used by some providers
jitterMax random delay (ms) before the request fires, to stagger upstream hits
retriesNumber of retry attempts on failure
skipProxyIf true, stream URLs are returned as-is without proxy wrapping
disabledIf true, the provider is excluded from all requests
+

Providers with disabled: true are completely skipped — they won't appear in health checks or + fanout responses.

+
+

Response Time

+

The first source event typically arrives within 3–8 seconds. Subsequent sources + stream in as each provider resolves. The stream closes once the slowest configured provider + times out — in the worst case this can be 60+ seconds (e.g. cinezo has a 60 s timeout), though + most sources emit well before then.

+

Best practice: always show a loading state, begin playback on the first source + event, and implement a client-side timeout for the overall stream. See the Error Handling guide.

+
+

Caching

+

Results are cached in-memory with a 5-minute TTL. The per-source stream cache is keyed by + source_key + tmdb_id + season + episode; the aggregate source-results cache is keyed by + tmdb_id + season + episode + base. Cache hits skip the provider fetch entirely and return + near-instantly. +

+
CACHE_TTL = 5 minutes
+
+

The cache resets on server restart. Cache size is reported in the /api/health response under the + cache field. +

+
+

The HLS Proxy

+

All provider stream URLs are wrapped in the server proxy (unless skipProxy: true):

+
http://localhost:7860/api?url=<encoded_stream_url>&<proxyParam>=1
+
+

When your player fetches this path from your self-hosted instance, the server:

+
    +
  1. Fetches the upstream M3U8 with the correct Referer, Origin, and any + provider-specific headers
  2. +
  3. Rewrites all segment URLs and encryption key URIs in the playlist to route through the same proxy
  4. +
  5. Returns the rewritten playlist with Access-Control-Allow-Origin: *
  6. +
+

This means your player never talks to the provider directly — all HLS traffic flows through + the proxy, keeping CORS and rate-limiting issues fully contained.

+
+

Stream Verification

+

Before a URL is emitted as a source event, it passes two verification steps:

+
    +
  1. verifyStream — a HEAD request to the raw upstream URL (skipped for + SKIP_VERIFY sources). Must return HTTP < 400. +
  2. +
  3. verifyPlayable — fetches the proxied URL, validates #EXTM3U, + checks for #EXTINF or #EXT-X-STREAM-INF, then fetches the first segment with a + range request.
  4. +
+

Sources that fail either step — HTTP errors, timeouts, non-HLS/non-MP4 content — are silently dropped. MP4 + sources skip the #EXTM3U check and are verified via HEAD + content-type inspection instead. HTTP + 429 responses and timeout/abort errors are treated as passing (stream exists but is + rate-limited).

+
+
+

/test runs the same two-step verification pipeline as /movie and + /tv. Check the ok field in the response body — HTTP status is always + 200 regardless of result. +

+
+
+
+

Disabled Providers

+

Providers marked disabled: true in config are completely excluded from:

+
    +
  • /api/movie and /api/tv fanout
  • +
  • /api/health probes
  • +
  • GET /api?sources_meta listings
  • +
+

If a provider you expect to see is missing from responses, check whether it's been disabled.

+
+

Multi-URL Sources

+

Some providers return multiple candidate stream URLs (via allUrls). For these, verification runs + through each candidate in order and returns the first one that passes. If none pass, the source is dropped. +

+

For providers with SKIP_VERIFY, all candidate URLs are returned as separate source events + without verification. These are not distinguished by numbered labels in the response.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..9fdbb82 Binary files /dev/null and b/favicon.ico differ diff --git a/go/index.html b/go/index.html new file mode 100644 index 0000000..ab50548 --- /dev/null +++ b/go/index.html @@ -0,0 +1,152 @@ + + + + + + + Vyla Go — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Vyla Go

+

The Android control center for running the Vyla media stack locally.

+ A collage of films and shows available through a Vyla experience +

Vyla Go is the Android application in the Vyla ecosystem. It prepares and runs the local + services that power your media experience, then opens the interface in a secure embedded window. It is + designed for people who want Vyla on their Android device without managing a separate server process or + service lifecycle by hand.

+

Downloads

+

Vyla Go is available for Android. Get the latest version below:

+ + + + + + + + + + + + + +
PlatformDownload
AndroidDownload .apk
+

Inside the releases page, you can find the latest version of Vyla Go for Android, find the .apk + file, and install it on your device.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/guides/error-handling/index.html b/guides/error-handling/index.html new file mode 100644 index 0000000..896109b --- /dev/null +++ b/guides/error-handling/index.html @@ -0,0 +1,432 @@ + + + + + + + Stream API error handling — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Stream API error handling

+

Handle SSE failures, source fallbacks, and edge cases in an optional Stream API + client.

+

The /movie and /tv endpoints stream via SSE — error handling looks different from a + standard JSON API. This page covers every failure mode and the recommended pattern for each.

+
+
+

These patterns apply when you choose the optional Stream API boundary. SDK integrations should apply the + same fallback principles inside their own server-side request flow.

+
+
+
+

Error Response Shape

+

Non-streaming endpoints (/health, /subtitles, /downloads, + /test) return standard JSON errors: +

+
{ "error": "description of what went wrong" }
+
+ + + + + + + + + + + + + + + + + + + + + +
HTTP StatusWhen
400Missing or invalid request parameters
404No data found (subtitles, downloads)
500Unexpected server error
+

For /movie and /tv, HTTP 200 is returned immediately when the SSE + stream opens. Failures surface as zero source events followed by a + done event with total: 0 — not as HTTP error codes. +

+
+

Handling No Sources

+

The most common failure — all providers failed to return a playable stream. Detect it by checking + total in the done event. +

+
const BASE = 'http://localhost:7860';
+
+const res     = await fetch(`${BASE}/movie?id=550`);
+const reader  = res.body!.getReader();
+const decoder = new TextDecoder();
+let buffer    = '';
+let started   = false;
+
+while (true) {
+  const { done, value } = await reader.read();
+  if (done) break;
+
+  buffer += decoder.decode(value, { stream: true });
+  const lines = buffer.split('\n');
+  buffer = lines.pop()!;
+
+  for (const line of lines) {
+    if (!line.startsWith('data: ')) continue;
+    const event = JSON.parse(line.slice(6));
+
+    if (event.type === 'source') { started = true; playSource(event.source.url); }
+
+    if (event.type === 'done' && !started) {
+      showError('This title is currently unavailable. Try again later.');
+    }
+  }
+}
+
+
+

Source Fallback Queue

+

Sources stream in as providers resolve. Queue every source event URL and fall back to the next + one if playback fails mid-stream:

+
import Hls from 'hls.js';
+
+const BASE = 'http://localhost:7860';
+
+async function playWithFallback(
+  videoEl: HTMLVideoElement,
+  tmdbId: number,
+  season?: number,
+  episode?: number,
+  onSourceChange?: (label: string) => void
+) {
+  const endpoint = season && episode
+    ? `${BASE}/tv?id=${tmdbId}&season=${season}&episode=${episode}`
+    : `${BASE}/movie?id=${tmdbId}`;
+
+  const queue: { url: string; label: string }[] = [];
+  let started = false;
+  let hls: Hls | null = null;
+
+  function tryNext() {
+    if (!queue.length) {
+      console.error('All sources exhausted');
+      return;
+    }
+
+    const { url, label } = queue.shift()!;
+    onSourceChange?.(label);
+
+    hls?.destroy();
+    hls = new Hls({ maxBufferLength: 30, maxMaxBufferLength: 60 });
+
+    hls.on(Hls.Events.ERROR, (_, err) => {
+      if (err.fatal) {
+        console.warn(`Source "${label}" failed:`, err.type);
+        tryNext();
+      }
+    });
+
+    hls.loadSource(url);
+    hls.attachMedia(videoEl);
+    hls.on(Hls.Events.MANIFEST_PARSED, () => videoEl.play());
+  }
+
+  const res = await fetch(endpoint);
+  const reader  = res.body!.getReader();
+  const decoder = new TextDecoder();
+  let buffer    = '';
+
+  while (true) {
+    const { done, value } = await reader.read();
+    if (done) break;
+
+    buffer += decoder.decode(value, { stream: true });
+    const lines = buffer.split('\n');
+    buffer = lines.pop()!;
+
+    for (const line of lines) {
+      if (!line.startsWith('data: ')) continue;
+      const event = JSON.parse(line.slice(6));
+
+      if (event.type === 'source') {
+        queue.push({ url: event.source.url, label: event.source.label });
+        if (!started) { started = true; tryNext(); }
+      }
+    }
+  }
+
+  return () => hls?.destroy();
+}
+
+
+

Network Timeouts

+

The SSE stream can stay open 20+ seconds while slow providers resolve. Set a client-side timeout using + AbortController: +

+
async function fetchStreamWithTimeout(
+  url: string,
+  timeoutMs = 20000
+): Promise<ReadableStreamDefaultReader<Uint8Array>> {
+  const controller = new AbortController();
+  const timer = setTimeout(() => controller.abort(), timeoutMs);
+
+  try {
+    const res = await fetch(url, {
+      signal: controller.signal
+    });
+    clearTimeout(timer);
+    return res.body!.getReader();
+  } catch (err: any) {
+    clearTimeout(timer);
+    if (err.name === 'AbortError') throw new Error('Stream timed out');
+    throw err;
+  }
+}
+
+
+

Handling Missing Subtitles

+

Subtitles arrive in the meta event. Always guard against an empty array:

+
if (event.type === 'meta') {
+  if (!event.subtitles.length) {
+    subtitleButton.style.display = 'none';
+  } else {
+    attachSubtitles(videoEl, event.subtitles);
+  }
+}
+
+
+

Retry Logic for Stream Failures

+

Add a top-level retry for complete stream failures (network errors, server 5xx before the stream opens):

+
async function fetchStreamWithRetry(
+  url: string,
+  attempts = 3,
+  delayMs = 2000
+): Promise<Response> {
+  for (let i = 0; i < attempts; i++) {
+    try {
+      const res = await fetch(url);
+      if (res.ok) return res;
+      if (i === attempts - 1) throw new Error(`HTTP ${res.status}`);
+    } catch (err) {
+      if (i === attempts - 1) throw err;
+      await new Promise(r => setTimeout(r, delayMs * (i + 1)));
+    }
+  }
+  throw new Error('Max retries exceeded');
+}
+
+
+

Checking Health Before Requests

+

If you're building a dashboard, check provider health before opening a stream to give users an early warning: +

+
async function checkAndStream(tmdbId: number) {
+  const health = await fetch(
+    'http://localhost:7860/api/health'
+  ).then(r => r.json());
+
+  if (health.status === 'degraded') {
+    const down = Object.entries(health.sources)
+      .filter(([, v]: any) => !v.ok)
+      .map(([k]) => k);
+    console.warn(`Degraded providers: ${down.length}`);
+  }
+
+  return fetch(
+    `http://localhost:7860/movie?id=${tmdbId}`
+  );
+}
+
+
+

Full Error Boundary (React)

+
import { useState, useCallback, useRef } from 'react';
+import Hls from 'hls.js';
+
+const BASE = 'http://localhost:7860';
+
+type PlayerState =
+  | { status: 'idle' }
+  | { status: 'loading' }
+  | { status: 'playing'; sourceLabel: string }
+  | { status: 'error'; message: string };
+
+export function useStreamPlayer(tmdbId: number) {
+  const [state, setState] = useState<PlayerState>({ status: 'idle' });
+  const hlsRef = useRef<Hls | null>(null);
+
+  const load = useCallback(async (videoEl: HTMLVideoElement) => {
+    setState({ status: 'loading' });
+
+    const queue: { url: string; label: string }[] = [];
+    let started = false;
+
+    function tryNext() {
+      if (!queue.length) {
+        setState({ status: 'error', message: 'All sources failed.' });
+        return;
+      }
+
+      const { url, label } = queue.shift()!;
+      hlsRef.current?.destroy();
+
+      const hls = new Hls();
+      hlsRef.current = hls;
+
+      hls.on(Hls.Events.MANIFEST_PARSED, () => {
+        setState({ status: 'playing', sourceLabel: label });
+        videoEl.play();
+      });
+      hls.on(Hls.Events.ERROR, (_, err) => {
+        if (err.fatal) { hls.destroy(); tryNext(); }
+      });
+
+      hls.loadSource(url);
+      hls.attachMedia(videoEl);
+    }
+
+    try {
+      const res = await fetch(`${BASE}/movie?id=${tmdbId}`);
+      const reader  = res.body!.getReader();
+      const decoder = new TextDecoder();
+      let buffer    = '';
+
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split('\n');
+        buffer = lines.pop()!;
+
+        for (const line of lines) {
+          if (!line.startsWith('data: ')) continue;
+          const event = JSON.parse(line.slice(6));
+
+          if (event.type === 'source') {
+            queue.push({ url: event.source.url, label: event.source.label });
+            if (!started) { started = true; tryNext(); }
+          }
+
+          if (event.type === 'done' && !started) {
+            setState({ status: 'error', message: 'No sources available for this title.' });
+          }
+        }
+      }
+    } catch {
+      setState({ status: 'error', message: 'Network error. Check your connection.' });
+    }
+  }, [tmdbId]);
+
+  return { state, load };
+}
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/guides/integrations/index.html b/guides/integrations/index.html new file mode 100644 index 0000000..c042cec --- /dev/null +++ b/guides/integrations/index.html @@ -0,0 +1,1253 @@ + + + + + + + Stream API client integration — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Stream API client integration

+

Build an optional Stream API player client with HLS/MP4 handling, SSE sources, + subtitles, and fallbacks.

+

Every source URL from /movie and /tv is either an HLS stream + (.m3u8) or an MP4 file — your player code must handle both. HLS goes through + HLS.js; MP4 sets video.src directly. The detection logic is the same everywhere.

+
+
+

These examples are for clients that use the optional self-hosted Stream API. If your product already has + a Node.js backend, prefer Vyla SDK directly and expose only the API your product + needs.

+
+
+
+

Source Type Detection

+

This is the single utility everything below depends on. A URL is MP4 if the proxy path ends in + .mp4 or the upstream served a video content-type that isn't HLS. +

+
export function isMp4(url: string): boolean {
+  try {
+    const inner = new URL(url).searchParams.get('url') ?? url;
+    return /\.mp4(\?|$)/i.test(inner) || /\.mkv(\?|$)/i.test(inner);
+  } catch {
+    return /\.mp4(\?|$)/i.test(url);
+  }
+}
+
+
+

Installation

+
+
+
+
+
npm install hls.js
+
+
+
yarn add hls.js
+
+
+
<script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
+
+
+
+
+

How the Stream Works

+

/movie and /tv are Server-Sent Events — not JSON. Each response is + a live stream of data: lines. Three event types arrive in sequence:

+ + + + + + + + + + + + + + + + + + + + + + + + + +
EventWhenContains
metaImmediately, before any provider resolvesTMDB metadata + all subtitle tracks
sourceOnce per working provider, as each resolvesProxied URL (HLS or MP4) + provider label
doneAfter all providers finish or time outTotal working source count
+

Start playback on the first source event — don't wait for done. + Queue every subsequent source as a fallback.

+
+

Minimal HTML Player

+

Zero dependencies beyond HLS.js. Handles both HLS and MP4 correctly.

+
<!DOCTYPE html>
+<html>
+<head>
+  <script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
+</head>
+<body>
+  <video id="player" controls style="width: 100%; max-width: 900px;"></video>
+  <div id="status">Loading…</div>
+
+  <script>
+    const BASE   = 'http://localhost:7860';
+    const video  = document.getElementById('player');
+    const status = document.getElementById('status');
+    let hlsInstance = null;
+
+    function isMp4(url) {
+      try {
+        const inner = new URL(url).searchParams.get('url') ?? url;
+        return /\.(mp4|mkv)(\?|$)/i.test(inner);
+      } catch { return /\.(mp4|mkv)(\?|$)/i.test(url); }
+    }
+
+    function attachSource(url, label) {
+      status.textContent = `Loading ${label}…`;
+      hlsInstance?.destroy();
+      hlsInstance = null;
+
+      if (isMp4(url)) {
+        video.src = url;
+        video.play().catch(() => {});
+        status.textContent = `Playing ${label} (MP4)`;
+        return;
+      }
+
+      if (Hls.isSupported()) {
+        hlsInstance = new Hls();
+        hlsInstance.loadSource(url);
+        hlsInstance.attachMedia(video);
+        hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
+          video.play().catch(() => {});
+          status.textContent = `Playing ${label}`;
+        });
+        return;
+      }
+
+      if (video.canPlayType('application/vnd.apple.mpegurl')) {
+        video.src = url;
+        video.play().catch(() => {});
+        status.textContent = `Playing ${label}`;
+        return;
+      }
+
+      status.textContent = 'HLS not supported in this browser.';
+    }
+
+    async function play(tmdbId) {
+      const res = await fetch(`${BASE}/movie?id=${tmdbId}`);
+      const reader  = res.body.getReader();
+      const decoder = new TextDecoder();
+      let buffer    = '';
+      let started   = false;
+
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split('\n');
+        buffer = lines.pop();
+
+        for (const line of lines) {
+          if (!line.startsWith('data: ')) continue;
+          const event = JSON.parse(line.slice(6));
+
+          if (event.type === 'source' && !started) {
+            started = true;
+            attachSource(event.source.url, event.source.label);
+          }
+          if (event.type === 'done' && !started) {
+            status.textContent = 'No sources available.';
+          }
+        }
+      }
+    }
+
+    play(550);
+  </script>
+</body>
+</html>
+
+
+ +

Source switching, fallback queue, subtitle tracks, quality selector. All source types handled.

+
<!DOCTYPE html>
+<html>
+<head>
+  <script src="https://cdn.jsdelivr.net/npm/hls.js@latest/dist/hls.min.js"></script>
+  <style>
+    * { box-sizing: border-box; }
+    body      { font-family: sans-serif; background: #0f0f0f; color: #fff; padding: 1rem; margin: 0; }
+    video     { width: 100%; max-width: 960px; display: block; background: #000; border-radius: 6px; }
+    #controls { max-width: 960px; margin-top: 0.5rem; display: flex; gap: 0.5rem; flex-wrap: wrap; align-items: center; }
+    #sources  { display: flex; gap: 0.4rem; flex-wrap: wrap; margin-top: 0.4rem; max-width: 960px; }
+    button    { padding: 0.35rem 0.75rem; background: #1e1e1e; color: #ccc; border: 1px solid #333;
+                border-radius: 4px; cursor: pointer; font-size: 0.8rem; }
+    button:hover   { background: #2a2a2a; }
+    button.active  { background: #fff; color: #111; border-color: #fff; }
+    select    { background: #1e1e1e; color: #ccc; border: 1px solid #333; border-radius: 4px;
+                padding: 0.35rem 0.5rem; font-size: 0.8rem; }
+    #status   { font-size: 0.8rem; color: #666; margin-top: 0.4rem; max-width: 960px; }
+    .badge    { font-size: 0.65rem; background: #333; color: #aaa; padding: 1px 5px;
+                border-radius: 3px; margin-left: 4px; vertical-align: middle; }
+  </style>
+</head>
+<body>
+  <video id="player" controls></video>
+  <div id="controls">
+    <select id="quality" style="display:none"><option value="-1">Quality: Auto</option></select>
+  </div>
+  <div id="sources"></div>
+  <div id="status">Connecting…</div>
+
+  <script>
+    const BASE      = 'http://localhost:7860';
+    const video     = document.getElementById('player');
+    const statusEl  = document.getElementById('status');
+    const sourcesEl = document.getElementById('sources');
+    const qualitySel = document.getElementById('quality');
+
+    let hlsInstance = null;
+    let allSources  = [];
+    let activeUrl   = null;
+
+    function isMp4(url) {
+      try {
+        const inner = new URL(url).searchParams.get('url') ?? url;
+        return /\.(mp4|mkv)(\?|$)/i.test(inner);
+      } catch { return /\.(mp4|mkv)(\?|$)/i.test(url); }
+    }
+
+    function setActive(url) {
+      activeUrl = url;
+      sourcesEl.querySelectorAll('button').forEach(b => {
+        b.classList.toggle('active', b.dataset.url === url);
+      });
+    }
+
+    function switchSource(url, label) {
+      setActive(url);
+      statusEl.textContent = `Loading ${label}…`;
+      qualitySel.style.display = 'none';
+      qualitySel.innerHTML = '<option value="-1">Quality: Auto</option>';
+
+      hlsInstance?.destroy();
+      hlsInstance = null;
+
+      if (isMp4(url)) {
+        video.src = url;
+        video.play().catch(() => {});
+        statusEl.textContent = `Playing ${label} · MP4`;
+        return;
+      }
+
+      if (Hls.isSupported()) {
+        hlsInstance = new Hls({ startLevel: -1 });
+        hlsInstance.loadSource(url);
+        hlsInstance.attachMedia(video);
+
+        hlsInstance.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
+          video.play().catch(() => {});
+          statusEl.textContent = `Playing ${label} · HLS`;
+
+          if (data.levels.length > 1) {
+            data.levels.forEach((level, i) => {
+              const opt = document.createElement('option');
+              opt.value = i;
+              opt.textContent = level.height ? `${level.height}p` : `Level ${i}`;
+              qualitySel.appendChild(opt);
+            });
+            qualitySel.style.display = '';
+          }
+        });
+
+        hlsInstance.on(Hls.Events.LEVEL_SWITCHED, (_, data) => {
+          qualitySel.value = hlsInstance.autoLevelEnabled ? '-1' : data.level;
+        });
+
+        hlsInstance.on(Hls.Events.ERROR, (_, err) => {
+          if (err.fatal) {
+            statusEl.textContent = `${label} failed — trying next…`;
+            tryFallback(url);
+          }
+        });
+        return;
+      }
+
+      if (video.canPlayType('application/vnd.apple.mpegurl')) {
+        video.src = url;
+        video.play().catch(() => {});
+        statusEl.textContent = `Playing ${label} · HLS`;
+        return;
+      }
+
+      statusEl.textContent = 'HLS not supported — try a different browser.';
+    }
+
+    function tryFallback(failedUrl) {
+      const next = allSources.find(s => s.url !== failedUrl && s.url !== activeUrl);
+      if (next) switchSource(next.url, next.label);
+      else statusEl.textContent = 'All sources failed.';
+    }
+
+    function addSourceButton(source) {
+      allSources.push(source);
+      const { url, label } = source;
+      const btn = document.createElement('button');
+      btn.dataset.url = url;
+      btn.innerHTML = label + (isMp4(url) ? '<span class="badge">MP4</span>' : '<span class="badge">HLS</span>');
+      btn.onclick = () => switchSource(url, label);
+      sourcesEl.appendChild(btn);
+    }
+
+    qualitySel.onchange = () => {
+      if (hlsInstance) hlsInstance.currentLevel = parseInt(qualitySel.value);
+    };
+
+    async function loadContent(tmdbId, season, episode) {
+      const url = (season && episode)
+        ? `${BASE}/tv?id=${tmdbId}&season=${season}&episode=${episode}`
+        : `${BASE}/movie?id=${tmdbId}`;
+
+      allSources = [];
+      sourcesEl.innerHTML = '';
+      statusEl.textContent = 'Connecting…';
+      let started = false;
+
+      const res     = await fetch(url);
+      const reader  = res.body.getReader();
+      const decoder = new TextDecoder();
+      let buffer    = '';
+
+      while (true) {
+        const { done, value } = await reader.read();
+        if (done) break;
+        buffer += decoder.decode(value, { stream: true });
+        const lines = buffer.split('\n');
+        buffer = lines.pop();
+
+        for (const line of lines) {
+          if (!line.startsWith('data: ')) continue;
+          const event = JSON.parse(line.slice(6));
+
+          if (event.type === 'meta') {
+            statusEl.textContent = `Loading ${event.meta?.title ?? event.meta?.name ?? ''}…`;
+            Array.from(video.querySelectorAll('track')).forEach(t => t.remove());
+            (event.subtitles ?? []).forEach((sub, i) => {
+              const track = Object.assign(document.createElement('track'), {
+                kind: 'subtitles', label: sub.label, src: sub.file,
+              });
+              if (i === 0) track.default = true;
+              video.appendChild(track);
+            });
+          }
+
+          if (event.type === 'source') {
+            addSourceButton(event.source);
+            if (!started) {
+              started = true;
+              switchSource(event.source.url, event.source.label);
+            }
+          }
+
+          if (event.type === 'done' && !started) {
+            statusEl.textContent = 'No sources available.';
+          }
+        }
+      }
+    }
+
+    loadContent(550);
+  </script>
+</body>
+</html>
+
+
+

React

+

useStream Hook

+

Handles SSE parsing, abort on unmount, and re-fetch on param change. Drop-in for any framework.

+
import { useState, useEffect, useRef } from 'react';
+
+const BASE = 'http://localhost:7860';
+
+export interface StreamSource {
+  source: string;
+  label:  string;
+  url:    string;
+}
+
+export interface StreamSubtitle {
+  label:  string;
+  file:   string;
+  type:   string;
+  source: string;
+}
+
+export interface StreamState {
+  sources:   StreamSource[];
+  subtitles: StreamSubtitle[];
+  meta:      Record<string, unknown> | null;
+  loading:   boolean;
+  done:      boolean;
+  error:     string | null;
+}
+
+export function useStream(
+  tmdbId:  number | null,
+  season?: number | null,
+  episode?: number | null,
+): StreamState {
+  const [state, setState] = useState<StreamState>({
+    sources: [], subtitles: [], meta: null, loading: false, done: false, error: null,
+  });
+  const abortRef = useRef<AbortController | null>(null);
+
+  useEffect(() => {
+    if (!tmdbId) return;
+
+    abortRef.current?.abort();
+    const controller = new AbortController();
+    abortRef.current = controller;
+
+    const url = season && episode
+      ? `${BASE}/tv?id=${tmdbId}&season=${season}&episode=${episode}`
+      : `${BASE}/movie?id=${tmdbId}`;
+
+    setState({ sources: [], subtitles: [], meta: null, loading: true, done: false, error: null });
+
+    (async () => {
+      try {
+        const res     = await fetch(url, {
+          signal: controller.signal,
+        });
+        const reader  = res.body!.getReader();
+        const decoder = new TextDecoder();
+        let buffer    = '';
+
+        while (true) {
+          const { done, value } = await reader.read();
+          if (done) break;
+          buffer += decoder.decode(value, { stream: true });
+          const lines = buffer.split('\n');
+          buffer = lines.pop()!;
+
+          for (const line of lines) {
+            if (!line.startsWith('data: ')) continue;
+            const event = JSON.parse(line.slice(6));
+            if (event.type === 'meta')
+              setState(s => ({ ...s, meta: event.meta ?? null, subtitles: event.subtitles ?? [] }));
+            if (event.type === 'source')
+              setState(s => ({ ...s, sources: [...s.sources, event.source] }));
+            if (event.type === 'done')
+              setState(s => ({ ...s, loading: false, done: true }));
+          }
+        }
+      } catch (err: any) {
+        if (err.name !== 'AbortError')
+          setState(s => ({ ...s, loading: false, error: err.message }));
+      }
+    })();
+
+    return () => controller.abort();
+  }, [tmdbId, season, episode]);
+
+  return state;
+}
+
+

attachSource Utility

+

Centralises HLS vs MP4 detection so every component shares one path.

+
import Hls from 'hls.js';
+
+export function isMp4(url: string): boolean {
+  try {
+    const inner = new URL(url).searchParams.get('url') ?? url;
+    return /\.(mp4|mkv)(\?|$)/i.test(inner);
+  } catch { return /\.(mp4|mkv)(\?|$)/i.test(url); }
+}
+
+export interface AttachResult {
+  hls: Hls | null;
+  type: 'hls' | 'mp4' | 'native-hls';
+}
+
+export function attachSource(
+  videoEl:  HTMLVideoElement,
+  url:      string,
+  prevHls?: Hls | null,
+  onLevels?: (levels: Hls.Level[]) => void,
+  onError?:  () => void,
+): AttachResult {
+  prevHls?.destroy();
+
+  if (isMp4(url)) {
+    videoEl.src = url;
+    videoEl.play().catch(() => {});
+    return { hls: null, type: 'mp4' };
+  }
+
+  if (Hls.isSupported()) {
+    const hls = new Hls({ startLevel: -1 });
+    hls.loadSource(url);
+    hls.attachMedia(videoEl);
+    hls.on(Hls.Events.MANIFEST_PARSED, (_, data) => {
+      videoEl.play().catch(() => {});
+      onLevels?.(data.levels);
+    });
+    hls.on(Hls.Events.ERROR, (_, err) => {
+      if (err.fatal) onError?.();
+    });
+    return { hls, type: 'hls' };
+  }
+
+  if (videoEl.canPlayType('application/vnd.apple.mpegurl')) {
+    videoEl.src = url;
+    videoEl.play().catch(() => {});
+    return { hls: null, type: 'native-hls' };
+  }
+
+  return { hls: null, type: 'mp4' };
+}
+
+

StreamPlayer Component

+

Full-featured player: auto-starts on first source, queues fallbacks, source switcher, quality selector, + subtitle tracks. Works for movies and TV.

+
'use client';
+import { useEffect, useRef, useState } from 'react';
+import Hls from 'hls.js';
+import { useStream, StreamSource } from '@/hooks/useStream';
+import { attachSource, isMp4 } from '@/lib/attachSource';
+
+interface Props {
+  tmdbId:   number;
+  season?:  number;
+  episode?: number;
+}
+
+export function StreamPlayer({ tmdbId, season, episode }: Props) {
+  const videoRef    = useRef<HTMLVideoElement>(null);
+  const hlsRef      = useRef<Hls | null>(null);
+  const queueRef    = useRef<StreamSource[]>([]);
+  const startedRef  = useRef(false);
+  const activeRef   = useRef<string>('');
+
+  const { sources, subtitles, meta, loading, done, error } = useStream(tmdbId, season, episode);
+  const [activeSource, setActiveSource] = useState<string>('');
+  const [levels, setLevels]             = useState<Hls.Level[]>([]);
+  const [quality, setQuality]           = useState(-1);
+  const [status, setStatus]             = useState('Connecting…');
+
+  useEffect(() => {
+    startedRef.current = false;
+    queueRef.current   = [];
+    activeRef.current  = '';
+    setActiveSource('');
+    setLevels([]);
+    setQuality(-1);
+    setStatus('Connecting…');
+  }, [tmdbId, season, episode]);
+
+  function playSource(src: StreamSource) {
+    if (!videoRef.current) return;
+    activeRef.current = src.url;
+    setActiveSource(src.source);
+    setLevels([]);
+    setQuality(-1);
+    setStatus(`Loading ${src.label}…`);
+
+    const { hls } = attachSource(
+      videoRef.current,
+      src.url,
+      hlsRef.current,
+      (newLevels) => {
+        setLevels(newLevels);
+        setStatus(`Playing via ${src.label}${isMp4(src.url) ? ' · MP4' : ' · HLS'}`);
+      },
+      () => {
+        const next = queueRef.current.shift();
+        if (next) playSource(next);
+        else setStatus('All sources failed.');
+      },
+    );
+
+    hlsRef.current = hls;
+
+    if (isMp4(src.url))
+      setStatus(`Playing via ${src.label} · MP4`);
+  }
+
+  useEffect(() => {
+    if (!sources.length) return;
+    const latest = sources[sources.length - 1];
+    if (!startedRef.current) {
+      startedRef.current = true;
+      playSource(latest);
+    } else {
+      queueRef.current.push(latest);
+    }
+  }, [sources]);
+
+  useEffect(() => {
+    if (hlsRef.current) hlsRef.current.currentLevel = quality;
+  }, [quality]);
+
+  useEffect(() => () => { hlsRef.current?.destroy(); }, []);
+
+  if (error) return (
+    <div style={{ padding: '1rem', color: '#f87171' }}>
+      Stream error: {error}
+    </div>
+  );
+
+  return (
+    <div style={{ width: '100%' }}>
+      <video
+        ref={videoRef}
+        controls
+        style={{ width: '100%', background: '#000', display: 'block', borderRadius: 6 }}
+      >
+        {subtitles.map(sub => (
+          <track
+            key={`${sub.label}-${sub.source}`}
+            kind="subtitles"
+            label={sub.label}
+            src={sub.file}
+            srcLang={sub.label.slice(0, 2).toLowerCase()}
+          />
+        ))}
+      </video>
+
+      <div style={{ fontSize: '0.75rem', color: '#666', marginTop: 4 }}>
+        {loading && !sources.length ? 'Connecting to providers…' : status}
+        {done && ` · ${sources.length} source${sources.length !== 1 ? 's' : ''} found`}
+      </div>
+
+      {(sources.length > 0 || levels.length > 1) && (
+        <div style={{ display: 'flex', gap: '0.5rem', flexWrap: 'wrap', marginTop: '0.5rem', alignItems: 'center' }}>
+          {levels.length > 1 && (
+            <select
+              value={quality}
+              onChange={e => setQuality(Number(e.target.value))}
+              style={{ fontSize: '0.8rem', padding: '0.3rem 0.5rem', borderRadius: 4,
+                       background: '#1e1e1e', color: '#ccc', border: '1px solid #333' }}
+            >
+              <option value={-1}>Quality: Auto</option>
+              {levels.map((l, i) => (
+                <option key={i} value={i}>
+                  {l.height ? `${l.height}p` : `Level ${i}`}
+                </option>
+              ))}
+            </select>
+          )}
+
+          {sources.length > 1 && sources.map(src => (
+            <button
+              key={src.source}
+              onClick={() => playSource(src)}
+              style={{
+                fontSize: '0.75rem', padding: '0.3rem 0.65rem', borderRadius: 4, cursor: 'pointer',
+                background: activeSource === src.source ? '#fff' : '#1e1e1e',
+                color:      activeSource === src.source ? '#111' : '#aaa',
+                border:     `1px solid ${activeSource === src.source ? '#fff' : '#333'}`,
+              }}
+            >
+              {src.label}
+              <span style={{ fontSize: '0.6rem', marginLeft: 4, opacity: 0.6 }}>
+                {isMp4(src.url) ? 'MP4' : 'HLS'}
+              </span>
+            </button>
+          ))}
+        </div>
+      )}
+    </div>
+  );
+}
+
+

Episode Selector

+

Renders an episode list and mounts the player only when an episode is selected — no streams open until the + user picks something.

+
'use client';
+import { useState } from 'react';
+import { StreamPlayer } from './StreamPlayer';
+
+interface Episode {
+  episode_number: number;
+  name: string;
+}
+
+interface Props {
+  seriesId: number;
+  season:   number;
+  episodes: Episode[];
+}
+
+export function EpisodeSelector({ seriesId, season, episodes }: Props) {
+  const [selected, setSelected] = useState<number | null>(null);
+
+  return (
+    <div style={{ display: 'grid', gridTemplateColumns: '220px 1fr', gap: '1rem' }}>
+      <aside style={{ overflowY: 'auto', maxHeight: 480, borderRight: '1px solid #222', paddingRight: '0.75rem' }}>
+        {episodes.map(ep => (
+          <button
+            key={ep.episode_number}
+            onClick={() => setSelected(ep.episode_number)}
+            style={{
+              display: 'block', width: '100%', textAlign: 'left',
+              padding: '0.5rem 0.6rem', marginBottom: 2, borderRadius: 4, cursor: 'pointer',
+              background: selected === ep.episode_number ? '#fff' : 'transparent',
+              color:      selected === ep.episode_number ? '#111' : '#aaa',
+              border: 'none', fontSize: '0.82rem',
+            }}
+          >
+            <span style={{ opacity: 0.5, marginRight: 6 }}>E{ep.episode_number}</span>
+            {ep.name}
+          </button>
+        ))}
+      </aside>
+
+      <section>
+        {selected === null
+          ? <div style={{ color: '#555', paddingTop: '2rem' }}>Select an episode to watch</div>
+          : <StreamPlayer tmdbId={seriesId} season={season} episode={selected} />
+        }
+      </section>
+    </div>
+  );
+}
+
+
+

Next.js App Router

+

Movie Page

+
import { StreamPlayer } from '@/components/StreamPlayer';
+
+export default function MoviePage({ params }: { params: { id: string } }) {
+  return (
+    <main style={{ maxWidth: 960, margin: '0 auto', padding: '1.5rem 1rem' }}>
+      <StreamPlayer tmdbId={Number(params.id)} />
+    </main>
+  );
+}
+
+

TV Episode Page

+
import { StreamPlayer } from '@/components/StreamPlayer';
+
+export default function EpisodePage({
+  params,
+}: {
+  params: { id: string; season: string; episode: string };
+}) {
+  return (
+    <main style={{ maxWidth: 960, margin: '0 auto', padding: '1.5rem 1rem' }}>
+      <StreamPlayer
+        tmdbId={Number(params.id)}
+        season={Number(params.season)}
+        episode={Number(params.episode)}
+      />
+    </main>
+  );
+}
+
+

Dynamic HLS.js Import (SSR-safe)

+

HLS.js accesses window and will crash during SSR. Either mark the component + 'use client' (already done above) or use a dynamic import to exclude it from the server bundle + entirely: +

+
import dynamic from 'next/dynamic';
+
+const StreamPlayer = dynamic(
+  () => import('@/components/StreamPlayer').then(m => m.StreamPlayer),
+  { ssr: false, loading: () => <div style={{ aspectRatio: '16/9', background: '#111', borderRadius: 6 }} /> }
+);
+
+
+
+

Never import hls.js at the top level of a file that can run on the server. Always use + 'use client' or a dynamic import with ssr: false. +

+
+
+
+

Vue 3

+
<template>
+  <div>
+    <video ref="videoEl" controls style="width:100%; background:#000; border-radius:6px; display:block">
+      <track
+        v-for="sub in subtitles"
+        :key="sub.label + sub.source"
+        kind="subtitles"
+        :label="sub.label"
+        :src="sub.file"
+      />
+    </video>
+
+    <div style="font-size:0.75rem; color:#666; margin-top:4px">{{ status }}</div>
+
+    <div v-if="sources.length > 1" style="display:flex; gap:0.4rem; flex-wrap:wrap; margin-top:0.5rem">
+      <button
+        v-for="src in sources"
+        :key="src.source"
+        @click="playSource(src)"
+        :style="{
+          fontSize: '0.75rem', padding: '0.3rem 0.65rem', borderRadius: '4px', cursor: 'pointer',
+          background: activeSource === src.source ? '#fff' : '#1e1e1e',
+          color:      activeSource === src.source ? '#111' : '#aaa',
+          border:    `1px solid ${activeSource === src.source ? '#fff' : '#333'}`,
+        }"
+      >
+        {{ src.label }}
+        <span style="font-size:0.6rem; margin-left:4px; opacity:0.6">
+          {{ isMp4(src.url) ? 'MP4' : 'HLS' }}
+        </span>
+      </button>
+    </div>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { ref, watch, onUnmounted } from 'vue';
+import Hls from 'hls.js';
+
+const props = defineProps<{
+  tmdbId:   number;
+  season?:  number;
+  episode?: number;
+}>();
+
+const BASE = 'https://api.vyla.cc';
+
+interface Source   { source: string; label: string; url: string; }
+interface Subtitle { label: string; file: string; type: string; source: string; }
+
+const videoEl     = ref<HTMLVideoElement | null>(null);
+const sources     = ref<Source[]>([]);
+const subtitles   = ref<Subtitle[]>([]);
+const status      = ref('Connecting…');
+const activeSource = ref('');
+
+let hlsInstance: Hls | null = null;
+let abortController: AbortController | null = null;
+let started = false;
+let queue: Source[] = [];
+
+function isMp4(url: string): boolean {
+  try {
+    const inner = new URL(url).searchParams.get('url') ?? url;
+    return /\.(mp4|mkv)(\?|$)/i.test(inner);
+  } catch { return /\.(mp4|mkv)(\?|$)/i.test(url); }
+}
+
+function playSource(src: Source) {
+  if (!videoEl.value) return;
+  hlsInstance?.destroy();
+  hlsInstance = null;
+  activeSource.value = src.source;
+
+  if (isMp4(src.url)) {
+    videoEl.value.src = src.url;
+    videoEl.value.play().catch(() => {});
+    status.value = `Playing ${src.label} · MP4`;
+    return;
+  }
+
+  if (Hls.isSupported()) {
+    hlsInstance = new Hls({ startLevel: -1 });
+    hlsInstance.loadSource(src.url);
+    hlsInstance.attachMedia(videoEl.value);
+    hlsInstance.on(Hls.Events.MANIFEST_PARSED, () => {
+      videoEl.value?.play().catch(() => {});
+      status.value = `Playing ${src.label} · HLS`;
+    });
+    hlsInstance.on(Hls.Events.ERROR, (_, err) => {
+      if (err.fatal) {
+        const next = queue.shift();
+        if (next) playSource(next);
+        else status.value = 'All sources failed.';
+      }
+    });
+    return;
+  }
+
+  if (videoEl.value.canPlayType('application/vnd.apple.mpegurl')) {
+    videoEl.value.src = src.url;
+    videoEl.value.play().catch(() => {});
+    status.value = `Playing ${src.label} · HLS`;
+  }
+}
+
+async function loadStream() {
+  abortController?.abort();
+  abortController = new AbortController();
+  sources.value   = [];
+  subtitles.value = [];
+  status.value    = 'Connecting…';
+  started         = false;
+  queue           = [];
+
+  const url = props.season && props.episode
+    ? `${BASE}/tv?id=${props.tmdbId}&season=${props.season}&episode=${props.episode}`
+    : `${BASE}/movie?id=${props.tmdbId}`;
+
+  try {
+    const res     = await fetch(url, {
+      signal: abortController.signal,
+    });
+    const reader  = res.body!.getReader();
+    const decoder = new TextDecoder();
+    let buffer    = '';
+
+    while (true) {
+      const { done, value } = await reader.read();
+      if (done) break;
+      buffer += decoder.decode(value, { stream: true });
+      const lines = buffer.split('\n');
+      buffer = lines.pop()!;
+
+      for (const line of lines) {
+        if (!line.startsWith('data: ')) continue;
+        const event = JSON.parse(line.slice(6));
+
+        if (event.type === 'meta') {
+          subtitles.value = event.subtitles ?? [];
+          status.value = `Loading ${event.meta?.title ?? event.meta?.name ?? ''}…`;
+        }
+        if (event.type === 'source') {
+          sources.value = [...sources.value, event.source];
+          if (!started) { started = true; playSource(event.source); }
+          else queue.push(event.source);
+        }
+        if (event.type === 'done' && !started) {
+          status.value = 'No sources available.';
+        }
+      }
+    }
+  } catch (err: any) {
+    if (err.name !== 'AbortError') status.value = `Error: ${err.message}`;
+  }
+}
+
+watch(() => [props.tmdbId, props.season, props.episode], loadStream, { immediate: true });
+onUnmounted(() => { hlsInstance?.destroy(); abortController?.abort(); });
+</script>
+
+
+

Android (Kotlin + ExoPlayer)

+

ExoPlayer handles both HLS and MP4 natively — just pass the URL and it auto-detects the type.

+
import androidx.media3.common.MediaItem
+import androidx.media3.exoplayer.ExoPlayer
+import kotlinx.coroutines.*
+import org.json.JSONObject
+import java.io.BufferedReader
+import java.io.InputStreamReader
+import java.net.HttpURLConnection
+import java.net.URL
+
+class StreamActivity : AppCompatActivity() {
+
+    private val BASE = "https://api.vyla.cc"
+    private lateinit var player: ExoPlayer
+    private val scope = CoroutineScope(Dispatchers.Main + Job())
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        player = ExoPlayer.Builder(this).build()
+        binding.playerView.player = player
+
+        val tmdbId = intent.getIntExtra("tmdb_id", 550)
+        loadStream(tmdbId)
+    }
+
+    private fun loadStream(tmdbId: Int, season: Int? = null, episode: Int? = null) {
+        scope.launch {
+            val streamUrl = if (season != null && episode != null)
+                "$BASE/tv?id=$tmdbId&season=$season&episode=$episode"
+            else
+                "$BASE/movie?id=$tmdbId"
+
+            withContext(Dispatchers.IO) {
+                val conn = URL(streamUrl).openConnection() as HttpURLConnection
+                val reader = BufferedReader(InputStreamReader(conn.inputStream))
+
+                reader.forEachLine { line ->
+                    if (!line.startsWith("data: ")) return@forEachLine
+                    val json = JSONObject(line.substring(6))
+
+                    when (json.getString("type")) {
+                        "source" -> {
+                            val source = json.getJSONObject("source")
+                            val url = source.getString("url")
+
+                            withContext(Dispatchers.Main) {
+                                if (!player.isPlaying) {
+                                    player.setMediaItem(MediaItem.fromUri(url))
+                                    player.prepare()
+                                    player.play()
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        scope.cancel()
+        player.release()
+    }
+}
+
+
+
+

ExoPlayer resolves HLS and MP4 automatically from the Content-Type returned by the proxy. No + manual type detection needed on Android.

+
+
+
+

iOS (Swift + AVPlayer)

+

AVPlayer handles both HLS and MP4 natively. Same SSE parsing, different player attachment.

+
import UIKit
+import AVKit
+
+class StreamViewController: UIViewController {
+
+    let BASE = "https://api.vyla.cc"
+    var player: AVPlayer?
+    var playerLayer: AVPlayerLayer?
+    var streamTask: URLSessionDataTask?
+    var started = false
+
+    override func viewDidLoad() {
+        super.viewDidLoad()
+        loadStream(tmdbId: 550)
+    }
+
+    func loadStream(tmdbId: Int, season: Int? = nil, episode: Int? = nil) {
+        let path: String
+        if let s = season, let e = episode {
+            path = "\(self.BASE)/tv?id=\(tmdbId)&season=\(s)&episode=\(e)"
+        } else {
+            path = "\(self.BASE)/movie?id=\(tmdbId)"
+        }
+
+        guard let url = URL(string: path) else { return }
+        var request = URLRequest(url: url)
+
+            class SSEDelegate: NSObject, URLSessionDataDelegate {
+                var buffer = ""
+                var onSource: (String) -> Void
+
+                init(_ onSource: @escaping (String) -> Void) { self.onSource = onSource }
+
+                func urlSession(_ session: URLSession, dataTask: URLSessionDataTask, didReceive data: Data) {
+                    guard let text = String(data: data, encoding: .utf8) else { return }
+                    buffer += text
+                    while let range = buffer.range(of: "\n") {
+                        let line = String(buffer[..<range.lowerBound])
+                        buffer.removeSubrange(..<range.upperBound)
+                        guard line.hasPrefix("data: "),
+                              let jsonData = line.dropFirst(6).data(using: .utf8),
+                              let json = try? JSONSerialization.jsonObject(with: jsonData) as? [String: Any],
+                              let type = json["type"] as? String else { continue }
+                        if type == "source",
+                           let source = json["source"] as? [String: Any],
+                           let streamUrl = source["url"] as? String {
+                            onSource(streamUrl)
+                        }
+                    }
+                }
+            }
+
+            let delegate = SSEDelegate { [weak self] streamUrl in
+                guard let self, !self.started else { return }
+                self.started = true
+                DispatchQueue.main.async {
+                    guard let url = URL(string: streamUrl) else { return }
+                    let player = AVPlayer(url: url)
+                    self.player = player
+                    let layer = AVPlayerLayer(player: player)
+                    layer.frame = self.view.bounds
+                    layer.videoGravity = .resizeAspect
+                    self.view.layer.addSublayer(layer)
+                    self.playerLayer = layer
+                    player.play()
+                }
+            }
+
+            let session = URLSession(configuration: .default, delegate: delegate, delegateQueue: nil)
+            self.streamTask = session.dataTask(with: request)
+            self.streamTask?.resume()
+        }
+    }
+
+    override func viewWillDisappear(_ animated: Bool) {
+        super.viewWillDisappear(animated)
+        streamTask?.cancel()
+        player?.pause()
+    }
+}
+
+
+
+

AVPlayer resolves HLS via application/vnd.apple.mpegurl and MP4 via video/mp4 + content-type returned by the proxy — no manual detection needed on iOS either.

+
+
+
+

Python (mpv)

+

For desktop apps or scripts. mpv handles HLS and MP4 natively via its own demuxer.

+
import requests
+import json
+import subprocess
+
+BASE = 'https://api.vyla.cc'
+
+def stream(tmdb_id: int, season: int = None, episode: int = None):
+    url = (
+        f'{BASE}/tv?id={tmdb_id}&season={season}&episode={episode}'
+        if season and episode else
+        f'{BASE}/movie?id={tmdb_id}'
+    )
+
+    with requests.get(url, stream=True) as res:
+        for line in res.iter_lines():
+            if not line or not line.startswith(b'data: '):
+                continue
+
+            event = json.loads(line[6:])
+
+            if event['type'] == 'source':
+                stream_url = event['source']['url']
+                label      = event['source']['label']
+                print(f'Playing via {label}: {stream_url}')
+                subprocess.run(['mpv', stream_url])
+                return
+
+            if event['type'] == 'done':
+                print('No sources available.')
+
+stream(550)
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/home/index.html b/home/index.html new file mode 100644 index 0000000..d58864b --- /dev/null +++ b/home/index.html @@ -0,0 +1,202 @@ + + + + + + + Vyla Home — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Vyla Home

+

The desktop control center for running the Vyla media stack locally.

+ Vyla Home desktop application preview +

Vyla Home

+

Vyla Home is the desktop application in the Vyla ecosystem. It prepares and runs the + local services that power your media experience, then opens the interface in a secure embedded + window. It is designed for people who want Vyla without managing a separate server process, reverse + proxy, or service lifecycle by hand.

+

Downloads

+

Vyla Home is available for Windows, macOS, and Linux. Get the latest version below:

+ + + + + + + + + + + + + + + + + + + + + +
PlatformDownload
WindowsDownload .exe
macOSDownload .dmg
LinuxBuild from source
+

Inside the releases page, you can find the latest version of Vyla Home for each platform, find the + .exe for windows, and .dmg for macOS. +

+

Linux Setup

+

For Linux, you'll need to build Vyla Home from source. Follow these steps to install on Fedora:

+

Step 1: Install dependencies

+
sudo dnf install webkit2gtk4.1-devel curl wget openssl-devel libappindicator-gtk3-devel librsvg2-devel
+sudo dnf install gcc gcc-c++ make automake autoconf
+
+

Step 2: Install Node.js and NPM

+
sudo dnf install nodejs npm
+
+

Step 3: Install Rust

+
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
+source "$HOME/.cargo/env"
+
+

Step 4: Clone the Git repository

+
git clone https://gitlab.com/vyla-entertainment/home.git vyla-home
+cd vyla-home
+
+

Step 5: Install more dependencies

+
npm install
+
+

Step 6: Build the Fedora package (.rpm)

+
cargo update --manifest-path src-tauri/Cargo.toml
+npx tauri build --bundles rpm,appimage
+
+

Step 7: Final installation

+
sudo dnf install src-tauri/target/release/bundle/rpm/*.rpm
+
+

Thank you @krishi279 for supplying the Linux setup instructions on Discord ❤️.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..a9a6385 --- /dev/null +++ b/index.html @@ -0,0 +1,241 @@ + + + + + + + Vyla Entertainment — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Vyla Entertainment

+

A community-built media project focused on freedom, privacy, and giving + people + control over their own experience.

+ A collage of films and shows available through a Vyla experience +

About Vyla

+

Vyla is a community-built media project created around a simple idea: people should have + control over + how they experience their media.

+

We started Vyla because we wanted something clean, private, and easy to use without relying on a + single + company or centralized service. What began as a small project has grown into a collection of + applications and + tools for desktop, Android, Android TV, and more.

+

Vyla is not a streaming service, and we do not host or provide the media itself. Instead, we build + the + software that brings the experience together while leaving control in the hands of the person using + it.

+

Where we come from

+

Vyla started as a small web project focused on creating a better way to discover and watch movies and + shows. + Over time, it grew into a broader project with applications for different platforms alongside the + tools that + power them.

+

We care about keeping Vyla open, accessible, and community-driven. Our project has also been shared + by + communities such as FMHY and r/Piracy, where people interested in + alternative and self-hosted media solutions have discovered Vyla.

+

We are proud to be part of a wider community of people who build, share, and explore alternatives to + traditional media platforms.

+

What we make

+

Vyla is made up of several projects designed for different ways of using the platform.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
PlatformDescriptionLink
Vyla HomeDesktop appLearn more
Vyla GoAndroid appLearn more
Vyla TVAndroid TV appLearn more
Vyla SDKDeveloper SDKLearn more
Stream APISelf-hosted APILearn more
+

Made for people, not platforms

+

We believe software should give people more control, not less.

+

That means keeping Vyla local and self-hostable, avoiding unnecessary centralized dependencies, and + allowing + developers and users to decide how they want to use the project.

+

Vyla can be as simple as a TV or mobile app, a desktop application, or the foundation for a + completely custom + media experience. You decide how much of Vyla you use and where it runs.

+

Start exploring

+ +
+
+

Vyla is self-hosted software and does not host or distribute media. Please respect applicable + laws, + upstream terms, and rights-holder requests. See the DMCA policy for + removal + information.

+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/llms.txt b/llms.txt new file mode 100644 index 0000000..b338ece --- /dev/null +++ b/llms.txt @@ -0,0 +1,77 @@ +# Vyla Documentation + +Vyla is a community-built media project focused on freedom, privacy, and giving people control over their own experience. + +## Project Overview + +Vyla is not a streaming service and does not host or provide media. Instead, Vyla builds software that brings the media experience together while leaving control in the hands of the person using it. The project consists of: + +- **Vyla Home**: Desktop application for Windows, macOS, and Linux +- **Vyla Go**: Android mobile application +- **Vyla TV**: Android TV application +- **Vyla SDK**: Node.js/TypeScript SDK for developers +- **Stream API**: Self-hosted HTTP/SSE API for browser and mobile clients + +## Key Features + +- Self-hosted and privacy-focused +- Open source community project +- Provider-based streaming resolution +- Multi-platform support (desktop, Android, Android TV) +- Developer-friendly SDK and API +- Support for HLS and MP4 streaming +- Built-in health monitoring and fallback systems + +## Documentation Structure + +- `/` - Introduction to Vyla +- `/architecture` - System architecture and component relationships +- `/home` - Vyla Home desktop application +- `/go` - Vyla Go Android application +- `/tv` - Vyla TV Android TV application +- `/player` - Building custom media players +- `/sdk` - SDK reference documentation +- `/sdk-quickstart` - Quick start guide for SDK +- `/stream-api/overview` - Stream API overview +- `/stream-api/paths` - API paths and SSE events +- `/stream-api/media-data` - Subtitles and downloads API +- `/stream-api/operations` - Health, testing, and operations +- `/self-hosting` - Self-hosting deployment guide +- `/concepts/providers` - Provider configuration and verification +- `/guides/integrations` - Client integration guide +- `/guides/error-handling` - Error handling and fallbacks +- `/misc/credit` - Credits and acknowledgments +- `/misc/dmca` - DMCA and copyright policy + +## Technical Stack + +- Node.js 18+ for SDK and Stream API +- TypeScript for SDK +- Server-Sent Events (SSE) for streaming +- HLS.js for web playback +- TMDB API for metadata +- Jetpack Compose for Android TV + +## Integration Options + +1. **Direct SDK**: For Node.js applications - embed provider resolution directly +2. **Stream API**: For browser/mobile clients - HTTP/SSE interface +3. **Vyla Home**: Managed desktop experience with bundled services + +## Important Notes + +- Vyla is self-hosted only - no public hosted instance +- Requires TMDB API key for metadata-dependent sources +- Provider availability varies by network environment +- Supports fallback strategies for resilient playback +- CORS-enabled by default +- No database required for Stream API + +## Community + +- GitLab: https://gitlab.com/vyla-entertainment/ +- Discord: https://discord.gg/5eqRhKeywG + +## Legal + +Vyla does not host or distribute media. Rights holders can request source removal via the DMCA policy. The project is intended for personal, educational, and development use. Users are responsible for complying with applicable laws. diff --git a/logo/light.png b/logo/light.png new file mode 100644 index 0000000..a80a11d Binary files /dev/null and b/logo/light.png differ diff --git a/misc/credit/index.html b/misc/credit/index.html new file mode 100644 index 0000000..4fd8c9f --- /dev/null +++ b/misc/credit/index.html @@ -0,0 +1,167 @@ + + + + + + + Credit — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Credit

+

Credits and inspiration behind the Vyla API ecosystem.

+

This page highlights the open-source projects and developers that heavily inspired the Vyla API, player + ecosystem, and overall UI direction.

+
+

Player & UI Inspiration

+

The overall player experience, streaming layout, and much of the UI direction for Vyla was heavily inspired + by P-Stream.

+ +

Their work served as a major reference point for the design language and viewing experience used throughout + Vyla.

+
+

Stream API Inspiration

+

The architecture and implementation ideas behind the Vyla streaming API were heavily inspired by CinePro.

+

Resources

+ +

CinePro's ecosystem played a large role in shaping how Vyla handles providers, source structures, streaming + workflows, and API organization.

+
+

Source Implementation & Fixes

+

The owner of Movieslay helped fix and implement new sources across the Vyla API.

+ +

Movieslay Emerald is a successor to the original Movieslay webapp. gavingogaming's direct contributions were + instrumental in expanding and stabilizing Vyla's source coverage.

+
+

Open Source Appreciation

+

Vyla would not exist in its current form without the incredible open-source work created by these developers + and communities.

+

Massive thanks to:

+
    +
  • Pasithea0
  • +
  • An0n-00
  • +
  • gavingogaming (Movieslay)
  • +
  • The P-Stream contributors
  • +
  • The CinePro contributors
  • +
  • The Movieslay contributors
  • +
+

for building and sharing their projects publicly.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/misc/dmca/index.html b/misc/dmca/index.html new file mode 100644 index 0000000..a21f137 --- /dev/null +++ b/misc/dmca/index.html @@ -0,0 +1,182 @@ + + + + + + + DMCA & Copyright — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

DMCA & Copyright

+

Content sources used by Vyla API and how rights holders can request removal.

+

Vyla API is a self-hosted streaming proxy that aggregates publicly accessible stream URLs from third-party + providers. Vyla does not host, store, upload, or distribute any video content. All media is served directly + from its original source.

+
+

How It Works

+

Vyla acts as a reverse proxy and aggregator. When a stream is requested:

+
    +
  1. Vyla queries third-party providers for a stream URL.
  2. +
  3. That URL is proxied through the Vyla server to handle CORS headers.
  4. +
  5. The underlying video segments are fetched from their original CDN and passed through to the client.
  6. +
+

At no point does Vyla store, transcode, or re-host any media file.

+
+

Content Sources

+

The following third-party sources are currently scraped or proxied by Vyla API. These are the upstream + providers — Vyla does not control, operate, or own any of them.

+

You can find the full list of sources in the stream-api sources directory. +

+
+
+

This list reflects active sources. These sources are active or in the works.

+
+
+
+

DMCA / Takedown Requests

+

If you are a rights holder and believe that content accessible through Vyla API infringes your copyright, you + may submit a takedown request. Because Vyla does not host the content itself, the most effective path is to + contact the upstream source directly.

+

However, Vyla can disable specific providers or sources upon valid request.

+

To submit a takedown request, contact:

+
+
DMCA Contact
+
+

Open an issue on the GitLab repository or reach out via Discord or email.

+ +

Please include the following in your request:

+
    +
  • Your name and organization
  • +
  • The specific content or provider you are requesting removal of
  • +
  • Evidence of your rights to the content
  • +
  • Your contact information
  • +
+
+
+
+

Good Faith Compliance

+

Vyla API is operated in good faith. Upon receiving a valid and complete takedown request, the relevant source + will be reviewed and disabled within a reasonable timeframe if the claim is substantiated.

+

Rights holders such as Sony Pictures, Universal, Warner Bros., Disney, Netflix, or any other studio or + distributor are welcome to contact via the channels above. We will respond promptly.

+
+

Disclaimer

+

Vyla API is intended for personal, educational, and development use only. The operator of this API does not + endorse or encourage copyright infringement. Users of this API are responsible for complying with the laws of + their jurisdiction.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..af66874 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2021 @@ +{ + "name": "vyla-docs", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "vyla-docs", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "gray-matter": "^4.0.3", + "hast-util-to-html": "^9.0.5", + "hastscript": "^9.0.1", + "highlight.js": "^11.12.0", + "mdast-util-to-string": "^4.0.0", + "rehype-highlight": "^7.0.2", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-mdx": "^3.1.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0" + } + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/estree-util-visit": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", + "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/hast-util-heading-rank": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-heading-rank/-/hast-util-heading-rank-3.0.0.tgz", + "integrity": "sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/highlight.js": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.12.0.tgz", + "integrity": "sha512-nbfWpyRMcMrPMmDwJB+dhX/eiaPKtc2RB+0QZskqJ3WjRA/FDS0e9hZrx8EC/lbEv8gXy98FcDbNa/dspAaJMg==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "3.15.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.2.tgz", + "integrity": "sha512-6EuL879VkRA+1Cz578mKMiKvjPNEuk6+r1JaFzoSWejZmtf7xWbIyw1e3KkxlkzTIt9Taw6JBhEppG7utc1P+w==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lowlight/-/lowlight-3.3.0.tgz", + "integrity": "sha512-0JNhgFoPvP6U6lE/UdVsSq99tn6DhjjpAj5MxG49ewd2mOBVtwWYIT8ClyABhq198aXXODMU6Ox8DrGy/CpTZQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.0.0", + "highlight.js": "~11.11.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lowlight/node_modules/highlight.js": { + "version": "11.11.2", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-11.11.2.tgz", + "integrity": "sha512-oaXMACAU0kzOMXBjWpNcX+vlwSBCIAiZ9BHa7gA15NOTtT2L/l8OSZDuqS2XppOhZBPJ7hm4o8ep2kyuip2uEQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz", + "integrity": "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", + "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", + "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.1.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.3" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-expression": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", + "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-mdx-jsx": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", + "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "micromark-factory-mdx-expression": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdx-md": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", + "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", + "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", + "license": "MIT", + "dependencies": { + "acorn": "^8.0.0", + "acorn-jsx": "^5.0.0", + "micromark-extension-mdx-expression": "^3.0.0", + "micromark-extension-mdx-jsx": "^3.0.0", + "micromark-extension-mdx-md": "^2.0.0", + "micromark-extension-mdxjs-esm": "^3.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-mdxjs-esm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", + "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-mdx-expression": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", + "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-events-to-acorn": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-position-from-estree": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-events-to-acorn": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", + "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "estree-util-visit": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "vfile-message": "^4.0.0" + } + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/rehype-highlight": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/rehype-highlight/-/rehype-highlight-7.0.2.tgz", + "integrity": "sha512-k158pK7wdC2qL3M5NcZROZ2tR/l7zOzjxXd5VGdcfIyoijjQqpHd3JKtYSBDpDZ38UI2WJWuFAtkMDxmx5kstA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-text": "^4.0.0", + "lowlight": "^3.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-slug": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/rehype-slug/-/rehype-slug-6.0.0.tgz", + "integrity": "sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "github-slugger": "^2.0.0", + "hast-util-heading-rank": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-stringify": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-stringify/-/rehype-stringify-10.0.1.tgz", + "integrity": "sha512-k9ecfXHmIPuFVI61B9DeLPN0qFHfawM6RsuX48hoqlaKSF61RskNjSm1lI8PhBEM0MRdLxVVm4WmTqJQccH9mA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-mdx": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", + "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", + "license": "MIT", + "dependencies": { + "mdast-util-mdx": "^3.0.0", + "micromark-extension-mdxjs": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position-from-estree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", + "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..713c7cf --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "vyla-docs", + "version": "1.0.0", + "description": "", + "main": "index.js", + "type": "module", + "scripts": { + "build": "node build.mjs", + "serve": "cd dist && python3 -m http.server 8080" + }, + "keywords": [], + "author": "", + "license": "ISC", + "dependencies": { + "gray-matter": "^4.0.3", + "hast-util-to-html": "^9.0.5", + "hastscript": "^9.0.1", + "highlight.js": "^11.12.0", + "mdast-util-to-string": "^4.0.0", + "rehype-highlight": "^7.0.2", + "rehype-slug": "^6.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-mdx": "^3.1.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0" + } +} diff --git a/player/index.html b/player/index.html new file mode 100644 index 0000000..906eb3f --- /dev/null +++ b/player/index.html @@ -0,0 +1,184 @@ + + + + + + + Build a player — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Build a player

+

Design a resilient Vyla playback experience around your own product and + infrastructure.

+

Your player, your rules

+

Vyla supplies the integration layer; your application owns the player experience. That includes design, + authentication, title discovery, parental controls, analytics, playback policy, and fallback behavior.

+

For a local managed experience, Vyla Home starts its bundled player service. For a custom product, choose one + of two boundaries:

+ + + + + + + + + + + + + + + + + +
IntegrationBest for
SDK directlyNode.js products that already have a backend and want complete control
Stream APIbrowser or mobile clients that need an HTTP/SSE source feed
+

Build for changing conditions

+

Provider results vary by title and network. A dependable player should:

+
    +
  1. Begin playback as soon as it has a usable candidate.
  2. +
  3. Keep later candidates as fallbacks.
  4. +
  5. Keep subtitles independent from first playback.
  6. +
  7. Report a helpful unavailable state when no candidates resolve.
  8. +
  9. Destroy playback and abort in-flight work when the viewer changes titles.
  10. +
+

The SDK returns provider results directly; Stream API emits meta, source, and + done events. Both models benefit from the same fallback strategy. +

+

Playback formats

+

Candidates may be HLS manifests or direct media files. Let the result metadata and your player capabilities + guide how a candidate is loaded; do not assume every URL has the same format or availability. HLS-capable web + players normally use HLS.js where native HLS is unavailable, while direct media can be assigned to a video + element.

+

Continue building

+ +
+ +
+
+ + + + + \ No newline at end of file diff --git a/robots.txt b/robots.txt new file mode 100644 index 0000000..97b36f0 --- /dev/null +++ b/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Allow: / +Disallow: /assets/ + +Sitemap: https://vyla.cc/sitemap.xml diff --git a/sdk-quickstart/index.html b/sdk-quickstart/index.html new file mode 100644 index 0000000..d0adbe5 --- /dev/null +++ b/sdk-quickstart/index.html @@ -0,0 +1,194 @@ + + + + + + + SDK quickstart — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

SDK quickstart

+

Install Vyla SDK, resolve a source, and build a safe server-side integration.

+

Before you start

+

Vyla SDK is for server-side Node.js. It contains provider integrations and makes outbound + requests from the process where you install it. Do not import it into a browser bundle or expose provider + results, credentials, or internal source keys directly to untrusted clients.

+

You need Node.js 18 or later and, for sources that resolve title metadata, a TMDB API key.

+

Install and initialize

+
+
+
+
+
npm install @vyla-entertainment/sdk
+
+
+
pnpm install @vyla-entertainment/sdk
+
+
+
yarn add @vyla-entertainment/sdk
+
+
+
+
import VylaSDK from "@vyla-entertainment/sdk";
+
+const vyla = new VylaSDK({
+  tmdbApiKey: process.env.TMDB_API_KEY,
+  penguManifest: process.env.PENGU_MANIFEST
+});
+
+
+
+

Keep TMDB_API_KEY and any deployment configuration in server-side environment variables. + Never commit them or send them to a client.

+
+
+

Resolve a movie

+

First inspect active providers. Provider availability changes over time, so select a key from the running + configuration instead of hard-coding a placeholder.

+
const providers = vyla.getSources(true);
+console.table(providers.map(({ key, label }) => ({ key, label })));
+
+const provider = providers[0];
+const result = await vyla.getStream(provider.key, 155);
+const candidates = result.allUrls ?? [result];
+
+console.log(candidates.map(({ url, type, quality }) => ({ url, type, quality })));
+
+

155 is a TMDB movie ID. For an episode, include season and episode:

+
const result = await vyla.getStream(provider.key, 1396, 1, 1);
+
+

Make it resilient

+

Provider results are inherently variable: a source can fail, time out, return more than one candidate, or be + temporarily disabled. In production:

+
    +
  1. Probe sources or maintain a recent availability signal before choosing one.
  2. +
  3. Give every provider call a request deadline in your application.
  4. +
  5. Treat allUrls as fallback candidates, not a guarantee that every URL will play.
  6. +
  7. Return only the fields your client needs and validate requests at your own API boundary.
  8. +
  9. Cache and rate-limit at your boundary to avoid repeatedly triggering upstream work.
  10. +
+
const health = await vyla.probeAllSources();
+const usableKey = Object.entries(health).find(([, result]) => result.ok)?.[0];
+
+if (!usableKey) throw new Error("No providers are currently available");
+
+const stream = await vyla.getStream(usableKey, 155);
+
+

Next steps

+
    +
  • Read the complete SDK reference for methods and return shapes.
  • +
  • Learn provider flags, timeout behavior, and source keys in Providers. +
  • +
  • Choose Stream API only when an SSE HTTP interface better fits your + client architecture.
  • +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/sdk/index.html b/sdk/index.html new file mode 100644 index 0000000..f002002 --- /dev/null +++ b/sdk/index.html @@ -0,0 +1,588 @@ + + + + + + + SDK Reference — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

SDK Reference

+

Scrape video streams, fetch subtitles, retrieve download links, and monitor provider + health directly from your Node.js application.

+

The Vyla SDK (@vyla-entertainment/sdk) is a server-side JavaScript/TypeScript + package that provides direct access to Vyla's stream resolvers, health probes, download resolvers, and + subtitle providers.

+

By embedding the SDK directly into your backend application, provider resolution executes in your own + environment. It is an alternative to operating the Stream API as a separate process: your application owns its + request boundary, caching, authorization, timeouts, and response shape.

+
+
+

The SDK can be used independently of the self-hosted Stream API. If you prefer to embed Vyla capabilities + in an existing Node.js application rather than run a separate HTTP service, begin with the SDK quickstart.

+
+
+
+

Features

+
    +
  • Direct-to-Client Playback: Scraped streams link directly to source CDNs. No proxy server + overhead required unless specified by provider headers.
  • +
  • Built-In Health Probing: Monitor latency and uptime per provider with built-in health + check utilities.
  • +
  • Automated Anime Sub/Dub Routing: Automatically configures audio track requirements + (sub vs dub) based on provider naming conventions.
  • +
  • Multi-Source Subtitle Aggregation: Merges subtitle tracks from multiple redundant + subtitle mirrors into a unified VTT/SRT format.
  • +
  • Metadata Fallbacks: Automatically maps TMDB IDs to external IDs (such as IMDB and + AniList) for smooth stream lookups.
  • +
  • Self-Contained: No need to deploy a separate API server — all streaming functionality + runs within your existing Node.js application.
  • +
+
+

Installation

+

Install the package into your Node.js backend using your package manager of choice:

+
+
+
+
+
npm install @vyla-entertainment/sdk
+
+
+
pnpm install @vyla-entertainment/sdk
+
+
+
yarn add @vyla-entertainment/sdk
+
+
+
+
+

Note: Node.js v18+ is required due to modern standard fetch + and AbortSignal.timeout usage.

+
+
+

Initialization

+

Initialize the VylaSDK client by supplying your TMDB API Key. The key resolves + media metadata, validates movie/TV show IDs, and translates TMDB IDs into AniList IDs for anime sources.

+

You may optionally provide a pengu.uk manifest URL to enable the Pengu source.

+
import VylaSDK from "@vyla-entertainment/sdk";
+
+const sdk = new VylaSDK({
+  tmdbApiKey: process.env.TMDB_API_KEY,
+
+  penguManifest: "" // Optional, get from pengu.uk
+});
+
+
+

Core Methods

+

getStream(key, id, s?, e?, clientIP?)

+

Scrapes video stream URLs and playability headers for a specified provider key.

+
// Fetch a movie stream (TMDB ID: 155 - The Dark Knight)
+const movieStream = await sdk.getStream("sourcename", "155");
+
+// Fetch a TV show episode (TMDB ID: 1399, Season 1, Episode 1)
+const tvStream = await sdk.getStream("sourcename", "1399", 1, 1);
+
+// Pass client IP for providers that use geo-verification
+const geoStream = await sdk.getStream("sourcename", "155", null, null, "203.0.113.195");
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeRequiredDefaultDescription
keystringYes—Unique key identifier for the provider (e.g. "sourcename", + "sourcename", "sourcename"). +
idstringYes—TMDB media ID (e.g., "155").
sstring | numberNonullSeason number (required for TV shows).
estring | numberNonullEpisode number (required for TV shows).
clientIPstringNonullEnd-user IP address forwarded for geo-restricted scrapers.
+

Example Response Format

+
{
+  "url": "https://example-cdn.com/hls/manifest.m3u8",
+  "quality": "auto",
+  "isM3U8": true,
+  "headers": {
+    "Referer": "https://example.com/",
+    "Origin": "https://example.com"
+  },
+  "allUrls": [
+    {
+      "url": "https://example-cdn.com/hls/manifest.m3u8",
+      "quality": "1080p",
+      "isM3U8": true
+    }
+  ]
+}
+
+
+

getSubtitles(id, s?, e?)

+

Fetches subtitle files aggregated across multiple subtitle backend mirrors (v1, v2, + febbox). Returns clean VTT and SRT tracks. +

+
// Fetch movie subtitles
+const movieSubs = await sdk.getSubtitles("155");
+
+// Fetch TV show episode subtitles
+const tvSubs = await sdk.getSubtitles("1399", 1, 1);
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ParameterTypeRequiredDefaultDescription
idstringYes—TMDB media ID.
sstring | numberNonullSeason number (required for TV shows).
estring | numberNonullEpisode number (required for TV shows).
+

Example Response Format

+
[
+  {
+    "label": "English",
+    "file": "https://sub.vdrk.site/v1/vtt/movie/155/English.vtt",
+    "type": "vtt",
+    "source": "v1"
+  },
+  {
+    "label": "Spanish",
+    "file": "https://fed-subs.pstream.mov/subtitles/spanish.srt",
+    "type": "srt",
+    "source": "febbox"
+  }
+]
+
+
+

getDownloads(id, s?, e?)

+

Fetches direct downloadable media files (MP4/MKV) paired with resolution and file size descriptors.

+
// Movie downloads
+const downloads = await sdk.getDownloads("155");
+
+// TV Episode downloads
+const tvDownloads = await sdk.getDownloads("1399", 1, 1);
+
+

Example Response Format

+
[
+  {
+    "url": "https://download-node.com/file/the-dark-knight-1080p.mp4",
+    "quality": "1080p",
+    "size": "2.14 GB",
+    "format": "MP4"
+  }
+]
+
+
+

Provider & Health Management

+

getSources(excludeDisabled?)

+

Retrieves provider configurations registered in the SDK.

+
// Get all sources including temporarily disabled ones
+const allSources = sdk.getSources(false);
+
+// Get only active/enabled sources
+const activeSources = sdk.getSources(true);
+
+

Parameters

+ + + + + + + + + + + + + + + + + + + +
ParameterTypeRequiredDefaultDescription
excludeDisabledbooleanNofalseWhen true, filters out providers marked as disabled + (disabled: true).
+
+

probeSource(key)

+

Executes a health ping test against a single provider to determine current availability and latency using a + probe payload (TMDB ID: 155).

+
const health = await sdk.probeSource("vidrock");
+console.log(health);
+// Output: { ok: true, ms: 840 }
+
+
+

probeAllSources()

+

Executes health tests in parallel across all non-disabled provider modules.

+
const results = await sdk.probeAllSources();
+console.log(results);
+
+

Example Response Format

+
{
+  "sourcename1": { "ok": true, "ms": 420 },
+  "sourcename2": { "ok": true, "ms": 1150 },
+  "sourcename3": { "ok": false, "ms": null }
+}
+
+
+

Provider Metadata Properties

+

When calling getSources(), each provider entry returns metadata describing its features and + streaming behavior:

+
{
+  key: 'example',
+  label: 'Example',
+  sourceFile: 'example',
+  proxyParam: 'ex',
+  timeout: 20000,
+  jitter: 800,
+  retries: 3,
+  multiUrl: true,
+  skipProxy: false,
+  cdnHeaders: [{
+    pattern: /./,
+    headers: {
+      Referer: 'https://example.com/',
+      Origin: 'https://example.com'
+    }
+  }]
+}
+
+

Property Reference

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
keystringUnique identifier used in getStream() and probeSource().
labelstringHuman-readable name for UI display.
skipProxybooleanWhen true, stream URLs can be fed directly to web video players (e.g., + HLS.js, Video.js) without needing an intermediate backend proxy.
multiUrlbooleanIndicates whether the source returns multiple stream resolution choices.
timeoutnumberInternal request timeout threshold in milliseconds.
retriesnumberMax automatic retry attempts executed by health/stream scrapers.
cdnHeadersArrayRegex pattern matching rules detailing required request headers (Referer, + Origin) needed to playback stream segments without 403 Forbidden errors. +
disabledbooleanSet to true if a source is down or undergoing maintenance.
+
+

Practical Express.js Backend Example

+

Here is an example showing how to serve stream links to your client web application while handling custom CDN + playback headers and subtitle merging:

+
import express from "express";
+import VylaSDK from "@vyla-entertainment/sdk";
+
+const app = express();
+const sdk = new VylaSDK({ tmdbApiKey: process.env.TMDB_API_KEY });
+
+// Scrape stream route
+app.get("/api/stream", async (req, res) => {
+  const { provider, tmdbId, season, episode } = req.query;
+  const clientIP = req.headers["x-forwarded-for"] || req.socket.remoteAddress;
+
+  try {
+    const streamData = await sdk.getStream(
+      provider,
+      tmdbId,
+      season || null,
+      episode || null,
+      clientIP
+    );
+
+    if (!streamData || !streamData.url) {
+      return res.status(404).json({ error: "No stream found for this provider" });
+    }
+
+    return res.json({
+      success: true,
+      provider,
+      stream: streamData,
+    });
+  } catch (error) {
+    return res.status(500).json({ error: error.message });
+  }
+});
+
+// Fetch subtitles route
+app.get("/api/subtitles", async (req, res) => {
+  const { tmdbId, season, episode } = req.query;
+
+  try {
+    const subtitles = await sdk.getSubtitles(
+      tmdbId,
+      season || null,
+      episode || null
+    );
+
+    return res.json({ subtitles });
+  } catch (error) {
+    return res.status(500).json({ subtitles: [] });
+  }
+});
+
+app.listen(3000, () => console.log("Vyla SDK Server running on port 3000"));
+
+

SDK vs Self-Hosted API

+

The SDK and the self-hosted API server provide the same streaming functionality but in different deployment + models:

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
AspectSDKSelf-Hosted API
DeploymentEmbedded in your Node.js appSeparate server process
NetworkDirect source CDN accessBuilt-in proxy for CORS
Rate LimitingNone (you control it)Per-API-key limits
Setupnpm install + initClone repo + configure
Use CaseExisting Node.js appsStandalone streaming service
+

Choose the SDK if you want to integrate streaming directly into an existing Node.js application. Choose the + self-hosted API if you want a standalone streaming service that can be used by multiple clients.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/self-hosting/index.html b/self-hosting/index.html new file mode 100644 index 0000000..1369a58 --- /dev/null +++ b/self-hosting/index.html @@ -0,0 +1,280 @@ + + + + + + + Self-host Stream API — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Self-host Stream API

+

Deploy the optional Stream API when your clients need an HTTP/SSE layer.

+

Overview

+

Stream API is a self-hosted Node.js application that you run on infrastructure you control. + It does not provide a public hosted instance. The service runs as a Node.js process, uses in-memory caching, + and can fork workers through Node's built-in cluster module. No database is required by the + service itself.

+
+
+

Stream API is self-hosted only. There is no public Vyla streaming endpoint. You only need it when your + architecture requires an HTTP/SSE layer; Node.js applications can use Vyla SDK + directly.

+
+
+

Requirements

+
    +
  • Node.js 18 or later
  • +
  • A TMDB API key if you want title validation and metadata lookups
  • +
+

Environment variables

+

Create a .env file in the project root:

+
TMDB_API_KEY=
+PORT=7860
+WORKER_COUNT=1
+ENABLE_DEBUG_ROUTE=false
+PROXY_STREAMS=false
+PROXY_URL=
+
+# Optional telemetry configuration
+GA_MEASUREMENT_ID=
+GA_API_SECRET=
+
+
+
Variable reference
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
VariableRequiredDescription
TMDB_API_KEYYesEnables TMDB-based ID validation, metadata lookups, and anime detection. Some sources will not + work without it.
GA_MEASUREMENT_ID / GA_API_SECRETNoEnables Google Analytics event reporting. Leave blank to disable.
PORTNoPort the server listens on. Defaults to 7860.
WORKER_COUNTNoNumber of cluster workers to fork. Defaults to 1 locally.
ENABLE_DEBUG_ROUTENoSet to true to enable /api/debug/:id. Keep this false in + production — it exposes upstream request/response headers and raw source output.
PROXY_STREAMSNoSet to true to route stream playback through your own server's /api + proxy endpoint instead of direct URLs.
PROXY_URLNoAn external proxy base URL to use instead of the built-in /api proxy. Only used if + PROXY_STREAMS is enabled. +
+
+
+

Installing and running

+
git clone https://gitlab.com/vyla-entertainment/stream-api
+cd stream-api
+npm install
+node server.js
+
+

The server listens on:

+
http://localhost:7860
+
+

Visit /health to confirm it is running and check which sources are currently reachable from your + host.

+

Running with multiple workers

+

Setting WORKER_COUNT above 1 forks additional worker processes via + cluster. +

+

The primary process holds a shared in-memory cache (capped at 1500 entries, pruned every 30 seconds) and + relays cache reads/writes between workers over IPC, allowing cached stream results to be shared rather than + duplicated.

+
WORKER_COUNT=4
+node server.js
+
+

Each worker independently loads source modules and handles outbound requests. Only shared cache operations + and concurrency limits are coordinated through the primary process.

+

Network considerations

+

Several sources are documented in config.js as blocked on datacenter IPs. This commonly affects + cloud hosting providers and VPS environments.

+

If a source's getStream consistently returns nothing from your server but works from a + residential connection, the cause is usually upstream blocking based on IP reputation. For the best + experience, run Vyla API on a residential connection or use a residential proxy.

+

When running on a local desktop environment, you should have minimal issues with source availability since + you're using a residential IP address.

+

Reverse proxy / TLS

+

The server runs over plain HTTP. For public deployments, place it behind a reverse proxy such as Caddy, + Nginx, or Cloudflare Tunnel to terminate TLS and forward requests to the configured PORT.

+

CORS is already configured at the application layer with:

+
Access-Control-Allow-Origin: *
+
+

No additional CORS configuration is required on the proxy.

+

Verifying your deployment

+

Run these checks in order:

+
    +
  1. +

    GET /health

    +

    Confirms the process is running and reports per-source reachability from your host.

    +
  2. +
  3. +

    GET /api/test/155?source=vidrock

    +

    Runs a real source resolution using a known TMDB movie ID and returns whether the source succeeded.

    +
  4. +
  5. +

    GET /api/debug/155?source=vidrock

    +

    Only available when ENABLE_DEBUG_ROUTE=true.

    +

    Returns request traces, headers, and raw candidate URLs for debugging source failures.

    +
  6. +
+
+
+

Leave ENABLE_DEBUG_ROUTE disabled on public deployments. The debug route exposes upstream + request information and raw stream URLs, which can leak internal details.

+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/sitemap.xml b/sitemap.xml new file mode 100644 index 0000000..98c690f --- /dev/null +++ b/sitemap.xml @@ -0,0 +1,93 @@ + + + + https://vyla.cc/ + daily + 1.0 + + + https://vyla.cc/architecture.html + weekly + 0.8 + + + https://vyla.cc/home.html + weekly + 0.9 + + + https://vyla.cc/go.html + weekly + 0.9 + + + https://vyla.cc/tv.html + weekly + 0.9 + + + https://vyla.cc/player.html + weekly + 0.8 + + + https://vyla.cc/sdk.html + weekly + 0.9 + + + https://vyla.cc/sdk-quickstart.html + weekly + 0.9 + + + https://vyla.cc/stream-api/overview.html + weekly + 0.9 + + + https://vyla.cc/stream-api/paths.html + weekly + 0.8 + + + https://vyla.cc/stream-api/media-data.html + weekly + 0.8 + + + https://vyla.cc/stream-api/operations.html + weekly + 0.8 + + + https://vyla.cc/self-hosting.html + weekly + 0.9 + + + https://vyla.cc/concepts/providers.html + weekly + 0.8 + + + https://vyla.cc/guides/integrations.html + weekly + 0.8 + + + https://vyla.cc/guides/error-handling.html + weekly + 0.8 + + + https://vyla.cc/misc/credit.html + monthly + 0.5 + + + https://vyla.cc/misc/dmca.html + monthly + 0.5 + + diff --git a/stream-api/media-data/index.html b/stream-api/media-data/index.html new file mode 100644 index 0000000..3712415 --- /dev/null +++ b/stream-api/media-data/index.html @@ -0,0 +1,199 @@ + + + + + + + Subtitles and downloads — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Subtitles and downloads

+

Unified media-data paths for movies and TV episodes.

+

Subtitle paths

+ + + + + + + + + + + + + + + + + +
MediaPath
MovieGET /subtitles/movie/:id
TV episodeGET /subtitles/tv/:id/:season/:episode
+

The same routes are available under /api/subtitles. A successful response is a JSON array of + tracks; a missing result returns 404 with an error object.

+
[
+  {
+    "label": "English",
+    "file": "https://example.invalid/subtitles/en.vtt",
+    "type": "vtt",
+    "source": "v1"
+  }
+]
+
+

file is the track URL, type is normally vtt or srt, and + source identifies the subtitle backend. Prefer the tracks included in the playback + meta event when you are already opening a playback stream; use these paths when subtitles are + fetched separately. +

+

Download paths

+ + + + + + + + + + + + + + + + + +
MediaPath
MovieGET /downloads/movie/:id
TV episodeGET /downloads/tv/:id/:season/:episode
+

The /api/downloads prefix is also supported. A successful response is an array; an empty array + simply means no candidates were found.

+
[
+  {
+    "url": "https://example.invalid/file.mp4",
+    "quality": "1080p",
+    "size": "2.14 GB",
+    "format": "MP4"
+  }
+]
+
+

Download URLs are provider results, not durable Vyla-hosted files. Display quality and size as informational + fields, and handle expired or unavailable links in your client.

+

Request and response rules

+
    +
  • IDs are TMDB IDs; TV paths additionally require season and episode numbers.
  • +
  • Both families return JSON, unlike the SSE playback paths.
  • +
  • Handle 404 for missing subtitles and 500 for an upstream or resolver failure. +
  • +
  • Do not block first playback on either call. Fetch them in parallel or after the player has started.
  • +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/stream-api/operations/index.html b/stream-api/operations/index.html new file mode 100644 index 0000000..56e1a0b --- /dev/null +++ b/stream-api/operations/index.html @@ -0,0 +1,167 @@ + + + + + + + Health, testing, and safe operations — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Health, testing, and safe operations

+

Observe SDK providers and diagnose a Stream API deployment without making debug data + public.

+

Health

+

GET /health (or /api/health) probes active providers and returns a JSON summary. It + includes a status, timestamp, TMDB-key presence, cache size, and a map of provider keys to + { ok, ms } results. +

+

Health is a signal, not a promise of playback. Provider behavior can differ by title, client network, and the + moment of the request. Use it for observability and source selection—not as a guarantee that a viewer will + receive a stream.

+

Test one provider

+

GET /test/:id?source=:providerKey resolves a single provider for a movie. Add + season and episode (or s and e) for TV. +

+
curl "http://localhost:7860/test/155?source=provider-key"
+curl "http://localhost:7860/test/1396?season=1&episode=1&source=provider-key"
+
+

Use source keys reported by the running deployment. A test result reports whether resolution and verification + succeeded; clients should inspect the result body rather than relying only on a transport success.

+

Debug route

+

GET /debug/:id?source=:providerKey offers detailed candidate and request traces, but it is only + available when ENABLE_DEBUG_ROUTE=true.

+
+
+

Do not enable the debug route on a public deployment. It can expose upstream request data, raw + candidates, and headers. Enable it temporarily in a controlled environment, then disable it again.

+
+
+

Production checklist

+
    +
  • Set TMDB_API_KEY when metadata-dependent sources are required.
  • +
  • Keep ENABLE_DEBUG_ROUTE=false outside controlled troubleshooting.
  • +
  • Use TLS and enforce your own access policy before making the service remotely reachable.
  • +
  • Monitor health trends and playback failures separately.
  • +
  • Run updates and provider changes through your normal deployment process.
  • +
  • Build player fallbacks instead of assuming a specific provider is always available.
  • +
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/stream-api/overview/index.html b/stream-api/overview/index.html new file mode 100644 index 0000000..20b40e9 --- /dev/null +++ b/stream-api/overview/index.html @@ -0,0 +1,194 @@ + + + + + + + Stream API — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Stream API

+

An optional self-hosted HTTP and SSE layer built on top of Vyla SDK.

+

Use this only when HTTP is the right boundary

+

Stream API packages Vyla SDK behind a small Node.js HTTP service. It is useful when a browser, mobile app, or + another process needs progressive playback candidates over HTTP. It is not required to use Vyla, and it is not + the primary integration for a Node.js application—use Vyla SDK directly in that case. +

+

The service accepts TMDB IDs, asks configured SDK providers for candidates, verifies results, and emits + usable sources as they arrive. A player can start with the first source while retaining later sources as + fallbacks.

+

What it provides

+ + + + + + + + + + + + + + + + + + + + + + + + + +
CapabilityEndpoint familyWhy it exists
Progressive playback/movie, /tvStream metadata and verified source candidates with SSE
Supplemental media/subtitles, /downloadsFetch tracks or downloadable candidates separately
Operations/health, /test, /debugCheck source status and investigate a provider when permitted
+

Start a local instance

+
git clone https://gitlab.com/vyla-entertainment/stream-api
+cd stream-api
+npm install
+TMDB_API_KEY=your_key
+node server.js
+
+

The default local address is http://localhost:7860. For remote access, place the service behind + TLS and your own access controls. Full configuration is in Self-hosting.

+

Lifecycle of a playback request

+
    +
  1. A client opens /movie?id=:tmdbId or + /tv?id=:tmdbId&season=:season&episode=:episode. +
  2. +
  3. The API returns an SSE response and emits a meta event with title metadata and subtitle + tracks when available.
  4. +
  5. Active SDK providers resolve in parallel; verified results arrive as individual source + events.
  6. +
  7. The server sends done after all source attempts finish or time out.
  8. +
+

Start the player on the first source. Never wait for done before allowing playback, and always + retain a fallback queue. See Paths and events for the contract.

+
+
+

If your application is already a Node.js service, call VylaSDK#getStream, + getSubtitles, and getDownloads directly. That removes an unnecessary network hop + and lets you define your own public API. +

+
+
+
+ +
+
+ + + + + \ No newline at end of file diff --git a/stream-api/paths/index.html b/stream-api/paths/index.html new file mode 100644 index 0000000..467eb94 --- /dev/null +++ b/stream-api/paths/index.html @@ -0,0 +1,196 @@ + + + + + + + Paths and playback events — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Paths and playback events

+

The complete Stream API playback contract, unified for movies and TV episodes.

+

Playback paths

+

The API supports both the short paths below and /api-prefixed equivalents. A movie needs only + id; a TV episode also needs season and episode. +

+ + + + + + + + + + + + + + + + + + + + +
MediaPathResponse
MovieGET /movie?id=:idServer-Sent Events
TV episodeGET /tv?id=:id&season=:season&episode=:episodeServer-Sent Events
+

id is a TMDB ID. For example:

+
curl -N "http://localhost:7860/movie?id=155"
+curl -N "http://localhost:7860/tv?id=1396&season=1&episode=1"
+
+

Event contract

+

Playback routes use text/event-stream, not a single JSON document. Read each data: + line as JSON.

+

meta

+

Sent before source resolution completes. It can include TMDB metadata and an array of subtitle tracks.

+
{
+  "type": "meta",
+  "meta": { "id": 155, "title": "The Dark Knight" },
+  "subtitles": [{ "label": "English", "file": "https://…", "type": "vtt", "source": "v1" }]
+}
+
+

source

+

One event for each candidate that passes the service's verification pipeline.

+
{
+  "type": "source",
+  "source": {
+    "source": "provider-key",
+    "label": "Provider name",
+    "url": "http://localhost:7860/api?url=…"
+  }
+}
+
+

The url is ready for a player. HLS sources may be wrapped by Vyla's proxy so manifest segments + and keys can be fetched with the appropriate upstream headers. Some configured sources bypass proxying. Do not + assume a filename extension alone determines playback behavior.

+

done

+

Sent after the configured attempts are complete.

+
{ "type": "done", "total": 3 }
+
+

total: 0 means no playable sources were produced for that request. The SSE connection itself can + still be successful.

+

Client rules

+
    +
  1. Start playback as soon as the first source arrives.
  2. +
  3. Queue every later source for manual switching or automatic fallback.
  4. +
  5. Attach subtitle tracks from meta without blocking playback.
  6. +
  7. Treat an empty result as a normal product state, not a malformed response.
  8. +
  9. Abort the request when the viewer leaves the page or chooses another title.
  10. +
+

The integration guide contains a practical player pattern; error handling covers fallback behavior.

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/tv/index.html b/tv/index.html new file mode 100644 index 0000000..ee043d8 --- /dev/null +++ b/tv/index.html @@ -0,0 +1,157 @@ + + + + + + + Vyla TV — Vyla + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

Vyla TV

+

The Android TV application for running the Vyla media experience on your television. +

+ A collage of films and shows available through a Vyla experience +

Vyla TV

+

Vyla TV is an Android TV streaming app built with Jetpack Compose and ExoPlayer that lets + you browse and watch movies and TV shows from TMDB.

+

Downloads

+

Vyla TV is available for Android TV. Get the latest version below:

+ + + + + + + + + + + + + +
PlatformDownload
Android TVDownload .apk
+

Inside the releases page, you can find the latest version of Vyla TV for Android TV, find the + .apk file, and install it on your device. +

+
+ +
+
+ + + + + \ No newline at end of file diff --git a/update_seo.py b/update_seo.py new file mode 100644 index 0000000..ad7f45c --- /dev/null +++ b/update_seo.py @@ -0,0 +1,118 @@ +import os +import re + +base_dir = "dist" + +seo_templates = { + "architecture": { + "title": "Our Architecture — Vyla", + "description": "How Vyla Home, SDK, and Stream API relate, and where each responsibility belongs.", + "keywords": "Vyla architecture, Vyla Home, Vyla SDK, Stream API, system design, media stack, deployment patterns, self-hosted media", + "canonical": "architecture.html", + "image": "thumbnail.png" + }, + "go": { + "title": "Vyla Go — Vyla", + "description": "The Android control center for running the Vyla media stack locally.", + "keywords": "Vyla Go, Android media player, mobile streaming, self-hosted media, Android app, media streaming", + "canonical": "go.html", + "image": "android-preview.png" + }, + "player": { + "title": "Build a player — Vyla", + "description": "Design a resilient Vyla playback experience around your own product and infrastructure.", + "keywords": "Vyla player, media player, custom player, playback experience, HLS player, MP4 player, streaming player", + "canonical": "player.html", + "image": "thumbnail.png" + }, + "sdk-quickstart": { + "title": "SDK quickstart — Vyla", + "description": "Install Vyla SDK, resolve a source, and build a safe server-side integration.", + "keywords": "Vyla SDK, SDK quickstart, Node.js SDK, server-side integration, stream resolution, API integration", + "canonical": "sdk-quickstart.html", + "image": "thumbnail.png" + }, + "self-hosting": { + "title": "Self-host Stream API — Vyla", + "description": "Deploy the optional Stream API when your clients need an HTTP/SSE layer.", + "keywords": "self-hosting, Stream API deployment, Node.js deployment, HTTP API, SSE, server setup, infrastructure", + "canonical": "self-hosting.html", + "image": "thumbnail.png" + }, + "tv": { + "title": "Vyla TV — Vyla", + "description": "The Android TV application for running the Vyla media experience on your television.", + "keywords": "Vyla TV, Android TV, TV streaming, smart TV, television app, media streaming, Android TV app", + "canonical": "tv.html", + "image": "android-tv-preview.png" + }, + "stream-api/overview": { + "title": "Stream API — Vyla", + "description": "An optional self-hosted HTTP and SSE layer built on top of Vyla SDK.", + "keywords": "Stream API, HTTP API, SSE, Server-Sent Events, streaming API, self-hosted API, Node.js API", + "canonical": "stream-api/overview.html", + "image": "thumbnail.png" + }, + "stream-api/paths": { + "title": "Paths and playback events — Vyla", + "description": "The complete Stream API playback contract, unified for movies and TV episodes.", + "keywords": "Stream API paths, playback events, SSE events, API endpoints, movie API, TV API, streaming endpoints", + "canonical": "stream-api/paths.html", + "image": "thumbnail.png" + }, + "stream-api/media-data": { + "title": "Subtitles and downloads — Vyla", + "description": "Unified media-data paths for movies and TV episodes.", + "keywords": "subtitles, downloads, media data, subtitle tracks, download links, VTT, SRT, subtitle API", + "canonical": "stream-api/media-data.html", + "image": "thumbnail.png" + }, + "stream-api/operations": { + "title": "Health, testing, and safe operations — Vyla", + "description": "Observe SDK providers and diagnose a Stream API deployment without making debug data public.", + "keywords": "health check, API testing, operations, debugging, provider health, API monitoring, deployment", + "canonical": "stream-api/operations.html", + "image": "thumbnail.png" + }, + "concepts/providers": { + "title": "Providers — Vyla", + "description": "How Vyla SDK sources are configured, selected, and verified.", + "keywords": "providers, source configuration, stream sources, provider verification, SDK providers, source selection", + "canonical": "concepts/providers.html", + "image": "thumbnail.png" + }, + "guides/integrations": { + "title": "Stream API client integration — Vyla", + "description": "Build an optional Stream API player client with HLS/MP4 handling, SSE sources, subtitles, and fallbacks.", + "keywords": "client integration, player integration, HLS.js, React, Vue, iOS, Android, SSE client, streaming client", + "canonical": "guides/integrations.html", + "image": "thumbnail.png" + }, + "guides/error-handling": { + "title": "Stream API error handling — Vyla", + "description": "Handle SSE failures, source fallbacks, and edge cases in an optional Stream API client.", + "keywords": "error handling, fallbacks, SSE errors, streaming errors, retry logic, error boundary, source fallback", + "canonical": "guides/error-handling.html", + "image": "thumbnail.png" + }, + "misc/credit": { + "title": "Credit — Vyla", + "description": "Credits and inspiration behind the Vyla API ecosystem.", + "keywords": "credits, attribution, open source, inspiration, P-Stream, CinePro, Movieslay, acknowledgments", + "canonical": "misc/credit.html", + "image": "thumbnail.png" + }, + "misc/dmca": { + "title": "DMCA & Copyright — Vyla", + "description": "Content sources used by Vyla API and how rights holders can request removal.", + "keywords": "DMCA, copyright, takedown, legal, content sources, rights holders, copyright policy, takedown request", + "canonical": "misc/dmca.html", + "image": "thumbnail.png" + } +} + +def generate_seo_head(template): + name = template["title"].replace(" — Vyla", "") + json_ld = " \n " + + return " \n \n " + template["title"] + "\n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n \n \n \n \n\n \n \n \n\n \n \n \n \n\n" + json_ld + "\n\n