{ "id": "WkAdUd9jXTtPagGO", "name": "MAM Series Matcher", "nodes": [ { "parameters": {}, "id": "trigger-execute-workflow", "name": "When Called by Another Workflow", "type": "n8n-nodes-base.executeWorkflowTrigger", "typeVersion": 1, "position": [ 224, 304 ] }, { "parameters": { "jsCode": "// Fuzzy match incoming title against followed series\n// Input: title from Execute Workflow trigger\n// Output: matched series or null\n\nconst inputItem = $input.first();\nconst title = inputItem.json.title || '';\nconst metadata = inputItem.json.metadata || {};\n\n// Get all followed series from Supabase\nconst seriesList = $('Get Active Followed Series').all();\n\n// Helper function to normalize strings for comparison\nfunction normalize(str) {\n return str.toLowerCase()\n .replace(/[^a-z0-9\\s]/g, '')\n .replace(/\\s+/g, ' ')\n .trim();\n}\n\n// Helper function to extract patterns from title\nfunction extractPatterns(title) {\n const patterns = {\n volume: null,\n author: null,\n cleanTitle: title\n };\n \n // Extract volume numbers (e.g., \"Vol 01\", \"Volume 1\", \"v01\")\n const volumeMatch = title.match(/\\b(?:vol(?:ume)?\\.?\\s*(\\d+)|v(\\d+))\\b/i);\n if (volumeMatch) {\n patterns.volume = parseInt(volumeMatch[1] || volumeMatch[2]);\n }\n \n // Extract author (common patterns: \"Author Name - Title\", \"[Author Name]\")\n const authorMatch1 = title.match(/^([^-\\[\\]]+?)\\s*-\\s*(.+)/);\n const authorMatch2 = title.match(/\\[([^\\]]+)\\]/);\n \n if (authorMatch1) {\n patterns.author = authorMatch1[1].trim();\n patterns.cleanTitle = authorMatch1[2].trim();\n } else if (authorMatch2) {\n patterns.author = authorMatch2[1].trim();\n }\n \n return patterns;\n}\n\n// Extract patterns from input title\nconst inputPatterns = extractPatterns(title);\nconst normalizedInput = normalize(inputPatterns.cleanTitle);\n\n// Try to match against series\nlet bestMatch = null;\nlet bestScore = 0;\n\nfor (const series of seriesList) {\n const seriesName = series.json.series_name || '';\n const author = series.json.author || '';\n const normalizedSeries = normalize(seriesName);\n const normalizedAuthor = normalize(author);\n \n let score = 0;\n \n // Check if series name is contained in title\n if (normalizedInput.includes(normalizedSeries)) {\n score += 50;\n }\n \n // Check if title is contained in series name\n if (normalizedSeries.includes(normalizedInput)) {\n score += 40;\n }\n \n // Check author match\n if (inputPatterns.author && normalizedAuthor) {\n if (normalize(inputPatterns.author).includes(normalizedAuthor)) {\n score += 30;\n }\n }\n \n // Simple word overlap check\n const inputWords = normalizedInput.split(' ').filter(w => w.length > 2);\n const seriesWords = normalizedSeries.split(' ').filter(w => w.length > 2);\n const commonWords = inputWords.filter(w => seriesWords.includes(w));\n score += commonWords.length * 5;\n \n if (score > bestScore) {\n bestScore = score;\n bestMatch = series.json;\n }\n}\n\n// Return match if score is above threshold\nif (bestMatch && bestScore >= 40) {\n return [{\n json: {\n matched: true,\n series_id: bestMatch.id,\n series_name: bestMatch.series_name,\n author: bestMatch.author,\n category: bestMatch.category,\n smb_path: bestMatch.smb_path,\n match_score: bestScore,\n original_title: title,\n extracted_volume: inputPatterns.volume,\n extracted_author: inputPatterns.author\n }\n }];\n} else {\n return [{\n json: {\n matched: false,\n series_id: null,\n series_name: null,\n author: null,\n category: null,\n smb_path: null,\n match_score: bestScore,\n original_title: title\n }\n }];\n}" }, "id": "code-fuzzy-match", "name": "Fuzzy Match Title", "type": "n8n-nodes-base.code", "typeVersion": 2, "position": [ 608, 304 ] }, { "parameters": { "respondWith": "allIncomingItems", "options": {} }, "id": "respond-to-workflow", "name": "Respond to Workflow", "type": "n8n-nodes-base.respondToWebhook", "typeVersion": 1, "position": [ 800, 304 ] }, { "parameters": { "operation": "select", "schema": { "__rl": true, "mode": "list", "value": "public" }, "table": { "__rl": true, "value": "followed_series", "mode": "list", "cachedResultName": "followed_series" }, "returnAll": true, "where": { "values": [ { "column": "active", "value": "true" } ] }, "options": {} }, "type": "n8n-nodes-base.postgres", "typeVersion": 2.6, "position": [ 432, 304 ], "id": "2f0708e1-996c-4705-b374-2a119fcd6b18", "name": "Get Active Followed Series", "credentials": { "postgres": { "id": "dsnKfvOBMkgU21Lt", "name": "supabase postgres account" } } } ], "connections": { "When Called by Another Workflow": { "main": [ [ { "node": "Get Active Followed Series", "type": "main", "index": 0 } ] ] }, "Fuzzy Match Title": { "main": [ [ { "node": "Respond to Workflow", "type": "main", "index": 0 } ] ] }, "Get Active Followed Series": { "main": [ [ { "node": "Fuzzy Match Title", "type": "main", "index": 0 } ] ] } }, "settings": { "executionOrder": "v1", "saveDataErrorExecution": "all", "saveDataSuccessExecution": "all", "saveManualExecutions": true, "saveExecutionProgress": true }, "triggerCount": 0, "versionId": "34e2aea3-0a6e-41fd-b277-207bfd0f6804", "owner": { "type": "personal", "projectId": "FeLO36wNUAcn61Wj", "projectName": "Ben W ", "personalEmail": "admin@ben.io" }, "parentFolderId": "6tDyZCwqELStb6Ik", "isArchived": false }