Watch It? Track It.

Auch die Suche ist ein statischer Index: etwa 7.000 kleine Dateien, die zusammen jeden Namen jeder Serie, jedes Films und jeder Person in jeder Sprache abdecken. Eine Suchanfrage braucht nur eine davon.

Dateien

  • 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

Eine Suchanfrage nachschlagen

  1. Lies v1/search/manifest.json, um die aktuelle Build-ID zu erhalten.
  2. Lies v1/search/{build}/tree.json. Die Datei ändert sich innerhalb eines Builds nie, also lade sie nur einmal.
  3. Normalisiere die Suchanfrage: in Kleinbuchstaben umwandeln, Akzente und Satzzeichen entfernen. Schlage sie über ihr längstes Wort nach, das kein Stoppwort ist.
  4. Durchlaufe den Baum mit diesem Wort, um seine Datei zu finden: einen Bucket in l/ oder, wenn das Wort genau an einem Teilungspräfix endet, eine Zusammenfassung in s/. Dateinamen sind die Hex-Darstellung der UTF-8-Bytes des Präfixes.
  5. Behalte Einträge, bei denen jedes Wort der Suchanfrage den Anfang eines Wortes im Namen bildet. Stelle Namen, die mit der ganzen Suchanfrage beginnen, an den Anfang und sortiere dann nach Beliebtheit.
  6. Wende v1/search/recent.json an: Sie listet Datensätze auf, die sich seit dem Build geändert haben. Verwirf die Einträge des Index für diese Datensätze und verwende stattdessen diese. Die Datei ändert sich, sobald Datensätze bearbeitet werden, also hänge einen Query-String an, der sich alle paar Minuten ändert (?w= plus das aktuelle 5-Minuten-Fenster), um eine frische Kopie zu erhalten.

Einträge

Jeder Eintrag ist ein Array:

PositionTypBeschreibung
0stringDatensatztyp.
1numberDatensatz-ID.
2stringDer passende Name, in welcher Sprache auch immer.
3numberRang innerhalb des Typs: 1.000.000 ist der beliebteste.
4stringDatensatz-Slug.
5number | nullJahr, bei Titeln.
6string | nullPfad zum Vorschaubild.

Beispiel (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"))

Der Index wird täglich neu erstellt. Die Dateien eines Builds ändern sich nie, du kannst sie also beliebig lange cachen; nur das Manifest und recent.json ändern sich.