organ-ai-zer
Version:
AI-powered file organizer CLI tool
574 lines • 20.5 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.PatternMatchingService = void 0;
const pattern_cache_1 = require("./pattern-cache");
/**
* Pattern Matching Service
* Optional utility to supplement AI analysis with pattern recognition
* Focuses on common patterns that can help guide AI understanding
*/
class PatternMatchingService {
constructor() {
this.patterns = [
new SeriesPatternMatcher(),
new VersionPatternMatcher(),
new DatePatternMatcher(),
new ProjectPatternMatcher(),
new MediaPatternMatcher(),
new DocumentSeriesPatternMatcher(),
new ArchivePatternMatcher(),
new TimestampPatternMatcher()
];
this.cache = pattern_cache_1.PatternCache.getInstance();
}
/**
* Analyze files for patterns and return insights for AI
*/
async analyzePatterns(files, directory) {
// Check cache first if directory is provided
if (directory) {
const cachedAnalysis = await this.cache.getCachedAnalysis(directory, files);
if (cachedAnalysis) {
return cachedAnalysis;
}
}
// Perform pattern analysis
const analysis = this.performPatternAnalysis(files);
// Cache results if directory is provided
if (directory) {
await this.cache.cacheAnalysis(directory, files, analysis);
}
return analysis;
}
/**
* Perform the actual pattern analysis (non-cached)
*/
performPatternAnalysis(files) {
const fileMatches = new Map();
const allMatches = [];
// Run pattern matching on all files
files.forEach(file => {
const matches = [];
this.patterns.forEach(pattern => {
const match = pattern.match(file);
if (match) {
matches.push(match);
allMatches.push(match);
}
});
if (matches.length > 0) {
fileMatches.set(file.name, matches);
}
});
// Group related files based on patterns
const groups = this.findPatternGroups(files, fileMatches);
// Generate insights for AI
const hints = this.generateHints(fileMatches, groups);
const structureSuggestions = this.generateStructureSuggestions(groups);
return {
fileMatches,
groups,
hints,
structureSuggestions
};
}
/**
* Get pattern hints that can be provided to AI for better analysis
*/
async getPatternHints(files, directory) {
const analysis = await this.analyzePatterns(files, directory);
return analysis.hints;
}
/**
* Check if files appear to be part of a series or collection
*/
async findSeries(files, directory) {
const analysis = await this.analyzePatterns(files, directory);
return analysis.groups.filter(group => group.type === 'series' ||
group.type === 'episode_series' ||
group.type === 'version_series');
}
/**
* Check if files appear to be project-related
*/
async findProjects(files, directory) {
const analysis = await this.analyzePatterns(files, directory);
return analysis.groups.filter(group => group.type === 'project');
}
/**
* Find pattern groups from individual matches
*/
findPatternGroups(files, fileMatches) {
const groups = [];
const processedFiles = new Set();
// Group by series patterns
groups.push(...this.groupBySeries(files, fileMatches, processedFiles));
// Group by project patterns
groups.push(...this.groupByProject(files, fileMatches, processedFiles));
// Group by date patterns
groups.push(...this.groupByDate(files, fileMatches, processedFiles));
// Group by common prefixes/suffixes
groups.push(...this.groupByNameSimilarity(files, processedFiles));
return groups;
}
/**
* Group files by series patterns
*/
groupBySeries(files, fileMatches, processedFiles) {
const groups = [];
const seriesMap = new Map();
files.forEach(file => {
if (processedFiles.has(file.name))
return;
const matches = fileMatches.get(file.name) || [];
const seriesMatch = matches.find(m => m.type === 'episode_series' || m.type === 'season_series');
if (seriesMatch && seriesMatch.metadata.seriesName) {
const seriesName = seriesMatch.metadata.seriesName;
if (!seriesMap.has(seriesName)) {
seriesMap.set(seriesName, []);
}
seriesMap.get(seriesName).push(file);
processedFiles.add(file.name);
}
});
seriesMap.forEach((seriesFiles, seriesName) => {
if (seriesFiles.length > 1) {
groups.push({
groupId: `series_${seriesName.toLowerCase().replace(/\s+/g, '_')}`,
type: 'series',
files: seriesFiles,
pattern: `TV/Movie series: ${seriesName}`,
confidence: 0.9,
metadata: { seriesName, fileCount: seriesFiles.length }
});
}
});
return groups;
}
/**
* Group files by project patterns
*/
groupByProject(files, fileMatches, processedFiles) {
const groups = [];
const projectMap = new Map();
files.forEach(file => {
if (processedFiles.has(file.name))
return;
const matches = fileMatches.get(file.name) || [];
const projectMatch = matches.find(m => m.type === 'project');
if (projectMatch && projectMatch.metadata.projectName) {
const projectName = projectMatch.metadata.projectName;
if (!projectMap.has(projectName)) {
projectMap.set(projectName, []);
}
projectMap.get(projectName).push(file);
processedFiles.add(file.name);
}
});
projectMap.forEach((projectFiles, projectName) => {
if (projectFiles.length > 1) {
groups.push({
groupId: `project_${projectName.toLowerCase().replace(/\s+/g, '_')}`,
type: 'project',
files: projectFiles,
pattern: `Code/Document project: ${projectName}`,
confidence: 0.8,
metadata: { projectName, fileCount: projectFiles.length }
});
}
});
return groups;
}
/**
* Group files by date patterns
*/
groupByDate(files, fileMatches, processedFiles) {
const groups = [];
const dateMap = new Map();
files.forEach(file => {
if (processedFiles.has(file.name))
return;
const matches = fileMatches.get(file.name) || [];
const dateMatch = matches.find(m => m.type === 'date');
if (dateMatch && dateMatch.metadata.dateGroup) {
const dateGroup = dateMatch.metadata.dateGroup;
if (!dateMap.has(dateGroup)) {
dateMap.set(dateGroup, []);
}
dateMap.get(dateGroup).push(file);
}
});
dateMap.forEach((dateFiles, dateGroup) => {
if (dateFiles.length > 2) {
groups.push({
groupId: `date_${dateGroup}`,
type: 'date_group',
files: dateFiles,
pattern: `Files from ${dateGroup}`,
confidence: 0.7,
metadata: { dateGroup, fileCount: dateFiles.length }
});
}
});
return groups;
}
/**
* Group files by name similarity
*/
groupByNameSimilarity(files, processedFiles) {
const groups = [];
const remainingFiles = files.filter(f => !processedFiles.has(f.name));
// Simple prefix grouping
const prefixMap = new Map();
remainingFiles.forEach(file => {
const prefix = this.extractCommonPrefix(file.name);
if (prefix.length > 3) {
if (!prefixMap.has(prefix)) {
prefixMap.set(prefix, []);
}
prefixMap.get(prefix).push(file);
}
});
prefixMap.forEach((prefixFiles, prefix) => {
if (prefixFiles.length > 2) {
groups.push({
groupId: `prefix_${prefix.toLowerCase().replace(/\W+/g, '_')}`,
type: 'name_similarity',
files: prefixFiles,
pattern: `Files with common prefix: ${prefix}`,
confidence: 0.6,
metadata: { prefix, fileCount: prefixFiles.length }
});
}
});
return groups;
}
/**
* Extract common prefix from filename
*/
extractCommonPrefix(filename) {
// Remove extension and extract potential prefix
const nameWithoutExt = filename.replace(/\.[^.]+$/, '');
// Look for common separators
const separators = [' - ', '_', ' ', '.'];
for (const sep of separators) {
const parts = nameWithoutExt.split(sep);
if (parts.length > 1 && parts[0].length > 3) {
return parts[0];
}
}
return nameWithoutExt.substring(0, Math.min(10, nameWithoutExt.length));
}
/**
* Generate hints for AI based on pattern analysis
*/
generateHints(fileMatches, groups) {
const hints = [];
// Series hints
const seriesGroups = groups.filter(g => g.type === 'series');
if (seriesGroups.length > 0) {
hints.push(`Detected ${seriesGroups.length} TV/movie series with episode patterns`);
seriesGroups.forEach(group => {
hints.push(`Series "${group.metadata.seriesName}" has ${group.metadata.fileCount} files`);
});
}
// Project hints
const projectGroups = groups.filter(g => g.type === 'project');
if (projectGroups.length > 0) {
hints.push(`Detected ${projectGroups.length} potential code/document projects`);
projectGroups.forEach(group => {
hints.push(`Project "${group.metadata.projectName}" has ${group.metadata.fileCount} files`);
});
}
// Date grouping hints
const dateGroups = groups.filter(g => g.type === 'date_group');
if (dateGroups.length > 0) {
hints.push(`Found files that could be organized by date/time periods`);
}
// Pattern variety hints
const patternTypes = new Set();
fileMatches.forEach(matches => {
matches.forEach(match => patternTypes.add(match.type));
});
if (patternTypes.has('version')) {
hints.push('Some files appear to be different versions of the same content');
}
if (patternTypes.has('archive')) {
hints.push('Archive files detected - consider if they should be extracted or organized separately');
}
return hints;
}
/**
* Generate structure suggestions based on groups
*/
generateStructureSuggestions(groups) {
const suggestions = [];
groups.forEach(group => {
switch (group.type) {
case 'series':
suggestions.push(`Consider organizing "${group.metadata.seriesName}" files in TV Shows/${group.metadata.seriesName}/`);
break;
case 'project':
suggestions.push(`Keep project "${group.metadata.projectName}" files together in Projects/${group.metadata.projectName}/`);
break;
case 'date_group':
suggestions.push(`Files from ${group.metadata.dateGroup} could be organized by date`);
break;
}
});
return suggestions;
}
}
exports.PatternMatchingService = PatternMatchingService;
/**
* Base pattern matcher interface
*/
class PatternMatcher {
}
/**
* Series pattern matcher (TV shows, movies, etc.)
*/
class SeriesPatternMatcher extends PatternMatcher {
match(file) {
const name = file.name.toLowerCase();
// TV series patterns
const episodePatterns = [
/(.+?)[-.\s]+s(\d+)e(\d+)/i,
/(.+?)[-.\s]+season[-.\s]*(\d+)[-.\s]*episode[-.\s]*(\d+)/i,
/(.+?)[-.\s]+(\d+)x(\d+)/i
];
for (const pattern of episodePatterns) {
const match = name.match(pattern);
if (match) {
return {
type: 'episode_series',
confidence: 0.9,
metadata: {
seriesName: this.cleanSeriesName(match[1]),
season: parseInt(match[2]),
episode: parseInt(match[3])
},
description: `TV series episode: ${match[1]} S${match[2]}E${match[3]}`
};
}
}
return null;
}
cleanSeriesName(name) {
return name.replace(/[-._]/g, ' ').replace(/\s+/g, ' ').trim();
}
}
/**
* Version pattern matcher
*/
class VersionPatternMatcher extends PatternMatcher {
match(file) {
const name = file.name.toLowerCase();
const versionPatterns = [
/(.+?)[-.\s]*v(\d+)\.(\d+)/i,
/(.+?)[-.\s]*version[-.\s]*(\d+)/i,
/(.+?)[-.\s]*\((\d+)\)/i
];
for (const pattern of versionPatterns) {
const match = name.match(pattern);
if (match) {
return {
type: 'version',
confidence: 0.8,
metadata: {
baseName: match[1].trim(),
version: match[2]
},
description: `Versioned file: ${match[1]} v${match[2]}`
};
}
}
return null;
}
}
/**
* Date pattern matcher
*/
class DatePatternMatcher extends PatternMatcher {
match(file) {
const name = file.name;
const datePatterns = [
/(\d{4})[-._](\d{2})[-._](\d{2})/,
/(\d{2})[-._](\d{2})[-._](\d{4})/,
/(\d{4})(\d{2})(\d{2})/
];
for (const pattern of datePatterns) {
const match = name.match(pattern);
if (match) {
const year = match[1].length === 4 ? match[1] : match[3];
const month = match[1].length === 4 ? match[2] : match[1];
const day = match[1].length === 4 ? match[3] : match[2];
return {
type: 'date',
confidence: 0.7,
metadata: {
year,
month,
day,
dateGroup: `${year}-${month}`
},
description: `Date-stamped file: ${year}-${month}-${day}`
};
}
}
return null;
}
}
/**
* Project pattern matcher
*/
class ProjectPatternMatcher extends PatternMatcher {
match(file) {
const name = file.name.toLowerCase();
// Project indicator files
const projectFiles = [
'package.json',
'requirements.txt',
'cargo.toml',
'pom.xml',
'makefile',
'dockerfile',
'readme.md',
'readme.txt',
'.gitignore'
];
if (projectFiles.some(pf => name.includes(pf))) {
return {
type: 'project',
confidence: 0.9,
metadata: {
projectName: this.extractProjectName(file.name),
isProjectFile: true
},
description: 'Project configuration/documentation file'
};
}
return null;
}
extractProjectName(filename) {
// Try to extract project name from directory or filename
const parts = filename.split(/[/\\]/);
return parts[parts.length - 2] || 'Unknown Project';
}
}
/**
* Media pattern matcher
*/
class MediaPatternMatcher extends PatternMatcher {
match(file) {
const ext = file.extension.toLowerCase();
const name = file.name.toLowerCase();
// Video files
if (ext.match(/\.(mp4|mkv|avi|mov|wmv|flv|webm)$/)) {
const yearMatch = name.match(/\((\d{4})\)|(\d{4})/);
const qualityMatch = name.match(/(1080p|720p|4k|hdr|bluray|dvd)/i);
return {
type: 'video',
confidence: 0.8,
metadata: {
year: yearMatch ? yearMatch[1] || yearMatch[2] : null,
quality: qualityMatch ? qualityMatch[1] : null,
isVideo: true
},
description: `Video file${yearMatch ? ` from ${yearMatch[1] || yearMatch[2]}` : ''}`
};
}
// Audio files
if (ext.match(/\.(mp3|wav|flac|m4a|aac|ogg)$/)) {
return {
type: 'audio',
confidence: 0.8,
metadata: {
isAudio: true
},
description: 'Audio file'
};
}
return null;
}
}
/**
* Document series pattern matcher
*/
class DocumentSeriesPatternMatcher extends PatternMatcher {
match(file) {
const name = file.name.toLowerCase();
const ext = file.extension.toLowerCase();
if (!ext.match(/\.(pdf|doc|docx|txt|md)$/))
return null;
const seriesPatterns = [
/(.+?)[-.\s]*part[-.\s]*(\d+)/i,
/(.+?)[-.\s]*chapter[-.\s]*(\d+)/i,
/(.+?)[-.\s]*vol[-.\s]*(\d+)/i,
/(.+?)[-.\s]*(\d+)[-.\s]*of[-.\s]*(\d+)/i
];
for (const pattern of seriesPatterns) {
const match = name.match(pattern);
if (match) {
return {
type: 'document_series',
confidence: 0.7,
metadata: {
seriesName: match[1].trim(),
part: match[2]
},
description: `Document series: ${match[1]} part ${match[2]}`
};
}
}
return null;
}
}
/**
* Archive pattern matcher
*/
class ArchivePatternMatcher extends PatternMatcher {
match(file) {
const ext = file.extension.toLowerCase();
if (ext.match(/\.(zip|rar|7z|tar|gz|bz2)$/)) {
return {
type: 'archive',
confidence: 0.9,
metadata: {
isArchive: true,
archiveType: ext
},
description: `Archive file (.${ext})`
};
}
return null;
}
}
/**
* Timestamp pattern matcher
*/
class TimestampPatternMatcher extends PatternMatcher {
match(file) {
const name = file.name;
// Unix timestamp pattern
const timestampMatch = name.match(/(\d{10,13})/);
if (timestampMatch) {
const timestamp = parseInt(timestampMatch[1]);
const date = new Date(timestamp.toString().length === 10 ? timestamp * 1000 : timestamp);
if (date.getFullYear() > 1990 && date.getFullYear() < 2030) {
return {
type: 'timestamp',
confidence: 0.6,
metadata: {
timestamp,
date: date.toISOString(),
dateGroup: `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`
},
description: `Timestamp-named file from ${date.toLocaleDateString()}`
};
}
}
return null;
}
}
//# sourceMappingURL=pattern-matching-service.js.map