feat: add channel categories and improved creator detection

- Add category field to TelegramChannel (filterable tag like STL, PDF, D&D)
- Category column in channels table with edit via dropdown menu
- Improved creator extraction: filename patterns + channel title fallback
- extractCreatorFromChannelTitle strips [Completed], (Paid), emoji, etc.
- Fix ArchiveType in PackageListItem and PackageRow for new types
- Add Prisma migration for category column
This commit is contained in:
admin
2026-03-21 20:37:44 +01:00
parent 53a76a8136
commit 36a7e3d5f4
10 changed files with 126 additions and 14 deletions

View File

@@ -1,21 +1,71 @@
/**
* Extract a creator name from common archive file naming patterns.
*
* Priority in the worker: topic name > filename extraction.
* This is the fallback when no forum topic name is available.
* Priority in the worker: topic name > filename extraction > channel title > null.
*
* Patterns handled (split on ` - `):
* Patterns handled:
* "Mammoth Factory - 2026-01.zip" → "Mammoth Factory"
* "Artist Name - Pack Title.part01.rar" → "Artist Name"
* "ArtistName_PackTitle.zip" → null (ambiguous)
* "some_random_file.zip" → null
*/
export function extractCreatorFromFileName(fileName: string): string | null {
// Strip archive extensions (.zip, .rar, .part01.rar, .z01, etc.)
const bare = fileName.replace(/(\.(part\d+\.rar|z\d{2}|zip|rar))+$/i, "");
// Strip archive/document extensions
const bare = fileName.replace(
/(\.(part\d+\.rar|z\d{2}|zip|rar|7z|pdf|stl|obj|3mf|step|stp|blend|gcode|svg|dxf|ai|eps|psd))+$/i,
""
);
const idx = bare.indexOf(" - ");
if (idx <= 0) return null;
// Pattern 1: "Creator - Title" (most common)
const dashIdx = bare.indexOf(" - ");
if (dashIdx > 0) {
const creator = bare.slice(0, dashIdx).trim();
if (creator.length > 1) return creator;
}
const creator = bare.slice(0, idx).trim();
return creator.length > 0 ? creator : null;
// Pattern 2: "Creator_Title" with underscores where first segment looks like a name
// Only match if the first segment has a space or capital letter pattern suggesting a name
const underscoreIdx = bare.indexOf("_");
if (underscoreIdx > 2) {
const candidate = bare.slice(0, underscoreIdx).trim();
// Accept if it contains a space (multi-word) or starts with upper + has lower (proper name)
if (candidate.includes(" ") || /^[A-Z][a-z]/.test(candidate)) {
return candidate;
}
}
return null;
}
/**
* Extract a creator name from a Telegram channel title.
* Strips common suffixes like "[Completed]", "(Paid)", dates, etc.
*/
export function extractCreatorFromChannelTitle(title: string): string | null {
let clean = title
// Remove bracketed suffixes: [Completed], [Open], [Closed], etc.
.replace(/\s*\[.*?\]\s*/g, " ")
// Remove parenthesized suffixes: (Paid), (partial upload...), etc.
.replace(/\s*\(.*?\)\s*/g, " ")
// Remove common emoji
.replace(/[\u{1F300}-\u{1FAFF}\u{2600}-\u{27BF}]/gu, "")
.trim();
// If there's a " - " separator, take the first part as creator
const dashIdx = clean.indexOf(" - ");
if (dashIdx > 0) {
clean = clean.slice(0, dashIdx).trim();
}
// Too generic or too short
if (clean.length < 2) return null;
// Skip overly generic channel names
const generic = [
"3d printing", "stl", "free stl", "stl zone", "stl forest", "stl all",
"marvel stl", "dc stl", "star wars stl", "pokemon stl",
];
if (generic.includes(clean.toLowerCase())) return null;
return clean;
}