d65f0b2091
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 19s
CI / backend-lint-and-test (push) Successful in 20s
extension / lint (push) Successful in 13s
CI / intimp (push) Successful in 3m41s
CI / intapi (push) Successful in 7m11s
CI / intcore (push) Successful in 7m41s
Operator-asked 2026-05-31 (during sidecar synthetic anchor cleanup): "the add source/subscription button idea to the firefox extension so it can tell me if a source/artist is added or not and offer an option to add it if it isn't." Plan tracked in Scribe task #507. ## Backend - `ExtensionService.probe(url)` — read-only resolution. Reuses `_derive` for platform+slug, then 2 SELECTs. Returns one of: - `source_match` (exact (artist, platform, url) Source exists) - `artist_match` (artist exists, this URL isn't a Source yet; collapses the sidecar-synthetic-only case from v26.06.01.0) - `new` (neither exists) - `unknown_platform` (URL didn't match any artist-page regex) - `GET /api/extension/probe?url=...` route with `X-Extension-Key` auth posture matching `/quick-add-source`. Read-only, side-effect free. - 6 backend tests in tests/test_api_extension.py covering each state + auth + invalid URL. ## Extension - `api.js`: `probeSource(url)` mirroring `quickAddSource` shape. - `background.js`: `PROBE_SOURCE` + `OPEN_ARTIST_PAGE` handlers. The latter strips the `/api` suffix from configured `apiUrl` (placeholder format per options.html) and opens `${base}/artist/{slug}` in a new tab via `browser.tabs.create`. - `content-script.js`: probe-first render — on page-load and SPA navigation, asks the backend for the URL's state and renders the chip in the matching color/copy on FIRST paint instead of flashing generic "Add" and updating after. Click handler branches: `source_match` → OPEN_ARTIST_PAGE; `artist_match`/`new` → existing ADD_AS_SOURCE flow (then re-probes so the chip flips green immediately, no wait for next nav). - `content-script.css`: three state-color modifiers (--new, --artist-match, --source-match) on the FC parchment-on-slate palette. Sage for already-added, amber for artist-exists, accent orange for new. ## Versioning - `extension/manifest.json` + `extension/package.json` → 1.0.6. build.yml's sign-extension job will fire on push to main since no `ext-1.0.6` Forgejo/Gitea release exists yet — exercises the regenerated AMO keys end-to-end. ## Behavior on the sidecar-synthetic case Filesystem-imported "Dymkens"-style artist with only a sidecar synthetic Source: probe returns `artist_match` (not `new`), so the chip reads "+ Add Patreon source to Dymkens" rather than offering to recreate the artist. Clicking adds the real Source; existing `_source_for_sidecar` preference logic (v26.06.01.0) routes future gallery-dl Posts to the real one.
100 lines
2.6 KiB
JavaScript
100 lines
2.6 KiB
JavaScript
/**
|
|
* FC backend client. Talks to /api/credentials (FC-3b), /api/sources
|
|
* (FC-3a), and the new /api/extension/* endpoints (FC-3g).
|
|
*/
|
|
|
|
class FabledCuratorAPI {
|
|
constructor() {
|
|
this.baseUrl = null;
|
|
this.apiKey = null;
|
|
}
|
|
|
|
async init() {
|
|
const cfg = await browser.storage.local.get(['apiUrl', 'apiKey']);
|
|
this.baseUrl = cfg.apiUrl || null;
|
|
this.apiKey = cfg.apiKey || null;
|
|
return this.isConfigured();
|
|
}
|
|
|
|
isConfigured() {
|
|
return !!(this.baseUrl && this.apiKey);
|
|
}
|
|
|
|
async request(method, endpoint, data = null) {
|
|
if (!this.isConfigured()) {
|
|
throw new Error('Not configured. Set FC URL + API key in settings.');
|
|
}
|
|
const url = `${this.baseUrl.replace(/\/+$/, '')}${endpoint}`;
|
|
const options = {
|
|
method,
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'X-Extension-Key': this.apiKey,
|
|
},
|
|
};
|
|
if (data) options.body = JSON.stringify(data);
|
|
|
|
let response;
|
|
try {
|
|
response = await fetch(url, options);
|
|
} catch (e) {
|
|
throw new Error('Cannot connect to FabledCurator. Check URL.');
|
|
}
|
|
|
|
if (!response.ok) {
|
|
let message;
|
|
try {
|
|
const body = await response.json();
|
|
message = body.error || `HTTP ${response.status}`;
|
|
if (body.detail) message += `: ${body.detail}`;
|
|
} catch {
|
|
message = `HTTP ${response.status}: ${response.statusText}`;
|
|
}
|
|
const err = new Error(message);
|
|
err.status = response.status;
|
|
throw err;
|
|
}
|
|
|
|
if (response.status === 204) return { success: true };
|
|
return response.json();
|
|
}
|
|
|
|
// FC-3b — credentials.
|
|
uploadCredentials(platform, credentialType, data) {
|
|
return this.request('POST', '/credentials', {
|
|
platform,
|
|
credential_type: credentialType,
|
|
data,
|
|
});
|
|
}
|
|
getCredentials() {
|
|
return this.request('GET', '/credentials');
|
|
}
|
|
|
|
// FC-3a — sources.
|
|
listSources() {
|
|
return this.request('GET', '/sources');
|
|
}
|
|
triggerSourceCheck(sourceId) {
|
|
return this.request('POST', `/sources/${sourceId}/check`);
|
|
}
|
|
|
|
// FC-3g — extension-specific.
|
|
quickAddSource(url) {
|
|
return this.request('POST', '/extension/quick-add-source', { url });
|
|
}
|
|
probeSource(url) {
|
|
// Read-only existence check. Drives the content-script chip's
|
|
// color/copy BEFORE the operator clicks Add.
|
|
const qs = new URLSearchParams({ url }).toString();
|
|
return this.request('GET', `/extension/probe?${qs}`);
|
|
}
|
|
|
|
// Connection test = the cheapest read with auth.
|
|
testConnection() {
|
|
return this.request('GET', '/credentials');
|
|
}
|
|
}
|
|
|
|
const api = new FabledCuratorAPI();
|