mirror of
https://github.com/xCyanGrizzly/DragonsStash.git
synced 2026-09-21 13:31:42 +00:00
feat: pattern/creator grouping, notification UI, failure alerts
Pattern grouping (Signal 3): - Extract YYYY-MM dates, month names, and project prefixes from filenames - Auto-group packages sharing the same pattern within a channel - Groups created with groupingSource=AUTO_PATTERN Creator grouping (Signal 4): - Auto-group 3+ ungrouped packages from the same creator within a channel - Runs after pattern grouping as lowest-priority automatic signal Notification UI: - Add NotificationBell component to header with unread badge - Popover panel shows recent notifications with severity icons - Mark individual or all notifications as read - Polls every 30 seconds for updates Failure notifications: - Upload/download failures now create SystemNotification records - Visible in the notification bell alongside hash mismatch alerts Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+139
-1
@@ -1,7 +1,7 @@
|
||||
import type { Client } from "tdl";
|
||||
import type { TelegramPhoto } from "./preview/match.js";
|
||||
import { downloadPhotoThumbnail } from "./tdlib/download.js";
|
||||
import { createOrFindPackageGroup, linkPackagesToGroup, createTimeWindowGroup } from "./db/queries.js";
|
||||
import { createOrFindPackageGroup, linkPackagesToGroup, createTimeWindowGroup, createAutoGroup } from "./db/queries.js";
|
||||
import { config } from "./util/config.js";
|
||||
import { childLogger } from "./util/logger.js";
|
||||
import { db } from "./db/client.js";
|
||||
@@ -150,6 +150,144 @@ export async function processTimeWindowGroups(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Group ungrouped packages that share a date pattern (YYYY-MM, YYYY_MM, etc.)
|
||||
* or project slug extracted from their filenames.
|
||||
*/
|
||||
export async function processPatternGroups(
|
||||
sourceChannelId: string,
|
||||
indexedPackages: IndexedPackageRef[]
|
||||
): Promise<void> {
|
||||
const ungrouped = await db.package.findMany({
|
||||
where: {
|
||||
id: { in: indexedPackages.map((p) => p.packageId) },
|
||||
packageGroupId: null,
|
||||
},
|
||||
select: { id: true, fileName: true },
|
||||
});
|
||||
|
||||
if (ungrouped.length < 2) return;
|
||||
|
||||
// Group by extracted pattern
|
||||
const patternMap = new Map<string, typeof ungrouped>();
|
||||
for (const pkg of ungrouped) {
|
||||
const pattern = extractPattern(pkg.fileName);
|
||||
if (!pattern) continue;
|
||||
const group = patternMap.get(pattern) ?? [];
|
||||
group.push(pkg);
|
||||
patternMap.set(pattern, group);
|
||||
}
|
||||
|
||||
for (const [pattern, members] of patternMap) {
|
||||
if (members.length < 2) continue;
|
||||
|
||||
try {
|
||||
const groupId = await createAutoGroup({
|
||||
sourceChannelId,
|
||||
name: pattern,
|
||||
packageIds: members.map((m) => m.id),
|
||||
groupingSource: "AUTO_PATTERN",
|
||||
});
|
||||
|
||||
log.info(
|
||||
{ groupId, pattern, memberCount: members.length },
|
||||
"Created pattern-based group"
|
||||
);
|
||||
} catch (err) {
|
||||
log.warn({ err, pattern }, "Failed to create pattern group");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract a grouping pattern from a filename.
|
||||
* Matches: YYYY-MM, YYYY_MM, "Month Year", or a project prefix before common separators.
|
||||
* Returns null if no usable pattern found.
|
||||
*/
|
||||
function extractPattern(fileName: string): string | null {
|
||||
// Strip extension for matching
|
||||
const name = fileName.replace(/\.(zip|rar|7z|pdf|stl)(\.\d+)?$/i, "");
|
||||
|
||||
// Match YYYY-MM or YYYY_MM patterns
|
||||
const dateMatch = name.match(/(\d{4})[\-_](\d{2})/);
|
||||
if (dateMatch) {
|
||||
return `${dateMatch[1]}-${dateMatch[2]}`;
|
||||
}
|
||||
|
||||
// Match "Month Year" patterns (e.g., "January 2025", "Jan 2025")
|
||||
const months = "(?:jan(?:uary)?|feb(?:ruary)?|mar(?:ch)?|apr(?:il)?|may|jun(?:e)?|jul(?:y)?|aug(?:ust)?|sep(?:tember)?|oct(?:ober)?|nov(?:ember)?|dec(?:ember)?)";
|
||||
const monthYearMatch = name.match(new RegExp(`(${months})\\s*(\\d{4})`, "i"));
|
||||
if (monthYearMatch) {
|
||||
const monthStr = monthYearMatch[1].toLowerCase().slice(0, 3);
|
||||
const monthNum = ["jan","feb","mar","apr","may","jun","jul","aug","sep","oct","nov","dec"].indexOf(monthStr) + 1;
|
||||
if (monthNum > 0) {
|
||||
return `${monthYearMatch[2]}-${String(monthNum).padStart(2, "0")}`;
|
||||
}
|
||||
}
|
||||
|
||||
// Match project prefix: text before " - ", " – ", or "(". Must be at least 5 chars.
|
||||
const prefixMatch = name.match(/^(.{5,}?)(?:\s*[\-–]\s|\s*\()/);
|
||||
if (prefixMatch) {
|
||||
return prefixMatch[1].trim();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Group ungrouped packages that share the same creator within a channel.
|
||||
* Only groups if there are 3+ packages from the same creator (to avoid
|
||||
* over-grouping when a creator only has a couple files).
|
||||
*/
|
||||
export async function processCreatorGroups(
|
||||
sourceChannelId: string,
|
||||
indexedPackages: IndexedPackageRef[]
|
||||
): Promise<void> {
|
||||
const ungrouped = await db.package.findMany({
|
||||
where: {
|
||||
id: { in: indexedPackages.map((p) => p.packageId) },
|
||||
packageGroupId: null,
|
||||
creator: { not: null },
|
||||
},
|
||||
select: { id: true, fileName: true, creator: true },
|
||||
});
|
||||
|
||||
if (ungrouped.length < 3) return;
|
||||
|
||||
// Group by creator
|
||||
const creatorMap = new Map<string, typeof ungrouped>();
|
||||
for (const pkg of ungrouped) {
|
||||
if (!pkg.creator) continue;
|
||||
const key = pkg.creator.toLowerCase();
|
||||
const group = creatorMap.get(key) ?? [];
|
||||
group.push(pkg);
|
||||
creatorMap.set(key, group);
|
||||
}
|
||||
|
||||
for (const [, members] of creatorMap) {
|
||||
if (members.length < 3) continue;
|
||||
|
||||
const creatorName = members[0].creator!;
|
||||
const name = findCommonPrefix(members.map((m) => m.fileName)) || creatorName;
|
||||
|
||||
try {
|
||||
const groupId = await createAutoGroup({
|
||||
sourceChannelId,
|
||||
name,
|
||||
packageIds: members.map((m) => m.id),
|
||||
groupingSource: "AUTO_PATTERN",
|
||||
});
|
||||
|
||||
log.info(
|
||||
{ groupId, creator: creatorName, memberCount: members.length },
|
||||
"Created creator-based group"
|
||||
);
|
||||
} catch (err) {
|
||||
log.warn({ err, creator: creatorName }, "Failed to create creator group");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the longest common prefix among a list of filenames,
|
||||
* trimming trailing separators and partial words.
|
||||
|
||||
Reference in New Issue
Block a user