{
  "$comment": "Kino plugin contract, apiVersion 1 to 7. The single source of truth for every number and rule a plugin meets. The app checks each value against its own code, and a test in the app pins this file to it; sdk/validate.mjs, sdk/run.mjs and the guide's tables read this file.",
  "apiVersion": 7,
  "maxApiVersion": 7,
  "apiVersionFromApp": { "6": "0.9.50", "7": "0.9.51" },
  "additiveFromApp": { "kino.meta": "0.9.53", "kino.tmdb": "0.9.53" },
  "capabilities": {
    "names": ["search", "home", "browse", "episodes", "resolve", "download", "drm", "channels", "migrate", "scopedSearch", "meta", "subtitles", "tracking", "segments"],
    "required": ["resolve"],
    "atLeastOneOf": ["search", "home"],
    "standalone": ["subtitles", "tracking", "segments"],
    "anyPluginExports": ["subtitles"],
    "declarative": ["download", "drm", "scopedSearch"],
    "apiVersions": { "download": 2, "drm": 2, "channels": 3, "migrate": 6, "scopedSearch": 6, "meta": 6, "tracking": 7, "segments": 7 },
    "requires": { "scopedSearch": "search" },
    "requiresMessage": "La capacidad \"{capability}\" necesita también \"{requires}\"",
    "needsApproval": ["download", "drm", "channels", "migrate", "tracking"],
    "exports": { "channels": ["liveCategories", "liveChannels"], "tracking": ["track"] },
    "optionalExports": { "channels": ["guide", "liveSearch"], "resolve": ["sign"] }
  },
  "manifest": {
    "maxBytes": 16384,
    "idPattern": "^[a-z0-9][a-z0-9-]{1,39}$",
    "reservedIds": ["live", "local", "unknown", "plugin", "own", "subtitle-keys", "subtitle-prefs"],
    "nameMaxChars": 40,
    "descriptionMaxChars": 300,
    "authorMaxChars": 60,
    "homepageMaxChars": 200,
    "minHosts": 1,
    "legacyMaxHosts": { "value": 20, "refusedUpToApp": "0.9.44", "noLimitFromApp": "0.9.45" },
    "noHostsApiVersion": 2,
    "insecureHostApiVersion": 2,
    "colorPattern": "^#[0-9A-Fa-f]{6}$",
    "entryMaxBytes": 1048576,
    "iconMaxBytes": 131072,
    "versionPattern": "^(0|[1-9]\\d{0,5})\\.(0|[1-9]\\d{0,5})\\.(0|[1-9]\\d{0,5})$",
    "pathSegmentPattern": "^[A-Za-z0-9._-]+$",
    "maxPathChars": 200,
    "liveStreamHosts": { "value": "any", "apiVersion": 3, "requires": "channels" },
    "streamHosts": { "value": "any", "apiVersion": 4 },
    "fetchHosts": { "value": "any", "apiVersion": 4 },
    "discoverable": { "default": true },
    "categories": { "values": ["movies", "series", "anime", "live", "radio", "subtitles", "utilities", "adult"], "message": "El campo \"categories\" debe ser una lista sin repetidos de: movies, series, anime, live, radio, subtitles, utilities, adult" },
    "debug": { "apiVersion": 6, "default": false, "switchLabel": "Modo debug", "switchHint": "Muestra los errores de este plugin en pantalla y guarda un registro que puedes compartir con su autor.", "defaultOnNote": "Modo debug encendido de entrada (\"debug\": true): quien instale el plugin verá sus errores en pantalla y se guardará su registro hasta que apague el interruptor en Ajustes. Sin el campo, cada persona puede encenderlo cuando quiera enviarte un registro" },
    "browser": { "apiVersion": 6, "consentLine": "Puede abrir páginas web ocultas para encontrar el video", "notBooleanMessage": "El campo \"browser\" debe ser true, false o \"pages\"", "pageApiVersion": 6, "pagesValue": "pages", "pageConsentLine": "Puede abrir páginas web ocultas para mostrar contenido y encontrar el video" },
    "telemetry": { "apiVersion": 6, "consentLine": "Comparte registros de errores con Kino para corregir fallas", "notBooleanMessage": "El campo \"telemetry\" debe ser true, false o \"verbose\"", "switchLabel": "Enviar registros de errores", "report": { "windowSeconds": 3600, "areaMaxChars": 24, "areaPattern": "^(?=[a-z0-9_:]*[_:])[a-z0-9_:]{1,24}$", "otherArea": "other", "perPluginPerSession": 3, "sessionShare": 10 }, "verbose": { "value": "verbose", "consentLine": "Comparte registros detallados de reproducción y errores con Kino para corregir fallas", "successSample": 0.25, "perPluginPerSession": 60, "areaWindowSeconds": 60 }, "logcat": { "playTag": "KinoPlay", "pluginTagPrefix": "KinoPlugin/" } },
    "section": { "apiVersion": 6, "labelMaxChars": 20, "export": "section" },
    "theme": {
      "apiVersion": 6,
      "tokens": ["accent", "onAccent", "background", "surface", "highlight"],
      "defaults": { "accent": "#E50914", "onAccent": "#FFFFFF", "background": "#0E0E0E", "surface": "#181818", "highlight": "#F5F5F5" },
      "brandRed": "#E50914",
      "minTextContrast": 4.5,
      "minUiContrast": 3,
      "maxBackgroundLuminance": 0.05,
      "maxSurfaceLuminance": 0.12,
      "minSurfaceContrast": 1.05,
      "minBrandDeltaE": 25
    },
    "secrets": { "apiVersion": 4, "namePattern": "^[A-Za-z][A-Za-z0-9_]{0,31}$", "maxSecrets": 16, "maxValueBytes": 4096, "prefix": "kino-sealed:v1:", "largeApiVersion": 6, "largeMaxValueBytes": 8192, "typed": { "apiVersion": 6, "uses": ["cipher-key"], "keyEncodings": ["hex", "base64"], "keyBytes": [16, 24, 32] } },
    "signature": { "apiVersion": 5, "fromApp": "0.9.45", "domain": "kino-signed-entry:v1", "authorKeyHexChars": 64, "valueHexChars": 128, "consentLine": "Firmado por su autor", "authorKeyLabel": "Clave del autor", "badFieldMessage": "El campo \"signature\" debe ser { \"authorKey\": 64 caracteres hex, \"value\": 128 caracteres hex } (node sdk/seal.mjs --sign)", "badSignatureMessage": "La firma del autor no es válida: el código no es el que firmó, o no es para este repositorio, este plugin o esta versión" }
  },
  "hostRules": {
    "labelPattern": "^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$",
    "privateSuffixes": [".local", ".lan", ".internal", ".localhost", ".home.arpa"],
    "maxHostChars": 253
  },
  "permissions": [],
  "settings": {
    "max": 12,
    "keyPattern": "^[a-z][a-zA-Z0-9_]{0,31}$",
    "labelMaxChars": 40,
    "hintMaxChars": 80,
    "sectionHintMaxChars": 300,
    "maxOptions": 20,
    "optionValueMaxChars": 40,
    "optionLabelMaxChars": 40,
    "types": {
      "text": { "maxChars": 500, "canBeRequired": true, "canHaveDefault": true },
      "url": { "maxChars": 2048, "canBeRequired": true, "canHaveDefault": false },
      "password": { "maxChars": 500, "canBeRequired": true, "canHaveDefault": true },
      "toggle": { "canBeRequired": false, "canHaveDefault": true },
      "select": { "canBeRequired": false, "canHaveDefault": true },
      "list": { "canBeRequired": true, "canHaveDefault": false },
      "section": { "canBeRequired": false, "canHaveDefault": false, "hasValue": false, "apiVersion": 6 },
      "status": { "canBeRequired": false, "canHaveDefault": false, "hasValue": false, "apiVersion": 6 },
      "action": { "canBeRequired": false, "canHaveDefault": false, "hasValue": false, "apiVersion": 6 }
    },
    "list": { "apiVersion": 4, "defaultMaxEntries": 20, "maxEntries": 50, "maxFields": 4, "fieldTypes": ["text", "url"] },
    "ui": {
      "apiVersion": 6,
      "maxItems": 16,
      "confirmMaxChars": 120,
      "statusMaxChars": 200,
      "messageMaxChars": 300,
      "fieldErrorMaxChars": 200,
      "clearSettings": { "maxEntries": 12 },
      "exports": { "status": "settingsStatus", "action": "action", "validate": "validateSettings" }
    }
  },
  "output": {
    "section": { "apiVersion": 6, "maxTabs": 8, "maxTabLabelChars": 24, "maxHeroTextChars": 300 },
    "categories": { "apiVersion": 6, "export": "categories", "maxCategories": 24, "maxTitleChars": 40 },
    "meta": { "apiVersion": 6, "fromApp": "0.9.51", "fields": ["logo", "ratings", "cast"], "ratingSources": ["imdb", "tmdb", "rottentomatoes", "metacritic", "letterboxd", "mal", "anilist", "trakt"], "maxRatings": 6, "ratingValuePattern": "^\\d{1,3}([.,]\\d{1,2})?(%|/\\d{1,3})?$", "maxCast": 20, "maxCastNameChars": 60, "maxEpisodes": 5000, "maxRatingSourceChars": 20, "yearReadChars": 9, "yearPattern": "\\d{4}", "minEpisodeSeason": 0, "answerFields": ["title", "overview", "poster", "backdrop", "episodes", "genres", "logo", "ratings", "cast"], "ratingLabels": { "imdb": "IMDb", "tmdb": "TMDB", "rottentomatoes": "Rotten Tomatoes", "metacritic": "Metacritic", "letterboxd": "Letterboxd", "mal": "MAL", "anilist": "AniList", "trakt": "Trakt" }, "pageCastNames": 5, "cacheTtlMs": 1800000, "maxCachedTitles": 200 },
    "retry": { "apiVersion": 6, "reasons": ["conflict", "expired"], "maxAttempts": 3, "statuses": [401, 403, 409] },
    "signing": {
      "value": "request",
      "apiVersion": 6,
      "maxContextChars": 4096,
      "hlsMimes": ["application/x-mpegurl", "application/vnd.apple.mpegurl", "audio/mpegurl"],
      "alternateHosts": { "apiVersion": 6, "maxEntries": 6, "pattern": "^[A-Za-z0-9.-]{1,253}(:[0-9]{1,5})?$" }
    },
    "itemIdPattern": "^[A-Za-z0-9._~-]{1,128}$",
    "maxSearchItems": 100,
    "maxHomeRows": 20,
    "maxRowItems": 60,
    "maxBrowseItems": 100,
    "maxEpisodes": 5000,
    "maxSeasons": 50,
    "maxRefChars": 4096,
    "maxCursorChars": 2048,
    "maxResultChars": 2000000,
    "maxImageUrlChars": 2048,
    "maxGenres": 5,
    "maxGenreChars": 30,
    "maxBadges": 3,
    "maxBadgeChars": 20,
    "imdbPattern": "^tt\\d{5,10}$",
    "minRating": 0,
    "maxRating": 10,
    "minRuntimeMinutes": 1,
    "maxRuntimeMinutes": 1000,
    "minExpiresInSeconds": 30,
    "maxExpiresInSeconds": 86400,
    "airDatePattern": "^\\d{4}-\\d{2}-\\d{2}$",
    "maxTitleChars": 200,
    "maxTextChars": 2000,
    "maxSeasonNumber": 999,
    "maxEpisodeNumber": 99999,
    "maxSubtitles": 30,
    "maxSubtitleLabelChars": 60,
    "maxAudioTracks": 8,
    "maxAlternatives": 8,
    "streamLabels": { "apiVersion": 6, "maxChars": 48, "maxRefChars": 512 },
    "skip": { "field": "skip", "fields": ["openingStartMs", "openingEndMs", "endingStartMs"], "maxMs": 86400000 },
    "maxHeaders": 20,
    "drmKeys": ["drm", "license", "licenseUrl", "drmLicenseUrl", "keySystem", "widevine"],
    "drm": { "field": "drm", "types": ["widevine"] },
    "itemKinds": ["movie", "series", "live"],
    "migrateKinds": ["movie", "series", "episode", "live"],
    "liveKindApiVersion": 2,
    "adultApiVersion": 6,
    "homeLiveApiVersion": 6
  },
  "live": {
    "apiVersion": 3,
    "maxCategories": 200,
    "maxChannelsPerPage": 500,
    "maxPagesPerCategory": 10,
    "pagesPerStep": 5,
    "maxChannelsPerCategory": 10000,
    "maxTotalPagesPerCategory": 200,
    "maxSearchChannels": 100,
    "minSearchChars": 2,
    "maxGuideChannels": 50,
    "maxGuideWindowMs": 86400000,
    "maxGuideEntriesPerChannel": 100,
    "maxChannelNumber": 9999,
    "maxPlaylists": 10,
    "adultApiVersion": 6,
    "defaultRefreshHours": 12,
    "minRefreshHours": 1,
    "maxRefreshHours": 168,
    "maxHideGroups": 50,
    "maxPlaylistBytes": 20971520,
    "maxEpgBytes": 52428800,
    "maxChannelsPerProvider": 5000,
    "maxCategoriesPerProvider": 500,
    "playlistParseBudgetMs": 20000,
    "epgParseBudgetMs": 30000,
    "reservedIdPrefix": "~",
    "playlistFormats": ["m3u"],
    "epgFormats": ["xmltv"]
  },
  "genres": ["peliculas", "series", "anime", "infantil", "documentales", "deportes", "noticias", "musica", "entretenimiento", "otros"],
  "discovery": { "topic": "kino-plugin", "maxResults": 30 },
  "search": {
    "types": ["movie", "series", "any"],
    "maxAltTitles": 5,
    "maxAltTitleChars": 200,
    "scoped": { "capability": "scopedSearch", "apiVersion": 6, "field": "within", "debounceMs": 400, "softTimeoutMs": 6000, "local": { "minMatches": 24, "maxExtraPages": 10, "maxExtraItems": 300 } }
  },
  "timeoutsMs": {
    "load": 10000,
    "search": 15000,
    "home": 20000,
    "browse": 20000,
    "episodes": 20000,
    "resolve": 20000,
    "nuvioResolve": 75000,
    "browserResolve": 75000,
    "liveCategories": 20000,
    "liveChannels": 20000,
    "guide": 20000,
    "liveSearch": 15000,
    "settingsStatus": 10000,
    "action": 30000,
    "section": 20000,
    "categories": 20000,
    "validateSettings": 20000,
    "migrate": 10000,
    "sign": 1500,
    "signTotal": 3000,
    "subtitles": 10000,
    "track": 10000,
    "segments": 8000,
    "meta": 6000
  },
  "runtime": {
    "memoryBytes": 67108864,
    "stackBytes": 1048576,
    "idleCloseMs": 300000,
    "timeoutsBeforeUnresponsive": 3,
    "maxLogChars": 2000,
    "reportLogLines": 30,
    "reportLogLineChars": 300,
    "reportLogChars": 2048,
    "maxErrorChars": 2000,
    "maxThrownNameChars": 100
  },
  "fetch": {
    "methods": ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"],
    "bodyKinds": ["text", "json", "form", "base64"],
    "redirectModes": ["follow", "manual"],
    "defaultTimeoutMs": 15000,
    "maxTimeoutMs": 30000,
    "maxBodyBytes": 5242880,
    "maxRequestChars": 1048576,
    "maxRequestsPerCall": 60,
    "nuvioMaxRequestsPerCall": 250,
    "maxInFlight": 6,
    "maxHostQuestionsPerCall": 3,
    "maxRedirects": 10,
    "errorCodes": ["host_not_allowed", "timeout", "network", "too_large", "invalid_request"]
  },
  "browser": {
    "apiVersion": 6,
    "onlyFrom": "resolve",
    "onlyWhen": "the person pressed play, or a download the person started (never a background resolve such as the live zap's resolve ahead, which Kino skips for a browser plugin); a download's capture never waits for another page: busy at once",
    "defaultTimeoutMs": 18000,
    "minTimeoutMs": 1000,
    "maxTimeoutMs": 25000,
    "maxMatchChars": 500,
    "maxMedia": 8,
    "maxSubtitles": 10,
    "graceMs": 1000,
    "errorCodes": ["browser_unavailable", "timeout", "blocked", "busy", "not_allowed", "invalid_request"],
    "page": {
      "apiVersion": 6,
      "manifest": "\"browser\": \"pages\" (\"browser\": true stays capture-only)",
      "onlyFrom": ["search", "home", "browse", "episodes", "section", "resolve"],
      "onlyWhen": "the person is using the app: their own search, Home, list, title or play (a resolve also for a download they started); never a background call (prewarm, the live zap's resolve ahead, sync, update checks), and a list's read only with the app in front. Never clicks, taps, types or scrolls: only a check that completes with no input passes",
      "defaultTimeoutMs": 15000,
      "minTimeoutMs": 1000,
      "maxTimeoutMs": 25000,
      "maxWaitForChars": 500,
      "maxHtmlChars": 2000000,
      "perMinute": 20,
      "pollMs": 500,
      "lockWaitMs": 8000,
      "callMarginMs": 1500,
      "topLevel": "every top-level navigation and the document read stay on the plugin's kino.fetch hosts (https); else blocked, no HTML",
      "waitForFlags": ["m", "s"],
      "errorCodes": ["browser_unavailable", "timeout", "blocked", "busy", "not_allowed", "invalid_request", "rate_limited"]
    }
  },
  "tracking": {
    "apiVersion": 7,
    "capability": "tracking",
    "export": "track",
    "consentLine": "Le contará a {hosts} qué ves y cuándo lo terminas",
    "consentLineNoHosts": "Le contará al servidor que escribas en su configuración qué ves y cuándo lo terminas",
    "consentMaxHosts": 3,
    "switchLabel": "Enviar lo que veo",
    "types": ["start", "progress", "stop", "watched"],
    "kinds": ["movie", "episode"],
    "idKeys": ["imdb", "tmdb", "tvdb", "anilist", "mal"],
    "progressIntervalMs": 300000,
    "watched": { "remainingMs": 180000, "minFraction": 0.9 },
    "outbox": { "maxPerPlugin": 200, "maxAgeMs": 604800000, "maxAttempts": 12, "backoffBaseMs": 30000, "backoffMaxMs": 21600000, "failuresBeforeShown": 3, "ledgerMaxPerPlugin": 5000 },
    "idResolveTimeoutMs": 8000,
    "retryCodes": ["timeout", "network", "unavailable", "rate_limited"],
    "dropCodes": ["auth_required", "invalid_request", "not_found", "geo_blocked", "host_not_allowed", "too_large"]
  },
  "segments": {
    "apiVersion": 7,
    "capability": "segments",
    "export": "segments",
    "consentLine": "Agrega el botón para saltar la intro y los créditos",
    "kinds": ["movie", "episode"],
    "idKeys": ["imdb", "tmdb", "tvdb", "anilist", "mal"],
    "types": ["intro", "outro", "recap", "credits", "preview"],
    "openingTypes": ["intro"],
    "endingTypes": ["outro", "credits"],
    "minLengthMs": 1000,
    "maxSegments": 10,
    "maxEntriesRead": 100,
    "endSlackMs": 5000,
    "waitMs": 12000,
    "durationBucketMs": 10000,
    "failureRetryMs": 120000
  },
  "kinoMeta": {
    "fromApp": "0.9.53",
    "featureDetect": "typeof kino.meta === \"function\"",
    "types": ["movie", "series"],
    "idKeys": ["imdb", "tmdb", "tvdb", "kitsu", "mal", "anilist"],
    "maxNumericId": 2147483647,
    "langPattern": "^[A-Za-z]{2,3}([-_][A-Za-z0-9]{2,8})?$",
    "maxRequestChars": 4096,
    "perMinute": 30,
    "timeoutMs": 8000,
    "providerTimeoutMs": 6000,
    "cacheTtlMs": 1800000,
    "maxCachedTitles": 200,
    "maxTmdbSeasons": 30,
    "maxAnswerChars": 1000000,
    "sources": ["tmdb", "anilist", "plugin"],
    "answerFields": ["title", "overview", "year", "poster", "backdrop", "logo", "genres", "runtimeMinutes", "tagline", "certification", "directors", "episodes", "ids", "ratings", "cast", "sources"],
    "notFrom": ["sign"],
    "noPluginsFrom": ["meta"],
    "errorCodes": ["invalid_request", "rate_limited", "not_allowed"]
  },
  "kinoTmdb": {
    "fromApp": "0.9.53",
    "featureDetect": "typeof kino.tmdb === \"function\"",
    "base": "https://api.themoviedb.org/3",
    "pathPrefixes": ["/discover", "/trending", "/search", "/movie", "/tv", "/find", "/genre", "/configuration", "/person", "/collection"],
    "pathPattern": "^/[A-Za-z0-9._/-]{1,255}$",
    "maxParams": 20,
    "paramKeyPattern": "^[A-Za-z0-9_.]{1,40}$",
    "maxParamValueChars": 500,
    "forbiddenParams": ["api_key", "session_id", "guest_session_id", "request_token", "access_token"],
    "perWindow": 40,
    "windowMs": 10000,
    "timeoutMs": 15000,
    "maxBodyBytes": 2097152,
    "cacheTtlMs": 600000,
    "maxCachedEntries": 300,
    "maxCachedBodyChars": 524288,
    "$keyOrder": "Kino's own key first, behind its cache, in-flight dedup and the two kinoKey limits; the person's key (their Ajustes setting, else a Stremio addon's they agreed to) only when Kino's key gets one of personKeyOn; with neither, a cached copy up to staleMaxDays old, else rate_limited (unavailable when TMDB refused Kino's key). no_tmdb_key only on a build without a key of Kino's and a person without one. A key a plugin keeps in its own settings is never read.",
    "keyOrder": ["kino", "setting", "stremioAddon"],
    "kinoKeyPerWindow": 20,
    "kinoKeyGlobalPerWindow": 60,
    "personKeyOn": [401, 403, 429, "kinoKeyLimit"],
    "staleMaxDays": 7,
    "keyPatterns": { "v3": "^[0-9a-fA-F]{32}$", "v4": "^eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+$" },
    "notFrom": ["sign"],
    "errorCodes": ["invalid_request", "rate_limited", "no_tmdb_key", "not_found", "too_large", "timeout", "network", "unavailable", "not_allowed"],
    "noKeyCode": "no_tmdb_key",
    "noKeyUserMessage": {
      "es": "Agrega tu llave de TMDB en Ajustes, o instala un addon de TMDB de Stremio configurado con tu llave.",
      "en": "Add your TMDB key in Settings, or install a Stremio TMDB addon set up with your key."
    }
  },
  "cookies": {
    "maxPerHost": 50,
    "maxTotalBytes": 65536
  },
  "storage": {
    "maxTotalBytes": 262144,
    "maxTtlMs": 2592000000
  },
  "crypto": {
    "hashes": ["md5", "sha1", "sha256", "sha512"],
    "ciphers": ["aes-128-cbc", "aes-192-cbc", "aes-256-cbc", "aes-128-ecb", "aes-192-ecb", "aes-256-ecb", "aes-128-ctr", "aes-192-ctr", "aes-256-ctr", "aes-128-gcm", "aes-192-gcm", "aes-256-gcm", "des-ede3-cbc", "des-ede3-ecb"],
    "encodings": ["utf8", "hex", "base64"],
    "pbkdf2Hashes": ["sha1", "sha256", "sha512"],
    "pbkdf2MaxIterations": 100000,
    "pbkdf2MaxKeyBytes": 64,
    "randomMaxBytes": 1024,
    "maxDataBytes": 5242880,
    "errorCode": "crypto_error",
    "keyPairs": {
      "apiVersion": 6,
      "types": ["ec", "ed25519", "x25519"],
      "curves": ["P-256", "P-384"],
      "signHashes": ["SHA-256", "SHA-384"],
      "signatureFormats": ["der", "ieee-p1363"],
      "importFormats": ["jwk", "spki", "raw"],
      "maxKeysPerRuntime": 64,
      "maxSignatureBytes": 512
    }
  },
  "sleep": {
    "maxMs": 5000
  },
  "errors": {
    "codes": ["auth_required", "not_found", "geo_blocked", "rate_limited", "unavailable"],
    "maxMessageChars": 200,
    "maxUserMessageChars": 160
  }
}
