Continue new scrapes where the last complete one started
This commit is contained in:
@@ -657,15 +657,27 @@ export class ScrapeStore {
|
|||||||
.run(preview.hash, preview.bytes, preview.ext ?? "webp", id);
|
.run(preview.hash, preview.bytes, preview.ext ?? "webp", id);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Message span of the walk, for completeness checks. */
|
/** Message span of the walk, for completeness checks and incremental scrapes. */
|
||||||
dbSummary(): { messages: number; oldest: string | null; newest: string | null } {
|
dbSummary(): {
|
||||||
|
messages: number;
|
||||||
|
oldest: string | null;
|
||||||
|
newest: string | null;
|
||||||
|
newestId: string | null;
|
||||||
|
} {
|
||||||
const row = this.db
|
const row = this.db
|
||||||
.prepare(
|
.prepare(
|
||||||
"SELECT COUNT(*) AS messages, MIN(timestamp) AS oldest, MAX(timestamp) AS newest FROM messages"
|
"SELECT COUNT(*) AS messages, MIN(timestamp) AS oldest, MAX(timestamp) AS newest, MAX(id) AS newestId FROM messages"
|
||||||
)
|
)
|
||||||
.get() as { messages: number; oldest: string | null; newest: string | null } | undefined;
|
.get() as
|
||||||
|
| { messages: number; oldest: string | null; newest: string | null; newestId: string | null }
|
||||||
|
| undefined;
|
||||||
|
|
||||||
return { messages: row?.messages ?? 0, oldest: row?.oldest ?? null, newest: row?.newest ?? null };
|
return {
|
||||||
|
messages: row?.messages ?? 0,
|
||||||
|
oldest: row?.oldest ?? null,
|
||||||
|
newest: row?.newest ?? null,
|
||||||
|
newestId: row?.newestId ?? null
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Preview state for the whole scrape and per kind, for the backfill UI. */
|
/** Preview state for the whole scrape and per kind, for the backfill UI. */
|
||||||
|
|||||||
@@ -1437,6 +1437,23 @@ void (async () => {
|
|||||||
reopened.close();
|
reopened.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
await test("dbSummary reports the message span and newest id", () => {
|
||||||
|
const dir = join(root, "summary");
|
||||||
|
mkdirSync(dir, { recursive: true });
|
||||||
|
const store = new ScrapeStore(dir);
|
||||||
|
store.insertPage([
|
||||||
|
message("1800", { timestamp: "2026-09-16T00:00:00.000Z" }),
|
||||||
|
message("1801", { timestamp: "2026-09-17T00:00:00.000Z" })
|
||||||
|
]);
|
||||||
|
|
||||||
|
const summary = store.dbSummary();
|
||||||
|
assert.equal(summary.messages, 2);
|
||||||
|
assert.equal(summary.oldest, "2026-09-16T00:00:00.000Z");
|
||||||
|
assert.equal(summary.newest, "2026-09-17T00:00:00.000Z");
|
||||||
|
assert.equal(summary.newestId, "1801");
|
||||||
|
store.close();
|
||||||
|
});
|
||||||
|
|
||||||
await test("message links build for guilds and DMs", () => {
|
await test("message links build for guilds and DMs", () => {
|
||||||
assert.equal(messageUrlFor("123", "456", "789"), "https://discord.com/channels/123/456/789");
|
assert.equal(messageUrlFor("123", "456", "789"), "https://discord.com/channels/123/456/789");
|
||||||
assert.equal(messageUrlFor(null, "456", "789"), "https://discord.com/channels/@me/456/789");
|
assert.equal(messageUrlFor(null, "456", "789"), "https://discord.com/channels/@me/456/789");
|
||||||
|
|||||||
+43
-3
@@ -632,8 +632,32 @@ export class MediaService {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const mode = input.mode ?? "links";
|
const mode = input.mode ?? "links";
|
||||||
const afterId = input.after ? snowflakeFromIso(input.after) : null;
|
let afterId = input.after ? snowflakeFromIso(input.after) : null;
|
||||||
const beforeId = input.before ? snowflakeFromIso(input.before) : null;
|
let beforeId = input.before ? snowflakeFromIso(input.before) : null;
|
||||||
|
let fromDate = input.after ?? null;
|
||||||
|
|
||||||
|
// Continuing a channel: stop where the best completed scrape of it starts, so a new
|
||||||
|
// walk only covers what is newer instead of re-fetching the whole history.
|
||||||
|
if (!afterId && !beforeId) {
|
||||||
|
const previous = this.previousCompleteScrape(input.channelId);
|
||||||
|
if (previous) {
|
||||||
|
const store = new ScrapeStore(scrapeDir(this.rootDir, previous.id));
|
||||||
|
try {
|
||||||
|
const summary = store.dbSummary();
|
||||||
|
if (summary.newestId) {
|
||||||
|
afterId = summary.newestId;
|
||||||
|
fromDate = summary.newest;
|
||||||
|
this.deps.log?.(
|
||||||
|
`continuing ${input.channelId} after ${summary.newestId} ` +
|
||||||
|
`(from scrape ${previous.id})`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
store.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let backend: ScrapeBackend;
|
let backend: ScrapeBackend;
|
||||||
|
|
||||||
if (input.backend === "puppet") {
|
if (input.backend === "puppet") {
|
||||||
@@ -672,7 +696,7 @@ export class MediaService {
|
|||||||
downloadDir: input.downloadDir ?? null,
|
downloadDir: input.downloadDir ?? null,
|
||||||
afterId,
|
afterId,
|
||||||
beforeId,
|
beforeId,
|
||||||
fromDate: input.after ?? null,
|
fromDate,
|
||||||
toDate: input.before ?? null,
|
toDate: input.before ?? null,
|
||||||
estimatedMessages
|
estimatedMessages
|
||||||
});
|
});
|
||||||
@@ -747,6 +771,22 @@ export class MediaService {
|
|||||||
return result?.messages?.find(candidate => candidate?.id === messageId) ?? result?.messages?.[0] ?? null;
|
return result?.messages?.find(candidate => candidate?.id === messageId) ?? result?.messages?.[0] ?? null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The completed scrape of a channel that walked the most, if any. */
|
||||||
|
private previousCompleteScrape(channelId: string): ScrapeMeta | null {
|
||||||
|
const candidates = this.engine
|
||||||
|
.list()
|
||||||
|
.filter(
|
||||||
|
meta =>
|
||||||
|
meta.channelId === channelId &&
|
||||||
|
meta.state === "done" &&
|
||||||
|
meta.messages > 0 &&
|
||||||
|
meta.mode === "links"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!candidates.length) return null;
|
||||||
|
return candidates.sort((a, b) => b.messages - a.messages)[0] ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
/** Continue a paused scrape from its cursor with the same backend family. */
|
/** Continue a paused scrape from its cursor with the same backend family. */
|
||||||
async resumeScrape(scrapeId: string): Promise<ScrapeMeta | null> {
|
async resumeScrape(scrapeId: string): Promise<ScrapeMeta | null> {
|
||||||
const meta = this.engine.get(scrapeId);
|
const meta = this.engine.get(scrapeId);
|
||||||
|
|||||||
Reference in New Issue
Block a user