Watch It? Track It.

Sökningen är också ett statiskt index: cirka 7 000 små filer som tillsammans täcker alla namn på alla serier, filmer och personer, på alla språk. En sökfråga behöver bara en av dem.

Filer

  • 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

Slå upp en sökfråga

  1. Läs v1/search/manifest.json för att få id:t för det aktuella bygget.
  2. Läs v1/search/{build}/tree.json. Den ändras aldrig inom ett bygge, så hämta den en gång.
  3. Normalisera sökfrågan: gör den till gemener och ta bort accenter och skiljetecken. Slå upp den via dess längsta ord som inte är ett stoppord.
  4. Gå igenom trädet med det ordet för att hitta dess fil: en bucket i l/ eller, om ordet slutar exakt på ett delningsprefix, en sammanfattning i s/. Filnamnen är prefixets UTF-8-byte i hexadecimal form.
  5. Behåll de poster där varje ord i sökfrågan inleder något ord i namnet. Placera namn som börjar med hela sökfrågan först och sortera sedan efter popularitet.
  6. Tillämpa v1/search/recent.json: den listar poster som ändrats sedan bygget. Ta bort indexets poster för dessa och använd de här i stället. Filen ändras när poster redigeras, så lägg till en query string som ändras med några minuters mellanrum (?w= plus det aktuella 5-minutersfönstret) för att få en färsk kopia.

Poster

Varje post är en array:

PositionTypBeskrivning
0stringPosttyp.
1numberPostens id.
2stringNamnet som matchade, på vilket språk det än är.
3numberRangordning inom typen: 1 000 000 är populärast.
4stringPostens slug.
5number | nullÅr, för titlar.
6string | nullSökväg till miniatyren.

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

Indexet byggs om dagligen. Ett bygges filer ändras aldrig, så du kan cacha dem hur länge du vill; bara manifestet och recent.json ändras.