{
  "nodes": [
    {
      "id": "ac555343-470d-4358-8a80-83fcb1f25680",
      "name": "Cut out Duplicate Songs",
      "type": "n8n-nodes-base.code",
      "position": [
        -1056,
        32
      ],
      "parameters": {
        "jsCode": "const seen = new Set();\n\nreturn items\n  .map(item => {\n    const track = item.json.track;\n    return {\n      json: {\n        trackName: track.name,\n        artistName: track.artists[0].name,\n        spotifyUrl: track.external_urls?.spotify || ''\n      }\n    };\n  })\n  .filter(item => {\n    const key = item.json.spotifyUrl;\n    if (seen.has(key)) return false;\n    seen.add(key);\n    return true;\n  });"
      },
      "typeVersion": 2
    },
    {
      "id": "4385abe2-943e-41c1-b892-50a155c9970c",
      "name": "Getting Lyrics",
      "type": "n8n-nodes-base.httpRequest",
      "onError": "continueRegularOutput",
      "position": [
        -864,
        32
      ],
      "parameters": {
        "url": "=https://lrclib.net/api/search?track_name={{ $json.trackName }}&artist_name={{ $json.artistName }}",
        "options": {}
      },
      "retryOnFail": true,
      "typeVersion": 4.4
    },
    {
      "id": "5562208c-a03f-4acf-aff2-52e3ae116417",
      "name": "Google Gemini Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatGoogleGemini",
      "position": [
        -992,
        512
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "25c8d472-8d61-466d-a4e4-26daa4b44c51",
      "name": "Every Sunday at noon",
      "type": "n8n-nodes-base.scheduleTrigger",
      "disabled": true,
      "position": [
        -1472,
        32
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 12 * * 7"
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "ea5d8c41-f819-466b-8a9e-45650617df8f",
      "name": "Combine existing Words with new Words",
      "type": "n8n-nodes-base.code",
      "position": [
        -320,
        32
      ],
      "parameters": {
        "jsCode": "const lyricsData = $('Put all Lyrics together').first().json;\n\nconst existingWords = items\n  .map(item => item.json.Word)\n  .filter(w => w);\n\nreturn [{\n  json: {\n    allLyrics: lyricsData.allLyrics,\n    songCount: lyricsData.songCount,\n    existingWords: existingWords\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8b180ba6-72d7-49e3-a3a3-ff2c92d8a5a0",
      "name": "Append to All Vocabularies",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        96,
        448
      ],
      "parameters": {
        "columns": {
          "value": {
            "Word": "={{ $json.Word }}",
            "Translation": "={{ $json.Translation }}"
          },
          "schema": [
            {
              "id": "Word",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Word",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Translation",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Translation",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=0",
          "cachedResultName": "All Vocabularies"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit?usp=drivesdk",
          "cachedResultName": "v"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "ce615da7-f013-4ecd-84fb-9e5f8d7ea435",
      "name": "Append to Weekly",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        96,
        256
      ],
      "parameters": {
        "columns": {
          "value": {
            "Word": "={{ $json.Word }}",
            "Translation": "={{ $json.Translation }}"
          },
          "schema": [
            {
              "id": "Word",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Word",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Translation",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Translation",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $('Parse, Deduplicate, Week Code').first().json.sheetName }}"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit?usp=drivesdk",
          "cachedResultName": "V"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "f6450cae-151f-4581-88a6-70a8aea78a40",
      "name": "Create sheet",
      "type": "n8n-nodes-base.googleSheets",
      "onError": "continueRegularOutput",
      "position": [
        96,
        48
      ],
      "parameters": {
        "title": "={{ $json.sheetName }}",
        "options": {},
        "operation": "create",
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit?usp=drivesdk",
          "cachedResultName": "V"
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.7
    },
    {
      "id": "0d01d6fb-ab2e-4f9f-a58d-e4a23fef7091",
      "name": "Cut out SheetName Column",
      "type": "n8n-nodes-base.code",
      "position": [
        -80,
        256
      ],
      "parameters": {
        "jsCode": "return items.map(item => ({\n  json: {\n    Word: item.json.Word,\n    Translation: item.json.Translation\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "320a4012-9d05-4f84-8ea5-1d3057903760",
      "name": "Parse, Deduplicate, Week Code",
      "type": "n8n-nodes-base.code",
      "position": [
        -400,
        256
      ],
      "parameters": {
        "jsCode": "// 1. Parse AI Output\nconst results = [];\nfor (const item of items) {\n  const output = item.json.output || '';\n  const clean = output.replace(/```json\\n?/g, '').replace(/```/g, '').trim();\n  try {\n    const words = JSON.parse(clean);\n    for (const word of words) {\n      results.push({\n        Word: word.word,\n        Translation: word.translation.replace(/\\s*\\(.*?\\)\\s*$/, '')\n      });\n    }\n  } catch (e) {\n    console.log('Parse Error:', e.message);\n    console.log('Raw output:', output);\n  }\n}\n\n// 2. Deduplicate\nconst existingWords = $('Combine existing Words with new Words')\n  .first()\n  .json\n  .existingWords\n  .map(w => String(w || '').toLowerCase().trim());\n\n\nconst unique = results.filter(r =>\n  !existingWords.includes(r.Word.toLowerCase().trim())\n);\n\n// 3. Week Number\nconst now = new Date();\nconst startOfYear = new Date(now.getFullYear(), 0, 1);\nconst weekNumber = Math.ceil(((now - startOfYear) / 86400000 + startOfYear.getDay() + 1) / 7);\nconst sheetName = `Week ${weekNumber}`;\n\nreturn unique.map(word => ({\n  json: {\n    Word: word.Word,\n    Translation: word.Translation,\n    sheetName: sheetName\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "85b1c956-61a4-42e8-b55a-51399a9bb136",
      "name": "Read all Vocabularies",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        -496,
        32
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit#gid=0",
          "cachedResultName": "All Vocabularies"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_SHEET_ID",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/YOUR_GOOGLE_SHEET_ID/edit?usp=drivesdk",
          "cachedResultName": "v"
        }
      },
      "typeVersion": 4.7,
      "alwaysOutputData": true
    },
    {
      "id": "a3b0fb08-7bf9-4472-a4a2-694e1eeedc5a",
      "name": "Put all Lyrics together",
      "type": "n8n-nodes-base.code",
      "position": [
        -672,
        32
      ],
      "parameters": {
        "jsCode": "const songs = {};\n\nfor (const item of items) {\n  const data = item.json;\n  \n  let cleanTrack = (data.trackName || '')\n    .replace(/\\s*[\\(\\[].*?[\\)\\]]/g, '')\n    .replace(/^.*?\\s*-\\s*\"?/, '')\n    .replace(/['\"\\\\]/g, '')\n    .trim()\n    .toLowerCase();\n  \n  let cleanArtist = (data.artistName || '')\n    .replace(/\\s*-\\s*Topic$/i, '')\n    .trim()\n    .toLowerCase();\n  \n  const key = cleanArtist + ' - ' + cleanTrack;\n  const lyrics = data.plainLyrics || '';\n  \n  if (!songs[key] || lyrics.length > songs[key].plainLyrics.length) {\n    songs[key] = {\n      trackName: data.trackName || '',\n      artistName: cleanArtist,\n      plainLyrics: lyrics.replace(/[\\n\\r]+/g, ' ').replace(/\"/g, '\\\\\"')\n    };\n  }\n}\n\nlet allLyrics = '';\nconst uniqueSongs = Object.values(songs).filter(song => song.plainLyrics.length > 0);\n\nfor (const song of uniqueSongs) {\n  allLyrics += `\\n\\n--- ${song.trackName} by ${song.artistName} ---\\n${song.plainLyrics}`;\n}\n\nreturn [{\n  json: {\n    allLyrics: allLyrics.trim(),\n    songCount: uniqueSongs.length\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d2f489e3-6d7b-459d-8c8b-5de5fbd416e0",
      "name": "Get the recently played Songs",
      "type": "n8n-nodes-base.spotify",
      "position": [
        -1264,
        32
      ],
      "parameters": {
        "limit": 20,
        "operation": "recentlyPlayed"
      },
      "typeVersion": 1
    },
    {
      "id": "1746123a-c0f4-4647-af27-5405a57489f9",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1264,
        -16
      ],
      "parameters": {
        "color": "#E6E6E6",
        "width": 496,
        "height": 96,
        "content": "### \ud83c\udfb5 Fetches recently played songs from Spotify, removes duplicates, and gets lyrics from lrclib.net.\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "f492d8c4-f78a-4beb-9d4b-ed8fe971e083",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -672,
        -16
      ],
      "parameters": {
        "color": "#EBEBEB",
        "width": 464,
        "height": 96,
        "content": "### \ud83d\udcd6 Reads all previously learned words from Google Sheets for duplicate checking.\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "3f5c49f9-8034-485a-83c4-0746038de529",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        464
      ],
      "parameters": {
        "color": "#FFFFFF",
        "width": 304,
        "height": 96,
        "content": "## \ud83e\udd16 AI EXTRACTION\n\u27a1\ufe0f To change the language: edit the prompt."
      },
      "typeVersion": 1
    },
    {
      "id": "950ce12a-f627-4564-8892-30c670fa4192",
      "name": "AI Agent",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -736,
        256
      ],
      "parameters": {
        "text": "=All Lyrics:\n{{ $json.allLyrics }}",
        "options": {
          "systemMessage": "=You are a vocabulary extraction assistant for language learners.\n\nInstructions:\n- You will receive lyrics from multiple songs\n- Extract a total of 40-60 of the most useful English vocabulary words across ALL songs\n- NO duplicates - each word only once, even if it appears in multiple songs\n- Skip proper nouns, names, slang abbreviations, and filler words\n- Skip very basic words like \"I\", \"you\", \"the\", \"is\", \"and\", \"to\", \"a\"\n- If a word is slang, extract its standard English equivalent\n- For each word provide the German translation\n- Focus on B1-B2 level vocabulary that is useful for everyday conversation\n\n\nRespond ONLY with a valid JSON array, no other text:\n[{\"word\": \"new vocabulary word\", \"translation\": \"translation in the learner's native language\"}]"
        },
        "promptType": "define"
      },
      "typeVersion": 3.1
    },
    {
      "id": "fd131c0f-f8a9-4f34-933c-8cfbd939023a",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1968,
        -192
      ],
      "parameters": {
        "width": 448,
        "height": 656,
        "content": "## How it works\n\nEvery Sunday it fetches your recently played songs, finds the lyrics via lrclib.net, and uses Google Gemini to extract 40-60 useful vocabulary words (B1-B2 level). New words are deduplicated against all previously learned words and written to Google Sheets \u2014 both a master tab and a weekly tab.\n\nYou review the flashcards using the free Flashcard Lab app (iOS + Android), which reads directly from Google Sheets with built-in spaced repetition.\n\n### Setup\n1. Google Cloud Console: Create project, enable Sheets + Drive API, create OAuth2 credentials, set app to \"In Production\"\n2. Get a free Gemini API key from Google AI Studio\n3. Spotify Developer Dashboard: Create app, note Client ID + Secret\n4. Create a Google Sheet with tab \"All Vocabularies\" and headers \"Word\" + \"Translation\"\n5. Import workflow, connect credentials, select your Sheet in all Google Sheets nodes\n6. Test with \"Execute Workflow\", then enable the schedule trigger\n\n### Customization\nTo change the language: edit the prompt in \"Prepare all Lyrics into Pairs\" \u2014 that's the only place to change. Listen to music in the language you're learning for best results."
      },
      "typeVersion": 1
    },
    {
      "id": "868c82e0-239c-40d2-a005-31103592e1d1",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        320,
        256
      ],
      "parameters": {
        "color": 3,
        "width": 224,
        "height": 128,
        "content": "\u26a0\ufe0f Set your Google Cloud app to \"In Production\" \u2014 in \"Testing\" mode, tokens expire after 7 days and break the weekly automation."
      },
      "typeVersion": 1
    },
    {
      "id": "7a525920-6616-4cca-8207-165635336ae0",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        -160
      ],
      "parameters": {
        "color": "#EBEBEB",
        "width": 304,
        "height": 192,
        "content": "## \ud83d\udcdd SAVE TO SHEETS\n\nCreates a weekly tab (e.g. \"Week 11\") and writes\nnew words there + to the master \"All Vocabularies\" tab.\nUse Flashcard Lab app to study from the weekly tabs.\n\n"
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "AI Agent": {
      "main": [
        [
          {
            "node": "Parse, Deduplicate, Week Code",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Getting Lyrics": {
      "main": [
        [
          {
            "node": "Put all Lyrics together",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every Sunday at noon": {
      "main": [
        [
          {
            "node": "Get the recently played Songs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read all Vocabularies": {
      "main": [
        [
          {
            "node": "Combine existing Words with new Words",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cut out Duplicate Songs": {
      "main": [
        [
          {
            "node": "Getting Lyrics",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Put all Lyrics together": {
      "main": [
        [
          {
            "node": "Read all Vocabularies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cut out SheetName Column": {
      "main": [
        [
          {
            "node": "Append to Weekly",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Gemini Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Get the recently played Songs": {
      "main": [
        [
          {
            "node": "Cut out Duplicate Songs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse, Deduplicate, Week Code": {
      "main": [
        [
          {
            "node": "Create sheet",
            "type": "main",
            "index": 0
          },
          {
            "node": "Cut out SheetName Column",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append to All Vocabularies",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine existing Words with new Words": {
      "main": [
        [
          {
            "node": "AI Agent",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}