Watch It? Track It.

La búsqueda también es un índice estático: unos 7000 archivos pequeños que, en conjunto, cubren todos los nombres de todas las series, películas y personas, en todos los idiomas. Una consulta solo necesita uno de ellos.

Archivos

  • https://api.watchittrackit.com/v1/search/manifest.json
  • https://api.watchittrackit.com/v1/search/{build}/tree.json
  • https://api.watchittrackit.com/v1/search/{build}/l/{hex}.json
  • https://api.watchittrackit.com/v1/search/{build}/s/{hex}.json
  • https://api.watchittrackit.com/v1/search/recent.json

Resolver una consulta

  1. Lee v1/search/manifest.json para obtener el id de la compilación actual.
  2. Lee v1/search/{build}/tree.json. Nunca cambia dentro de una compilación, así que descárgalo una sola vez.
  3. Normaliza la consulta: pásala a minúsculas y elimina los acentos y la puntuación. Búscala por su palabra más larga que no sea una palabra vacía.
  4. Recorre el árbol con esa palabra para encontrar su archivo: un bucket en l/ o, si la palabra termina justo en un prefijo de división, un resumen en s/. Los nombres de archivo son el hexadecimal de los bytes UTF-8 del prefijo.
  5. Conserva las entradas en las que cada palabra de la consulta sea el comienzo de alguna palabra del nombre. Pon primero los nombres que empiezan por la consulta completa y después ordena por popularidad.
  6. Aplica v1/search/recent.json: enumera los registros que han cambiado desde la compilación. Descarta las entradas del índice para esos registros y usa estas en su lugar. Cambia a medida que se editan registros, así que añade una cadena de consulta que cambie cada pocos minutos (?w= más la ventana actual de 5 minutos) para obtener una copia actualizada.

Entradas

Cada entrada es un array:

PosiciónTipoDescripción
0stringTipo de registro.
1numberId del registro.
2stringEl nombre que coincidió, en el idioma que sea.
3numberPosición dentro de su tipo: 1 000 000 es el más popular.
4stringSlug del registro.
5number | nullAño, para los títulos.
6string | nullRuta de la miniatura.

Ejemplo (JavaScript)

const BASE = "https://api.watchittrackit.com/v1/search"
const STOPWORDS = new Set(["a", "an", "and", "the", "of", "in", "on", "to", "for", "at", "by", "with", "or",
  "de", "del", "la", "le", "les", "el", "los", "las", "lo", "un", "une", "et", "du", "des", "da", "do", "dos",
  "der", "die", "das", "und", "ein", "eine", "den", "dem", "von", "zu", "il", "i", "e", "di", "y", "en", "het", "een", "van"])

const normalize = (s) => s.normalize("NFKD").replace(/\p{M}/gu, "").toLowerCase()
  .replace(/[^\p{L}\p{N}]+/gu, " ").trim()
const hex = (s) => [...new TextEncoder().encode(s)].map((b) => b.toString(16).padStart(2, "0")).join("")

async function search(query) {
  const { build } = await fetch(`${BASE}/manifest.json`).then((r) => r.json())
  const tree = await fetch(`${BASE}/${build}/tree.json`).then((r) => r.json())

  const q = normalize(query)
  const words = q.split(" ").filter(Boolean)
  const real = words.filter((w) => !STOPWORDS.has(w))
  const word = (real.length ? real : words).sort((a, b) => [...b].length - [...a].length)[0]

  // Walk the tree: descend while the prefix is split; stop at a bucket or a summary.
  let key = "", file
  for (const c of word) {
    if (tree[key + c]) { key += c; continue }
    const starts = tree[key] ?? []
    const start = starts.filter((s) => s <= c).pop() ?? starts[0] ?? c
    file = `l/${hex(key + start)}.json`
    break
  }
  file ??= `s/${hex(key)}.json`

  const res = await fetch(`${BASE}/${build}/${file}`)
  let entries = res.ok ? await res.json() : []

  // Records changed since the build replace the index's entries for them.
  // The query string changes every 5 minutes, so caches don't serve an old copy.
  const recent = await fetch(`${BASE}/recent.json?w=${Math.floor(Date.now() / 300000)}`).then((r) => r.json())
  entries = entries.filter(([type, id]) => !(`${type}:${id}` in recent.records))
  entries.push(...Object.values(recent.records).flat())

  const seen = new Set()
  return entries
    .filter((e) => { const n = normalize(e[2]).split(" "); return words.every((w) => n.some((x) => x.startsWith(w))) })
    .sort((a, b) => Number(normalize(b[2]).startsWith(q)) - Number(normalize(a[2]).startsWith(q)) || b[3] - a[3])
    .filter(([type, id]) => !seen.has(`${type}:${id}`) && seen.add(`${type}:${id}`))
    .slice(0, 30)
}

// [["tv", 81189, "Breaking Bad", 999998, "breaking-bad", 2008, "/images/tv/81189/….t.jpg"], …]
console.log(await search("breaking bad"))

El índice se reconstruye a diario. Los archivos de una compilación nunca cambian, así que puedes guardarlos en caché todo el tiempo que quieras; solo cambian el manifiesto y recent.json.