Files
SoulStream/src/lib/import-query.js
T

74 lines
3.4 KiB
JavaScript

// 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));
}