// How the import finder asks when it is not ALLOWED to ask. // // The Soulseek server drops a search outright when the query carries every // word of one of its excluded phrases. Measured 2026-08-11 against the live // server, with the record's own copies provably on the network: // // animal collective merriweather 578 peers ยท 2,664 FLAC files // merriweather pavilion 683 peers // post pavilion 712 peers // merriweather post pavilion 0 peers <- every time // pavilion merriweather post 0 peers <- order is irrelevant // merriweather post pavilion flac 0 peers <- extra terms don't help // // The published exclusions behave identically ("village people" 0, "bryan // adams" 0, "village people flac" 0, while "abba gold" answers with 608), so // this is that same mechanism โ€” but the server publishes only SOME of its // list. `sanitizeSearchQuery` strips the phrases it does publish; an // unpublished one like this album's title reaches the wire and is swallowed. // // Nothing here can make the server answer a refused query. What it CAN do is // ask a shorter question the server does answer, and then apply the part we // were not allowed to say to the answers, where no server is involved. const STOPWORDS = new Set(["the", "a", "an", "and", "or", "of", "in", "on", "at", "to", "for", "with"]); /** Words, punctuation folded away. `&` and `-` are separators, not letters, so * "Rock & Roll" and "Rock-n-Roll" both yield asking words rather than one * token nothing will match. */ export function queryWords(s) { return String(s ?? "") .toLowerCase() .replace(/[^\p{L}\p{N}]+/gu, " ") .split(/\s+/) .filter(Boolean); } /** The record's name with its last word dropped โ€” the shortest change that * breaks the refused word-set while still naming the record. * * Trailing stopwords go with it: "The Dark Side of the Moon" without "moon" * is "the dark side of the", which asks the network for three words that * carry no meaning and narrow the result set for nothing. * * Returns "" when there is nothing left worth asking. A one-word name * shortens to nothing at all, and a bare artist query asks the network for * that artist's entire discography โ€” the same thing `findQueries` refuses * to do. */ export function shortenName(name) { const w = queryWords(name); if (w.length < 2) return ""; w.pop(); while (w.length && STOPWORDS.has(w[w.length - 1])) w.pop(); return w.join(" "); } /** Does this peer's path claim to BE the record we could not ask for? Every * word of the full name has to appear in it. * * This is the half of the question the server was not allowed to answer, and * it is what keeps the shortened query honest: "animal collective merriweather * post" also matches a live set recorded AT Merriweather Post Pavilion, and * the venue is in the path of every bootleg taped there. * * Deliberately whole-word, not substring: "Pavillion" is not "Pavilion", and * a folder that misspells the record is refused rather than guessed at. That * costs a few real copies whose owners cannot spell; accepting near-misses * costs correctness, and there is no way to tell the two apart from here. */ export function pathHasTitle(path, name) { const need = queryWords(name); if (!need.length) return false; const have = new Set(queryWords(path)); return need.every((w) => have.has(w)); }