{
  "id": "Seod85SvKJz8s9i8",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "YouTube New Video Tracker to RocketChat",
  "tags": [],
  "nodes": [
    {
      "id": "2eb03397-3ee1-4b10-9999-86cc0a867334",
      "name": "Hourly Check",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -3216,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "e2b2952c-f756-42db-96bb-668c581183b1",
      "name": "Channel List",
      "type": "n8n-nodes-base.set",
      "position": [
        -3024,
        0
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "channel-urls",
              "name": "channel_urls",
              "type": "string",
              "value": "=https://www.youtube.com/@NoCopyrightSounds/videos\nhttps://www.youtube.com/@chillnation/videos\nhttps://www.youtube.com/@TrapNation/videos"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "d677b521-7c4e-4b30-8eb8-097af6e2b379",
      "name": "Process URLs",
      "type": "n8n-nodes-base.function",
      "position": [
        -2832,
        0
      ],
      "parameters": {
        "functionCode": "const urlsText = $input.item.json.channel_urls;\nconst channelUrls = urlsText.split('\\n').map(url => url.trim()).filter(url => url.length > 0 && url.startsWith('http'));\nconst processedChannels = channelUrls.map(url => {\n  let channelName = '', needsChannelIdFetch = false, rssUrl = '', type = '', fetchUrl = '', videosUrl = url.replace('/videos', '') + '/videos';\n  const handleMatch = url.match(/@([^/]+)/);\n  if (handleMatch) {\n    channelName = handleMatch[1];\n    type = 'handle';\n    needsChannelIdFetch = true;\n    fetchUrl = url.replace('/videos', '');\n  }\n  const userMatch = url.match(/\\/user\\/([^/]+)/);\n  if (userMatch) {\n    channelName = userMatch[1];\n    rssUrl = `https://www.youtube.com/feeds/videos.xml?user=${channelName}`;\n    type = 'user';\n    needsChannelIdFetch = false;\n  }\n  const channelMatch = url.match(/\\/channel\\/(UC[^/]+)/);\n  if (channelMatch) {\n    const channelId = channelMatch[1];\n    channelName = channelId;\n    rssUrl = `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`;\n    type = 'channel';\n    needsChannelIdFetch = false;\n  }\n  return {json: {originalUrl: url, channelName, rssUrl, videosUrl, type, needsChannelIdFetch, fetchUrl}};\n});\nreturn processedChannels;"
      },
      "typeVersion": 1
    },
    {
      "id": "f1f63f20-4582-4390-a4b3-a24913a1e210",
      "name": "Loop Over Channels",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -2640,
        0
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "7ea5498f-d113-4744-9fea-a92a5444fa19",
      "name": "Need Channel ID?",
      "type": "n8n-nodes-base.if",
      "position": [
        -2448,
        0
      ],
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.needsChannelIdFetch}}",
              "value2": true
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "89d1ebf6-57bc-472e-85cb-bfdf4b0e48f9",
      "name": "Fetch Channel Page",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -2256,
        -96
      ],
      "parameters": {
        "url": "={{$json.fetchUrl}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "86171ccd-6601-4131-aa14-ceb71b9bc83e",
      "name": "Extract Channel ID",
      "type": "n8n-nodes-base.function",
      "position": [
        -2064,
        -96
      ],
      "parameters": {
        "functionCode": "const html = $input.first().json.data || '';\nconst channelData = $input.first().json;\nlet channelId = null, method = 'none';\nlet match = html.match(/\"channelId\":\"(UC[^\"]+)\"/);\nif (match && match[1]) { channelId = match[1]; method = 'channelId_regex'; }\nif (!channelId) { match = html.match(/\"externalId\":\"(UC[^\"]+)\"/); if (match && match[1]) { channelId = match[1]; method = 'externalId_regex'; }}\nif (!channelId) {\n  try {\n    const ytInitialMatch = html.match(/var ytInitialData = (\\{.+?\\});/);\n    if (ytInitialMatch && ytInitialMatch[1]) {\n      const ytData = JSON.parse(ytInitialMatch[1]);\n      channelId = ytData?.metadata?.channelMetadataRenderer?.externalId || ytData?.header?.c4TabbedHeaderRenderer?.channelId;\n      if (channelId && channelId.startsWith('UC')) { method = 'ytInitialData'; } else { channelId = null; }\n    }\n  } catch (e) {}\n}\nif (channelId) {\n  return [{json: {originalUrl: channelData.originalUrl, channelName: channelData.channelName, videosUrl: channelData.videosUrl, type: channelData.type, channelId, rssUrl: `https://www.youtube.com/feeds/videos.xml?channel_id=${channelId}`, method}}];\n}\nreturn [{json: {originalUrl: channelData.originalUrl, channelName: channelData.channelName, videosUrl: channelData.videosUrl, type: channelData.type, channelId: null, rssUrl: null, needsScraping: true}}];"
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "62c06677-5dbd-4ee4-9527-90fe24fee224",
      "name": "Merge Channels",
      "type": "n8n-nodes-base.merge",
      "position": [
        -1872,
        0
      ],
      "parameters": {},
      "typeVersion": 2.1
    },
    {
      "id": "108b6f50-2d21-4224-9a1b-15f6ef3852ed",
      "name": "Has RSS URL?",
      "type": "n8n-nodes-base.if",
      "position": [
        -1680,
        0
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.rssUrl }}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "6bd5da5c-045f-4efe-96c9-a772fad86982",
      "name": "Fetch RSS Feed",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1488,
        -96
      ],
      "parameters": {
        "url": "={{$json.rssUrl}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "76736b53-59b6-4f9f-99dc-62785cd3da63",
      "name": "Scrape Videos Page",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -1488,
        96
      ],
      "parameters": {
        "url": "={{$json.videosUrl}}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text"
            }
          }
        }
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "6f7d9d73-1942-4190-bd96-14cef1a52a90",
      "name": "Parse XML",
      "type": "n8n-nodes-base.xml",
      "position": [
        -1296,
        -96
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "02414efd-c8d2-4994-b126-431d07ddf979",
      "name": "Parse Scraped Videos",
      "type": "n8n-nodes-base.function",
      "position": [
        -1104,
        96
      ],
      "parameters": {
        "functionCode": "const html = $input.first().json.data || '';\nconst channelData = $input.first().json;\nif (!html) return [];\nconst videos = [];\ntry {\n  const ytInitialMatch = html.match(/var ytInitialData = (\\{.+?\\});/);\n  if (!ytInitialMatch) return [];\n  const ytData = JSON.parse(ytInitialMatch[1]);\n  const tabs = ytData?.contents?.twoColumnBrowseResultsRenderer?.tabs || [];\n  for (const tab of tabs) {\n    const contents = tab?.tabRenderer?.content?.sectionListRenderer?.contents || [];\n    for (const section of contents) {\n      const items = section?.itemSectionRenderer?.contents || [];\n      for (const item of items) {\n        const gridRenderer = item?.gridRenderer?.items || [];\n        for (const gridItem of gridRenderer) {\n          const videoRenderer = gridItem?.gridVideoRenderer;\n          if (videoRenderer && videoRenderer.videoId) {\n            videos.push({json: {channel: channelData.channelName || 'Unknown', title: videoRenderer.title?.runs?.[0]?.text || 'Unknown', url: `https://www.youtube.com/watch?v=${videoRenderer.videoId}`, published: videoRenderer.publishedTimeText?.simpleText || '', source: 'scraped', videoId: videoRenderer.videoId}});\n            if (videos.length >= 20) break;\n          }\n        }\n        if (videos.length >= 20) break;\n      }\n      if (videos.length >= 20) break;\n    }\n    if (videos.length >= 20) break;\n  }\n} catch (e) {\n  const videoMatches = html.match(/\"videoId\":\"([a-zA-Z0-9_-]{11})\"/g) || [];\n  for (let i = 0; i < Math.min(videoMatches.length, 20); i++) {\n    const match = videoMatches[i].match(/\"videoId\":\"([^\"]+)\"/);\n    if (match && match[1]) {\n      videos.push({json: {channel: channelData.channelName || 'Unknown', title: `Video ${i + 1}`, url: `https://www.youtube.com/watch?v=${match[1]}`, published: 'Unknown', source: 'scraped_regex', videoId: match[1]}});\n    }\n  }\n}\nreturn videos;"
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "6d24ee39-38d4-4342-adcb-e1676be222cc",
      "name": "Merge RSS & Scraped",
      "type": "n8n-nodes-base.merge",
      "position": [
        -912,
        0
      ],
      "parameters": {},
      "typeVersion": 2.1
    },
    {
      "id": "38de295a-4716-4316-a7dc-d3e7ec51c326",
      "name": "Filter New Videos",
      "type": "n8n-nodes-base.function",
      "position": [
        -720,
        0
      ],
      "parameters": {
        "functionCode": "const items = $input.all();\nconst now = new Date();\nconst cutoffDate = new Date(now);\ncutoffDate.setHours(cutoffDate.getHours() - 1);\nconst results = [];\nfor (const item of items) {\n  const data = item.json;\n  if (data.feed) {\n    const feed = data.feed;\n    const channelTitle = feed.title || 'Unknown Channel';\n    const entries = feed.entry || [];\n    if (Array.isArray(entries)) {\n      for (const entry of entries) {\n        const publishedStr = entry.published || '';\n        const updatedStr = entry.updated || '';\n        const publishedDate = publishedStr ? new Date(publishedStr) : null;\n        const updatedDate = updatedStr ? new Date(updatedStr) : null;\n        const isRecent = (publishedDate && publishedDate >= cutoffDate) || (updatedDate && updatedDate >= cutoffDate);\n        if (isRecent) {\n          const linkObj = entry.link || {};\n          const videoUrl = typeof linkObj === 'object' ? (linkObj.href || '') : '';\n          if (videoUrl && !videoUrl.includes('/shorts/')) {\n            results.push({json: {channel: channelTitle, title: entry.title || '', url: videoUrl, published: publishedStr, updated: updatedStr, source: 'rss'}});\n          }\n        }\n      }\n    }\n  } else if (data.source === 'scraped' || data.source === 'scraped_regex') {\n    if (data.url && !data.url.includes('/shorts/')) {\n      results.push({json: data});\n    }\n  }\n}\nreturn results;"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "d342d259-030a-47be-bd6d-3d236418d73f",
      "name": "Has Videos?",
      "type": "n8n-nodes-base.if",
      "position": [
        -576,
        0
      ],
      "parameters": {
        "conditions": {
          "number": [
            {
              "value1": "={{ $input.all().length }}",
              "value2": 1,
              "operation": "largerEqual"
            }
          ],
          "string": [
            {
              "value1": "={{ $json.url }}",
              "value2": "youtube",
              "operation": "contains"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "669cdb4a-5add-4f98-908e-4fc1a6af4566",
      "name": "Loop Over Videos",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -416,
        -96
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "e4ed2923-c345-4aad-97e1-ed6b9a9dd7bb",
      "name": "Has New Video?",
      "type": "n8n-nodes-base.if",
      "position": [
        -256,
        -96
      ],
      "parameters": {
        "conditions": {
          "string": [
            {
              "value1": "={{ $json.url }}",
              "operation": "isNotEmpty"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "89f92304-c802-4886-a268-8048881310aa",
      "name": "RocketChat Notification",
      "type": "n8n-nodes-base.rocketchat",
      "position": [
        80,
        -192
      ],
      "parameters": {
        "text": "=New YouTube video!\nChannel: {{ $json.channel }}\nTitle: {{$json.title}}\nLink: {{ $json.url }}\nPublished: {{ $json.published }}\nSource: {{ $json.source }}",
        "channel": "YOUR-CHANNEL-NAME",
        "options": {},
        "attachments": []
      },
      "credentials": {
        "rocketchatApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "d2ddb2c0-874d-4ded-a896-477fdb7f57ac",
      "name": "No New Video",
      "type": "n8n-nodes-base.function",
      "position": [
        -96,
        0
      ],
      "parameters": {
        "functionCode": "return [];"
      },
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "7ed90169-64b5-44e0-8b71-1c1a773fe0a9",
      "name": "Continue Channel",
      "type": "n8n-nodes-base.set",
      "position": [
        -416,
        96
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "processed",
              "name": "processed",
              "type": "boolean",
              "value": "true"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "30967f34-ee84-4135-b74e-54e1e27c5d7c",
      "name": "Wait 30 sec",
      "type": "n8n-nodes-base.wait",
      "position": [
        -96,
        -192
      ],
      "parameters": {
        "amount": 30
      },
      "typeVersion": 1.1
    },
    {
      "id": "d8f7bd28-6dbb-4620-ad2c-cbbec0ff7f55",
      "name": "Schedule",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3440,
        -48
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 216,
        "content": "## Schedule\n\nRuns every hour by default. Adjust interval here if needed. Match schedule with filter window (1 hour). Avoid checking more than every 15 minutes."
      },
      "typeVersion": 1
    },
    {
      "id": "da7fd470-b90f-475a-aa4d-c54bee91b670",
      "name": "Config: Channel URLs",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3120,
        176
      ],
      "parameters": {
        "color": 5,
        "width": 280,
        "height": 252,
        "content": "## CONFIG REQUIRED\n\nAdd YouTube channel URLs, one per line. All formats supported:\n- youtube.com/@handle\n- youtube.com/user/username\n- youtube.com/channel/UCxxx\n\nTip: Include /videos suffix or workflow adds it automatically."
      },
      "typeVersion": 1
    },
    {
      "id": "2359231d-a676-4839-8cba-2bf9778859c1",
      "name": "Performance",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2688,
        -448
      ],
      "parameters": {
        "color": 4,
        "width": 200,
        "height": 232,
        "content": "## Performance Tips\n\nMonitor 20-50 channels per workflow for best performance. For more channels, split into multiple workflows running at different times."
      },
      "typeVersion": 1
    },
    {
      "id": "9c303bb8-8c33-4d6c-8e0e-02c3d32016a4",
      "name": "Batch Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        288
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 168,
        "content": "## Batch Processing\n\nProcesses each channel sequentially to avoid overwhelming the workflow."
      },
      "typeVersion": 1
    },
    {
      "id": "e54c6f26-31bd-4630-be5a-3b9eba5b85ee",
      "name": "Error Handling",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1936,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 204,
        "content": "## Error Handling\n\nHTTP nodes use continueOnFail to ensure workflow completes even if a channel fails. Failed channels are skipped, others continue processing normally."
      },
      "typeVersion": 1
    },
    {
      "id": "acb76108-d961-4990-a6e3-8e72802c09e1",
      "name": "Time Window",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 196,
        "content": "## 1-Hour Window\n\nOnly videos published in the last hour are processed. This matches the hourly schedule and prevents duplicate notifications."
      },
      "typeVersion": 1
    },
    {
      "id": "d07a0015-ace6-4477-933e-7c97eaf6d18c",
      "name": "Shorts Excluded",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        416
      ],
      "parameters": {
        "color": 7,
        "width": 200,
        "height": 248,
        "content": "## Shorts Excluded\n\nYouTube Shorts are filtered out by checking URL for /shorts/ path. This keeps focus on regular videos and reduces notification volume."
      },
      "typeVersion": 1
    },
    {
      "id": "ea8e34dd-5db7-4ac8-9c36-16f4fa1e3ece",
      "name": "Config: RocketChat",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        80,
        16
      ],
      "parameters": {
        "color": 5,
        "width": 280,
        "height": 356,
        "content": "## CONFIG REQUIRED\n\nRocketChat setup:\n1. Create bot user in RocketChat admin\n2. Generate personal access token or use password\n3. Add bot to target notification channel\n4. Select credentials in this node\n5. Replace YOUR-CHANNEL-NAME with actual channel\n\nCustomize message template using available fields: channel, title, url, published, source."
      },
      "typeVersion": 1
    },
    {
      "id": "e716aff5-b87b-42da-8de8-b435e578ccd2",
      "name": "Credentials Setup",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3328,
        -384
      ],
      "parameters": {
        "color": 5,
        "width": 320,
        "height": 252,
        "content": "## Before You Start\n\nRequired: RocketChat API credentials\n\n1. Create bot user in RocketChat\n2. Generate personal access token\n3. Add credentials in n8n Credentials section\n4. Select credential in RocketChat node"
      },
      "typeVersion": 1
    },
    {
      "id": "e8e53f60-0b87-4d1d-9375-bb5f3171a84a",
      "name": "Main Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3888,
        -272
      ],
      "parameters": {
        "width": 400,
        "height": 640,
        "content": "## How it works\n\nThis workflow monitors YouTube channels and sends RocketChat alerts for new videos. It runs hourly and supports all YouTube URL formats: handles, user URLs, and channel IDs.\n\nThe workflow uses dual fetching: official RSS feeds for reliability and HTML scraping for immediate results. It filters videos published in the last hour and automatically excludes YouTube Shorts. Each new video triggers a RocketChat notification with channel name, title, and link.\n\n## Setup steps\n\n1. Create RocketChat bot user and get API credentials\n2. Add bot to your notification channel\n3. Edit Channel List node: Add YouTube channel URLs, one per line\n4. Configure RocketChat node: Select credentials and set channel name\n5. Test manually: Run workflow to verify notifications work\n6. Activate: Enable workflow for automatic hourly monitoring\n\nBest practice: Keep hourly schedule to avoid rate limits and match the 1-hour filter window."
      },
      "typeVersion": 1
    },
    {
      "id": "64220b47-55a9-42e4-aa21-3ee079568e3d",
      "name": "URL Processing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2928,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "content": "## URL Processing\n\nParses channel URLs and identifies format type. Handles require fetching the channel page to extract the channel ID for RSS feed generation."
      },
      "typeVersion": 1
    },
    {
      "id": "d0dc82dc-7602-4938-ad57-7dfb6a2be4b4",
      "name": "Channel ID Extraction",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2272,
        -288
      ],
      "parameters": {
        "color": 7,
        "width": 320,
        "content": "## Channel ID Extraction\n\nHandle URLs need channel ID to generate RSS feed URL. Workflow scrapes the channel page HTML and uses regex to extract the ID (starts with UC)."
      },
      "typeVersion": 1
    },
    {
      "id": "52684022-bb73-48dc-8f4d-a23c2f58958c",
      "name": "Video Fetching",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1520,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 360,
        "height": 180,
        "content": "## Video Fetching\n\nDual approach: fetches official YouTube RSS feed when available, otherwise scrapes videos page HTML. RSS updates every 15 minutes. Both methods merge and deduplicate for maximum reliability."
      },
      "typeVersion": 1
    },
    {
      "id": "0cdd6251-777d-4cc5-9a78-5bd1ff95144c",
      "name": "Filtering",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -784,
        -192
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 168,
        "content": "## Filtering\n\nFilters videos from last hour and excludes Shorts. RSS and scraped results are deduplicated to prevent double notifications."
      },
      "typeVersion": 1
    },
    {
      "id": "8b424188-fd3a-4a97-8ae1-ae1de948a928",
      "name": "Troubleshooting",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        -320
      ],
      "parameters": {
        "color": 3,
        "width": 280,
        "height": 268,
        "content": "## No Notifications?\n\nCommon causes:\n- No new videos in last hour (normal)\n- Wrong RocketChat credentials\n- Bot not in channel\n- Channel URL incorrect\n\nCheck node outputs and execution logs for details."
      },
      "typeVersion": 1
    },
    {
      "id": "28245fad-96f4-482a-a075-204df24a613b",
      "name": "Notification",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -416,
        -352
      ],
      "parameters": {
        "color": 7,
        "width": 600,
        "height": 124,
        "content": "## Notification\n\nLoops through new videos and sends RocketChat message for each. 30-second delay prevents rate limiting. Configure target channel in RocketChat node."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "931dfdfc-fa9f-4998-b1a7-238463dad634",
  "connections": {
    "Parse XML": {
      "main": [
        [
          {
            "node": "Merge RSS & Scraped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has Videos?": {
      "main": [
        [
          {
            "node": "Loop Over Videos",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Continue Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 30 sec": {
      "main": [
        [
          {
            "node": "RocketChat Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Channel List": {
      "main": [
        [
          {
            "node": "Process URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has RSS URL?": {
      "main": [
        [
          {
            "node": "Fetch RSS Feed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Scrape Videos Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Hourly Check": {
      "main": [
        [
          {
            "node": "Channel List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No New Video": {
      "main": [
        [
          {
            "node": "Continue Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process URLs": {
      "main": [
        [
          {
            "node": "Loop Over Channels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch RSS Feed": {
      "main": [
        [
          {
            "node": "Parse XML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Has New Video?": {
      "main": [
        [
          {
            "node": "Wait 30 sec",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No New Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Channels": {
      "main": [
        [
          {
            "node": "Has RSS URL?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Continue Channel": {
      "main": [
        [
          {
            "node": "Loop Over Channels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Videos": {
      "main": [
        [
          {
            "node": "Has New Video?",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Has New Video?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Need Channel ID?": {
      "main": [
        [
          {
            "node": "Fetch Channel Page",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Merge Channels",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Filter New Videos": {
      "main": [
        [
          {
            "node": "Has Videos?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Channel ID": {
      "main": [
        [
          {
            "node": "Merge Channels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Channel Page": {
      "main": [
        [
          {
            "node": "Extract Channel ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Channels": {
      "main": [
        [],
        [
          {
            "node": "Need Channel ID?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Videos Page": {
      "main": [
        [
          {
            "node": "Parse Scraped Videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge RSS & Scraped": {
      "main": [
        [
          {
            "node": "Filter New Videos",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Scraped Videos": {
      "main": [
        [
          {
            "node": "Merge RSS & Scraped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "RocketChat Notification": {
      "main": [
        [
          {
            "node": "No New Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}