Files
n8n-backup-v2/workflows/xXUnt2hL2FKxzOhBnkd3Z.json
2026-02-05 21:59:40 +00:00

739 lines
24 KiB
JSON

{
"id": "xXUnt2hL2FKxzOhBnkd3Z",
"name": "MAM Series Checker",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{}
]
}
},
"typeVersion": 1.3,
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-832,
496
],
"id": "279ca2c5-3c3e-4cd6-bc97-e43010fb693c"
},
{
"parameters": {
"options": {}
},
"typeVersion": 3,
"id": "2412dfa0-8362-4cc4-8a18-3f233d5338b2",
"type": "n8n-nodes-base.splitInBatches",
"name": "Loop Over Items",
"position": [
-384,
496
]
},
{
"parameters": {
"method": "POST",
"url": "https://www.myanonamouse.net/tor/js/loadSearchJSONbasic.php",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "= {\n \"dlLink\": \"1\",\n \"tor\": {\n \"text\": \"{{ $json.series_name }}\",\n \"srchIn\": [\"series\"],\n \"main_cat\": [\"13\"],\n \"searchType\": \"all\",\n \"sortType\": \"seedersDesc\"\n }\n }\n\n",
"options": {}
},
"typeVersion": 4.3,
"name": "HTTP Request",
"position": [
-160,
416
],
"type": "n8n-nodes-base.httpRequest",
"id": "f387a918-881e-492c-b41a-1dc621e8e822",
"credentials": {
"httpHeaderAuth": {
"id": "G8eA8XeS9P5axwJd",
"name": "mam cookie auth header"
}
}
},
{
"parameters": {
"fieldsToAggregate": {
"fieldToAggregate": [
{
"fieldToAggregate": "book_name"
}
]
},
"options": {}
},
"id": "019177aa-2c4f-4291-90f7-c58750033e77",
"type": "n8n-nodes-base.aggregate",
"typeVersion": 1,
"name": "Aggregate",
"position": [
736,
272
]
},
{
"parameters": {
"promptType": "define",
"text": "=You are a Librarian checking for missing audiobooks.\n\n Series: {{ $('Loop Over Items').item.json.series_name }}\n\n Our library contains:\n {{ $json.book_name }}\n\n Available from provider (ID | Title):\n {{ $('Format for LLM').item.json.llm_available }}\n\n IMPORTANT MATCHING RULES:\n - Match by book NUMBER, not exact title. \"Book 7\", \"Vol. 7\", \"Volume 7\", and just \"7\" are the same.\n - Ignore subtitles after the number. \"The Primal Hunter 7\" matches \"The Primal Hunter 7 - A LitRPG Adventure\".\n - Ignore format differences like \"(Light Novel)\", \"[Audiobook]\", \"A LitRPG Saga\", etc.\n - If unsure, assume we already own it (don't include in missing).\n\n Return ONLY a JSON object with IDs of books we are CERTAIN we don't have:\n {\"missing_ids\": [123, 456]}\n\n If we have all books or are unsure, return: {\"missing_ids\": []}",
"batching": {}
},
"id": "aef43627-3228-42ab-a2fe-9f4b50d1a855",
"typeVersion": 1.9,
"position": [
960,
160
],
"type": "@n8n/n8n-nodes-langchain.chainLlm",
"name": "Basic LLM Chain",
"retryOnFail": true,
"onError": "continueRegularOutput"
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT * FROM followed_series\n WHERE active = true\n AND (last_checked_at IS NULL OR last_checked_at < NOW() - INTERVAL '24 hours')\n ORDER BY last_checked_at ASC NULLS FIRST\n",
"options": {}
},
"name": "Select Series",
"type": "n8n-nodes-base.postgres",
"id": "e7bc495c-a3e1-46b8-b0f1-ff49286344e7",
"typeVersion": 2.6,
"position": [
-608,
496
],
"credentials": {
"postgres": {
"id": "9grzZwW7Br6SzdV8",
"name": "n8n-media"
}
}
},
{
"parameters": {
"operation": "select",
"schema": {
"mode": "list",
"value": "public",
"__rl": true
},
"table": {
"__rl": true,
"cachedResultName": "smb_general_books",
"value": "smb_general_books",
"mode": "list"
},
"returnAll": true,
"where": {
"values": [
{
"column": "followed_series_id",
"value": "={{ $('Loop Over Items').item.json.id }}"
}
]
},
"options": {}
},
"typeVersion": 2.6,
"name": "Select Book Titles",
"type": "n8n-nodes-base.postgres",
"position": [
512,
272
],
"id": "62be829f-6d74-4142-9420-eb4f6947df09",
"executeOnce": true,
"credentials": {
"postgres": {
"id": "9grzZwW7Br6SzdV8",
"name": "n8n-media"
}
}
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "kRZyX9H2uDHHncpE",
"mode": "list",
"cachedResultUrl": "/workflow/kRZyX9H2uDHHncpE",
"cachedResultName": "MAM Transmission Manager"
},
"workflowInputs": {
"schema": [],
"matchingColumns": [],
"value": {},
"mappingMode": "defineBelow",
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"mode": "each",
"options": {}
},
"position": [
1984,
272
],
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.3,
"id": "b6ec81b3-4f62-494e-9437-1a31c82a0299",
"name": "Call 'MAM Transmission Manager'"
},
{
"parameters": {},
"type": "n8n-nodes-base.merge",
"typeVersion": 3.2,
"position": [
1312,
416
],
"id": "0e6b9d38-bf8b-4f52-83a7-55256eec306a",
"name": "Merge"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "d8fe3601-6f8f-4502-8231-a533dad5e5e5",
"leftValue": "={{ $json.error }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notExists",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
64,
416
],
"id": "2291aa2e-fb6b-42ec-892b-ee2b015514f7",
"name": "Search Error Check"
},
{
"parameters": {
"operation": "update",
"schema": {
"__rl": true,
"mode": "list",
"value": "public"
},
"table": {
"__rl": true,
"value": "followed_series",
"mode": "list",
"cachedResultName": "followed_series"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"series_name": "={{ $('Loop Over Items').last().json.series_name }}",
"last_checked_at": "={{ $now }}"
},
"matchingColumns": [
"series_name"
],
"schema": [
{
"id": "id",
"displayName": "id",
"required": false,
"defaultMatch": true,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "series_name",
"displayName": "series_name",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "author",
"displayName": "author",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "category",
"displayName": "category",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "smb_path",
"displayName": "smb_path",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "active",
"displayName": "active",
"required": false,
"defaultMatch": false,
"display": true,
"type": "boolean",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "created_at",
"displayName": "created_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "updated_at",
"displayName": "updated_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "mam_series_id",
"displayName": "mam_series_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "enrichment_status",
"displayName": "enrichment_status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": true
},
{
"id": "last_checked_at",
"displayName": "last_checked_at",
"required": false,
"defaultMatch": false,
"display": true,
"type": "dateTime",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {}
},
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
2432,
624
],
"id": "62ec7f39-4f13-4d8e-8676-076e281677a0",
"name": "Update rows in a table",
"credentials": {
"postgres": {
"id": "9grzZwW7Br6SzdV8",
"name": "n8n-media"
}
}
},
{
"parameters": {
"model": {
"__rl": true,
"mode": "id",
"value": "gemini_cli/gemini-3-flash-preview"
},
"responsesApiEnabled": false,
"options": {}
},
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"typeVersion": 1.3,
"position": [
960,
352
],
"id": "c6a2245c-ad50-465c-813a-a8ec206110e1",
"name": "OpenAI Chat Model",
"credentials": {
"openAiApi": {
"id": "sxSUdecXdMfKPuTu",
"name": "llm-proxy.ext.ben.io"
}
}
},
{
"parameters": {
"language": "pythonNative",
"pythonCode": "import json\n\n# Format search results for LLM consumption\n# Filters by m4b, English, and outputs ID|Title pairs\n\ninput_json = _items[0].get('json', {})\n\n# Handle both response structures\nbody = input_json.get('body', {})\nsearch_data = []\n\nif isinstance(body, dict):\n search_data = body.get('data', [])\n\nif not search_data and isinstance(input_json, dict):\n search_data = input_json.get('data', [])\n\nif not isinstance(search_data, list):\n search_data = []\n\n# Filter and format\nfiltered = []\nllm_lines = []\n\nfor x in search_data:\n filetype = str(x.get('filetype', '')).lower()\n lang_code = str(x.get('lang_code', '')).upper()\n\n # Must be m4b and English\n if 'm4b' not in filetype or lang_code != 'ENG':\n continue\n\n book_id = x.get('id')\n title = str(x.get('title', '')).strip()\n\n if book_id and title:\n filtered.append(x)\n llm_lines.append(f\"{book_id} | {title}\")\n\n# Output: full data for later lookup + formatted string for LLM\nresults = [{\n \"json\": {\n \"search_data\": filtered, # Full objects for ID lookup later\n \"llm_available\": \"\\n\".join(llm_lines), # Formatted for LLM prompt\n \"count\": len(filtered)\n }\n}]\n\nreturn results"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
272,
400
],
"id": "41482579-dd45-4e60-be7b-2e9f7d08f8b3",
"name": "Format for LLM"
},
{
"parameters": {
"language": "pythonNative",
"pythonCode": "import json\nimport re\n\n# Build download links from LLM output\n# LLM returns {\"missing_ids\": [123, 456]}\n# We match IDs to the search_data stored earlier\n\n# Collect inputs\nsearch_data = []\nmissing_ids = []\n\nfor item in _items:\n data = item.get('json', {})\n\n # From Format for LLM - the full search data\n if 'search_data' in data:\n search_data = data['search_data']\n\n # From LLM - parse the text field\n if 'text' in data:\n try:\n parsed = json.loads(data['text'])\n # Handle various formats\n if 'missing_ids' in parsed:\n missing_ids = parsed['missing_ids']\n elif 'missing_books' in parsed:\n # Fallback if LLM uses old format\n missing_ids = parsed['missing_books']\n except:\n pass\n\n # Direct missing_ids field\n if 'missing_ids' in data:\n missing_ids = data['missing_ids']\n\n# Ensure missing_ids is a list\nif not isinstance(missing_ids, list):\n missing_ids = []\n\n# Convert to set of ints for lookup\ntry:\n missing_id_set = set(int(x) for x in missing_ids if x)\nexcept:\n missing_id_set = set()\n\nif not missing_id_set:\n return [{\"json\": {\"found\": False, \"reason\": \"No missing books identified\"}}]\n\n# Build search_data lookup by ID\nid_to_item = {}\nfor item in search_data:\n book_id = item.get('id')\n if book_id:\n id_to_item[int(book_id)] = item\n\n# Patterns to filter out (box sets, collections, etc.)\nSKIP_PATTERNS = [\n 'box set',\n 'boxset',\n 'collection',\n \"publisher's pack\",\n 'publishers pack',\n 'omnibus',\n 'complete series',\n 'books 1',\n 'books 2',\n 'books 3',\n ', books ',\n 'audio immersion tunnel',\n]\n\ndef should_skip_title(title):\n \"\"\"Check if title matches skip patterns.\"\"\"\n title_lower = title.lower()\n for pattern in SKIP_PATTERNS:\n if pattern in title_lower:\n return True\n # Skip if title ends with \"series\" (e.g., \"The Land: Chaos Seeds Series\")\n if title_lower.rstrip().endswith(' series'):\n return True\n return False\n\ndef normalize_title(title):\n \"\"\"Normalize title for deduplication - extracts core title without subtitles.\"\"\"\n t = title.lower().strip()\n # Remove common suffixes/subtitles\n t = re.sub(r'\\s*[-:]\\s*a\\s+litrpg.*$', '', t, flags=re.IGNORECASE)\n t = re.sub(r'\\s*\\(light novel\\).*$', '', t, flags=re.IGNORECASE)\n t = re.sub(r'\\s*\\(audiobook\\).*$', '', t, flags=re.IGNORECASE)\n t = re.sub(r'\\s*\\[.*?\\]', '', t) # Remove bracketed text like [B00I2VWW5U]\n # Remove trailing subtitle after number (e.g., \"Book 7 - The Subtitle\" -> \"Book 7\")\n t = re.sub(r'(\\d+)\\s*[-:]\\s*.+$', r'\\1', t)\n # Normalize \"Vol.\" variations\n t = re.sub(r'\\bvol\\.?\\s*', 'vol ', t, flags=re.IGNORECASE)\n t = re.sub(r'\\bvolume\\s*', 'vol ', t, flags=re.IGNORECASE)\n # Normalize \"Book\" to just number\n t = re.sub(r'\\bbook\\s+(\\d+)', r'\\1', t, flags=re.IGNORECASE)\n # Normalize spacing and punctuation\n t = re.sub(r'[,:]', '', t)\n t = re.sub(r'\\s+', ' ', t).strip()\n return t\n\nresults = []\nseen_ids = set() # For deduplication by ID\nseen_titles = set() # For deduplication by normalized title\n\nfor book_id in missing_id_set:\n # Dedupe by ID check\n if book_id in seen_ids:\n continue\n seen_ids.add(book_id)\n\n if book_id not in id_to_item:\n continue\n\n item = id_to_item[book_id]\n title = str(item.get('title', '')).strip()\n\n # Skip box sets and collections\n if should_skip_title(title):\n continue\n\n # Dedupe by normalized title (prevents multiple uploads of same book)\n normalized = normalize_title(title)\n if normalized in seen_titles:\n continue\n seen_titles.add(normalized)\n\n # Parse series info\n meta_series = \"Unknown\"\n meta_book_num = \"0\"\n try:\n series_raw = item.get('series_info', '{}')\n s_data = json.loads(series_raw) if isinstance(series_raw, str) else series_raw\n if isinstance(s_data, dict) and s_data:\n for key, val in s_data.items():\n if isinstance(val, list) and len(val) >= 2:\n meta_series = str(val[0]).replace('&#039;', \"'\")\n meta_book_num = str(val[1])\n break\n except:\n pass\n\n # Skip items with Unknown series\n if meta_series == \"Unknown\":\n continue\n\n # Parse author info\n meta_author = \"Unknown\"\n try:\n author_raw = item.get('author_info', '{}')\n a_data = json.loads(author_raw) if isinstance(author_raw, str) else author_raw\n if isinstance(a_data, dict) and a_data:\n meta_author = str(list(a_data.values())[0])\n except:\n pass\n\n results.append({\n \"json\": {\n \"meta_title\": title,\n \"meta_series\": meta_series,\n \"meta_book_number\": meta_book_num,\n \"meta_author\": meta_author,\n \"mam_id\": book_id,\n \"dl_link\": f\"https://www.myanonamouse.net/tor/download.php?tid={book_id}\",\n \"found\": True\n }\n })\n\nif not results:\n return [{\"json\": {\"found\": False, \"reason\": \"No matching IDs found after filtering\"}}]\n\nreturn results\n"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1536,
416
],
"id": "fb4fed9b-1d34-4d07-85cf-ad3c50a52218",
"name": "Build Downloads from LLM"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"id": "ab236a62-6482-49d4-b3b4-00d8a692cf61",
"leftValue": "={{ $json.dl_link }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "exists",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1760,
416
],
"id": "a5baf7a6-b47f-4628-81f8-c5bf0c06f21a",
"name": "If"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
1024,
560
],
"id": "356d68c5-4ab8-4bbf-a91e-29567501d6e2",
"name": "do nothing"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
2208,
416
],
"id": "48de6e72-1427-46aa-87ce-0225323b78b4",
"name": "do nothing 2"
},
{
"parameters": {},
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
-176,
160
],
"id": "4b6e7bf3-ed30-4518-883c-b9e39bdbf982",
"name": "No Operation, do nothing"
}
],
"connections": {
"Schedule Trigger": {
"main": [
[
{
"type": "main",
"node": "Select Series",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
],
[
{
"type": "main",
"index": 0,
"node": "HTTP Request"
}
]
]
},
"HTTP Request": {
"main": [
[
{
"node": "Search Error Check",
"type": "main",
"index": 0
}
]
]
},
"Aggregate": {
"main": [
[
{
"index": 0,
"type": "main",
"node": "Basic LLM Chain"
}
]
]
},
"Select Book Titles": {
"main": [
[
{
"node": "Aggregate",
"type": "main",
"index": 0
}
]
]
},
"Select Series": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Call 'MAM Transmission Manager'": {
"main": [
[
{
"node": "do nothing 2",
"type": "main",
"index": 0
}
]
]
},
"Basic LLM Chain": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Merge": {
"main": [
[
{
"node": "Build Downloads from LLM",
"type": "main",
"index": 0
}
]
]
},
"Search Error Check": {
"main": [
[
{
"node": "Format for LLM",
"type": "main",
"index": 0
}
],
[
{
"node": "Update rows in a table",
"type": "main",
"index": 0
}
]
]
},
"Update rows in a table": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Basic LLM Chain",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"Format for LLM": {
"main": [
[
{
"node": "Select Book Titles",
"type": "main",
"index": 0
},
{
"node": "do nothing",
"type": "main",
"index": 0
}
]
]
},
"Build Downloads from LLM": {
"main": [
[
{
"node": "If",
"type": "main",
"index": 0
}
]
]
},
"If": {
"main": [
[
{
"node": "Call 'MAM Transmission Manager'",
"type": "main",
"index": 0
}
],
[
{
"node": "do nothing 2",
"type": "main",
"index": 0
}
]
]
},
"do nothing": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"do nothing 2": {
"main": [
[
{
"node": "Update rows in a table",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"availableInMCP": false,
"callerPolicy": "workflowsFromSameOwner"
},
"triggerCount": 1,
"versionId": "1b02aafb-2cc4-4541-a26f-be001d7e7741",
"owner": {
"type": "personal",
"projectId": "FeLO36wNUAcn61Wj",
"projectName": "Ben W <admin@ben.io>",
"personalEmail": "admin@ben.io"
},
"parentFolderId": "6tDyZCwqELStb6Ik",
"isArchived": false
}