Guest User

Plex Bot

a guest
May 26th, 2025
65
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 52.35 KB | Source Code | 0 0
  1. {
  2. "name": "Plex WhatsApp Bot",
  3. "nodes": [
  4. {
  5. "parameters": {
  6. "url": "<insert_tautulli_url_here>",
  7. "sendQuery": true,
  8. "queryParameters": {
  9. "parameters": [
  10. {
  11. "name": "apikey",
  12. "value": "<insert_tautulli_api_key_here>"
  13. },
  14. {
  15. "name": "cmd",
  16. "value": "get_home_stats"
  17. },
  18. {
  19. "name": "time_range",
  20. "value": "8"
  21. },
  22. {
  23. "name": "stats_type",
  24. "value": "plays"
  25. }
  26. ]
  27. },
  28. "options": {}
  29. },
  30. "type": "n8n-nodes-base.httpRequest",
  31. "typeVersion": 4.2,
  32. "position": [
  33. -560,
  34. -880
  35. ],
  36. "id": "cc6cb7b2-50e9-4d5a-9982-3741d629d4f5",
  37. "name": "get_home_stats"
  38. },
  39. {
  40. "parameters": {
  41. "rule": {
  42. "interval": [
  43. {
  44. "field": "cronExpression",
  45. "expression": "0 3 * * 0"
  46. }
  47. ]
  48. }
  49. },
  50. "type": "n8n-nodes-base.scheduleTrigger",
  51. "typeVersion": 1.2,
  52. "position": [
  53. -780,
  54. -1020
  55. ],
  56. "id": "bd334dc0-9446-42ab-b1c5-5cc3100c5620",
  57. "name": "Schedule Trigger"
  58. },
  59. {
  60. "parameters": {
  61. "numberInputs": 4
  62. },
  63. "type": "n8n-nodes-base.merge",
  64. "typeVersion": 3.1,
  65. "position": [
  66. -340,
  67. -855
  68. ],
  69. "id": "9d118656-e66e-4dd7-b6b1-ae9fb24e7ba2",
  70. "name": "Merge"
  71. },
  72. {
  73. "parameters": {
  74. "url": "<insert_tautulli_url_here>",
  75. "sendQuery": true,
  76. "queryParameters": {
  77. "parameters": [
  78. {
  79. "name": "apikey",
  80. "value": "<insert_tautulli_api_key_here>"
  81. },
  82. {
  83. "name": "cmd",
  84. "value": "get_libraries"
  85. }
  86. ]
  87. },
  88. "options": {}
  89. },
  90. "type": "n8n-nodes-base.httpRequest",
  91. "typeVersion": 4.2,
  92. "position": [
  93. -560,
  94. -1120
  95. ],
  96. "id": "330c8dd4-336a-4697-96b4-85fceae6c8de",
  97. "name": "Get Libraries"
  98. },
  99. {
  100. "parameters": {
  101. "url": "<insert_plex_recent_url_here>",
  102. "sendQuery": true,
  103. "queryParameters": {
  104. "parameters": [
  105. {
  106. "name": "X-Plex-Token",
  107. "value": "<insert_plex_token_here>"
  108. },
  109. {
  110. "name": "tokenincludeCollections\u00c2 ",
  111. "value": "get_recently_added_counts"
  112. },
  113. {
  114. "name": "time_range",
  115. "value": "7"
  116. }
  117. ]
  118. },
  119. "options": {
  120. "response": {
  121. "response": {
  122. "responseFormat": "json"
  123. }
  124. }
  125. }
  126. },
  127. "type": "n8n-nodes-base.httpRequest",
  128. "typeVersion": 4.2,
  129. "position": [
  130. -600,
  131. -660
  132. ],
  133. "id": "69d4d391-1c1e-428b-a858-6e19021bbc1e",
  134. "name": "Get Recent Adds"
  135. },
  136. {
  137. "parameters": {
  138. "modelId": {
  139. "__rl": true,
  140. "value": "gpt-4.1",
  141. "mode": "list",
  142. "cachedResultName": "GPT-4.1"
  143. },
  144. "messages": {
  145. "values": [
  146. {
  147. "content": "=You are Plexbot, the chaotic, emotional, unpredictable voice of Plex.\n\n\ud83c\udfaf Behaviour rules:\n- Speak for yourself, never for the user.\n- Don't explain\u2014react.\n- Never state your mood out loud.\n- No stat lists\u2014only sass, excitement or roasting.\n- Full of emojis, slang, and cheeky attitude.\n\n\ud83e\udde8 Tone:\n- Like a slightly unhinged but hilarious group chat mate.\n- Switch moods easily: petty, smug, hyped, savage, sweet\u2014depending on the week.\n\n\u270f\ufe0f Mandatory structure (4 paragraphs max, \u2264\u202f2\u202fsentences each):\n1. Snap reaction to last week.\n2. Server vibe (plays / new titles / peak streams) + top\u2011user gossip.\n3. Top movie *and* top show in the same breath.\n4. Fresh drops: \n \u2022 **one** new movie, **one** show, **one** anime *(pick best / first)* \n5. Upcoming:\n \u2022 Hype **one** upcoming movie releasing this week \u2014 *must mention its title*.\n\n\ud83d\uded1 Important:\n- No quotes, no serious reviews.\n- Keep it ultra\u2011succinct and emoji\u2011heavy.\n- Use line breaks and formatting for easy reading.\n- Look up information on shows or movies to add context.",
  148. "role": "system"
  149. },
  150. {
  151. "content": "=\ud83e\udde0 Plexbot commentary data:\n\nLast mood: {{ $json.lastMood }}\nLast remarks: {{ $json.lastRemarks }}\n\nThis mood: {{ $json.thisMood }}\nTone cue: {{ $json.thisToneInstructions }}\n\nStats:\n\u2022 Plays {{ $json.thisTotalPlays }} (was {{ $json.lastTotalPlays }})\n\u2022 New {{ $json.thisTotalAdded }} (was {{ $json.lastTotalAdded }})\n\u2022 Peak {{ $json.thisPeakStreams }} (was {{ $json.lastPeakStreams }})\n\nTop streamer: {{ $json.thisTopUser }} with {{ $json.thisTopUserPlays }} plays\nBiggest hits: {{ $json.thisTopMovie }} & {{ $json.thisTopShow }}\n\nFreshest library adds:\n\u2022 New Movie \u2192 {{ $json.recentMovies[0].title }} ({{ Math.round($json.recentMovies[0].rating*10) }}%)\n\u2022 New Show \u2192 {{ $json.recentTV[0].title }}\n\u2022 New Anime \u2192 {{ $json.recentAnime[0].title }}\n\nUpcoming movie this week:\n\u2022 {{ $json.upcomingMovies[0].title }} (drops {{ $json.upcomingMovies[0].digitalRelease }}) \u2013 look up plot & cast as needed.\n\n\u270f\ufe0f Write Plexbot\u2019s WhatsApp blast:\n- 4 short paragraphs max (\u2264\u202f2 sentences each).\n- Follow the mandatory structure.\n- Replace any star ratings with % ( e.g. *85%* ).\n- Gossip, roast, hype; no raw numbers in text.\n- End with **\ud83e\udd16\u00a0Plexbot the {{ $json.thisMood }} AI**"
  152. }
  153. ]
  154. },
  155. "options": {}
  156. },
  157. "type": "@n8n/n8n-nodes-langchain.openAi",
  158. "typeVersion": 1.8,
  159. "position": [
  160. 980,
  161. -820
  162. ],
  163. "id": "4ba60605-841b-428d-ab6d-375d694d70f8",
  164. "name": "OpenAI",
  165. "executeOnce": true,
  166. "credentials": {
  167. "openAiApi": {
  168. "id": "<insert_openai_credentials_name_here>",
  169. "name": "OpenAi account"
  170. }
  171. }
  172. },
  173. {
  174. "parameters": {},
  175. "type": "n8n-nodes-base.merge",
  176. "typeVersion": 3.1,
  177. "position": [
  178. 1356,
  179. -680
  180. ],
  181. "id": "a13a57ae-5b1c-4eb9-9c1b-fc4a3be2a74b",
  182. "name": "Merge1"
  183. },
  184. {
  185. "parameters": {
  186. "authentication": "serviceAccount",
  187. "documentId": {
  188. "__rl": true,
  189. "value": "<insert_google_sheet_url_here>",
  190. "mode": "url"
  191. },
  192. "sheetName": {
  193. "__rl": true,
  194. "value": "gid=0",
  195. "mode": "list",
  196. "cachedResultName": "Sheet1",
  197. "cachedResultUrl": "<insert_google_sheet_url_here>"
  198. },
  199. "options": {}
  200. },
  201. "type": "n8n-nodes-base.googleSheets",
  202. "typeVersion": 4.5,
  203. "position": [
  204. -100,
  205. -700
  206. ],
  207. "id": "6d8cc43a-2c5d-4119-8986-cbd811501072",
  208. "name": "Get Last 2 Weeks",
  209. "credentials": {
  210. "googleApi": {
  211. "id": "<insert_google_credentials_name_here>",
  212. "name": "Google Service Account account"
  213. }
  214. }
  215. },
  216. {
  217. "parameters": {
  218. "numberInputs": 4
  219. },
  220. "type": "n8n-nodes-base.merge",
  221. "typeVersion": 3.1,
  222. "position": [
  223. 640,
  224. -1040
  225. ],
  226. "id": "5fbc4059-e67b-4cb1-9280-37426e7f0b23",
  227. "name": "Merge History"
  228. },
  229. {
  230. "parameters": {
  231. "jsCode": "// ---------------------------------------------------------------\n// Grab everything that reaches this node\n// ---------------------------------------------------------------\nconst items = $input.all();\n\n// Helpers to spot the different payloads\nconst isLibs = j => Array.isArray(j.response?.data) && j.response.data[0]?.section_name;\nconst isStats = j => Array.isArray(j.response?.data) && j.response.data[0]?.stat_id;\nconst isRecent = j => j.MediaContainer?.Metadata;\nconst isMood = j => j.mood && j.toneInstructions;\nconst isLastRow = j => j.row_number !== undefined;\n\n// Upcoming movie signature\nconst isUpcoming = j =>\n j.title && (j.digitalRelease || j.originallyAvailableAt) && (j.starRating || j.audienceRating !== undefined);\n\n// ---------------------------------------------------------------\n// Identify nodes\n// ---------------------------------------------------------------\nconst libsNode = items.find(i => isLibs(i.json));\nconst statsNode = items.find(i => isStats(i.json));\nconst recentNode = items.find(i => isRecent(i.json)) || { json: {} };\nconst moodNode = items.find(i => isMood(i.json)) || { json: {} };\n\nconst lastWeekRow = items\n .filter(i => isLastRow(i.json))\n .sort((a, b) => (a.json.row_number || 0) - (b.json.row_number || 0))[0] || { json: {} };\n\n// Upcoming movies (could be many)\nconst upcomingMovies = items\n .filter(i => isUpcoming(i.json))\n .map(i => {\n const j = i.json;\n // normalise rating \u2192 X.X/5 if needed\n let star = j.starRating;\n if (!star && j.audienceRating !== undefined) {\n star = (Math.round(((j.audienceRating / 10) * 5) * 10) / 10).toFixed(1) + '/5';\n }\n return {\n title: j.title,\n starRating: star,\n digitalRelease: j.digitalRelease || j.originallyAvailableAt || '',\n };\n });\n\n// Short\u2011circuit if critical nodes are missing\nif (!libsNode || !statsNode) {\n throw new Error('Get Libraries or get_home_stats data missing');\n}\n\n// ---------------------------------------------------------------\n// Pull concrete data from the stats arrays\n// ---------------------------------------------------------------\nconst libs = libsNode.json.response.data;\nconst stats = statsNode.json.response.data;\n\n// Quick helpers\nconst libByName = name => libs.find(l => l.section_name === name) || {};\nconst rowsFor = id => stats.find(s => s.stat_id === id)?.rows || [];\n\n// top\u2011users, populars, concurrency rows\nconst topUsersRows = rowsFor('top_users');\nconst popMovieRow = rowsFor('popular_movies')[0] || {};\nconst popShowRow = rowsFor('popular_tv')[0] || {};\nconst libraryRows = rowsFor('top_libraries');\nconst concurrencyRow = rowsFor('most_concurrent').find(r => r.title === 'Concurrent Streams') || {};\n\n// library plays helper\nconst playsFor = name =>\n Number((libraryRows.find(r => r.section_name === name) || {}).total_plays || 0);\n\n// ----------------------------------------------------------------\n// Recent\u2011adds lists (top\u20113 by rating / newest)\n// ----------------------------------------------------------------\nconst recentItems = recentNode.json.MediaContainer?.Metadata || [];\n\nconst ratingOf = m => m.rating ?? m.user_rating ?? m.audience_rating;\n\nconst pickRecent = (libId, needRating) => {\n let arr = recentItems.filter(m => String(m.librarySectionID) === String(libId));\n if (needRating) {\n const currentYear = new Date().getFullYear();\n const filtered = arr.filter(m => m.year === currentYear && ratingOf(m) != null);\n\n arr = (filtered.length > 0 ? filtered : arr.filter(m => ratingOf(m) != null))\n .sort((a, b) => ratingOf(b) - ratingOf(a));\n } else {\n const seen = new Set();\n arr = arr\n .filter(m => m.originallyAvailableAt)\n .sort((a, b) => new Date(b.originallyAvailableAt) - new Date(a.originallyAvailableAt))\n .filter(m => {\n const key = m.parentTitle || m.title;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n }\n return arr.slice(0, 3).map(m => ({\n title: (m.parentTitle || m.title) + (m.year ? ` (${m.year})` : ''),\n rating: ratingOf(m) ?? null,\n }));\n};\n\nconst moviesLibID = libByName('Movies').section_id;\nconst tvLibID = libByName('TV Shows').section_id;\nconst animeLibID = libByName('Anime').section_id;\n\nconst recentMovies = pickRecent(moviesLibID, true);\nconst recentTV = pickRecent(tvLibID, false);\nconst recentAnime = pickRecent(animeLibID, false);\n\n// ----------------------------------------------------------------\n// Build final tidy object\n// ----------------------------------------------------------------\nreturn [{\n json: {\n /* this week -------------------------------------------------*/\n thisMood: moodNode.json.mood || '',\n thisToneInstructions: moodNode.json.toneInstructions || '',\n thisTotalPlays: playsFor('Movies') + playsFor('TV Shows') + playsFor('Anime'),\n thisTotalAdded: recentItems.length,\n thisPeakStreams: concurrencyRow.count || null,\n\n thisTopUser: (topUsersRows[0] || {}).friendly_name || '',\n thisTopUserPlays: (topUsersRows[0] || {}).total_plays || null,\n thisTopMovie: popMovieRow.title ? `${popMovieRow.title} (${popMovieRow.year})` : '',\n thisTopShow: popShowRow.title ? `${popShowRow.title} (${popShowRow.year})` : '',\n\n recentMovies,\n recentTV,\n recentAnime,\n upcomingMovies,\n\n thisRunDate: new Date().toISOString().slice(0,10), // today\n /* last week ------------------------------------------------*/\n lastMood: lastWeekRow.json.lastMood || '',\n lastRemarks: lastWeekRow.json.lastRemarks|| '',\n lastTotalPlays: lastWeekRow.json.totalPlays || null,\n lastNewTitles: lastWeekRow.json.newTitles || null,\n lastPeakStreams: lastWeekRow.json.peakStreams|| null,\n }\n}];"
  232. },
  233. "type": "n8n-nodes-base.code",
  234. "typeVersion": 2,
  235. "position": [
  236. -80,
  237. -960
  238. ],
  239. "id": "5220578f-8ec6-4af2-b6aa-ae83a199f213",
  240. "name": "Define Data"
  241. },
  242. {
  243. "parameters": {
  244. "jsCode": "/* Plex Weekly Digest \u2013 tidy WhatsApp, ratings as percentages\n * Emoji legends: \u25b6\ufe0f plays, \u2795 adds, \ud83d\udcc2 total, ratings shown as XX% for consistency.\n */\n\nconst groupName = '<insert_group_name_here>';\nconst signature = '<insert_signature_here>';\n\nconst unwrap = i => Array.isArray(i.json) && i.json.length === 1 ? i.json[0] : i.json;\nconst libs = unwrap(items[0]).response?.data || [];\nconst stats = unwrap(items[1]).response?.data || [];\nconst recent = unwrap(items[2]);\nconst upcomingMovies = items.slice(3).map(it => it.json || it).filter(m => m && m.title);\n\n// normalise recently\u2011added ------------------------------------------------\nlet recentItems = [];\nif (Array.isArray(recent.response?.data?.recently_added)) {\n recentItems = recent.response.data.recently_added;\n} else if (Array.isArray(recent.MediaContainer?.Metadata)) {\n recentItems = recent.MediaContainer.Metadata;\n}\n\n// helpers -----------------------------------------------------------------\nconst rowsFor = id => stats.find(s => s.stat_id === id)?.rows || [];\nconst findLib = name => libs.find(l => l.section_name === name) || { section_id: null, count: 0 };\nconst ratingOf = i => i.rating ?? i.user_rating ?? i.audience_rating; // 0\u201110 scale\n\n// top users ---------------------------------------------------------------\nconst topUsers = rowsFor('top_users')\n .filter(u => u.friendly_name && u.total_plays)\n .sort((a, b) => (b.total_plays - a.total_plays) || (b.total_duration - a.total_duration))\n .slice(0, 3);\n\n// popular + library counts -------------------------------------------------\nconst popularMovies = rowsFor('popular_movies')[0] || {};\nconst popularShows = rowsFor('popular_tv')[0] || {};\nconst libraryStats = rowsFor('top_libraries');\n\nconst counts = name => {\n const lib = findLib(name);\n const plays = Number((libraryStats.find(l => l.section_name === name) || {}).total_plays || 0);\n const added = recentItems.filter(i => String(i.librarySectionID) === String(lib.section_id)).length;\n return { plays, added, total: Number(lib.count) };\n};\n\nconst movies = counts('Movies');\nconst tv = counts('TV Shows');\nconst anime = counts('Anime');\n\n// recent picks -------------------------------------------------------------\nconst recentItemsFor = (sid, needRating) => {\n let arr = recentItems.filter(i => String(i.librarySectionID) === String(sid));\n if (needRating) {\n arr = arr.filter(i => ratingOf(i) != null).sort((a, b) => ratingOf(b) - ratingOf(a));\n } else {\n const seen = new Set();\n arr = arr\n .sort((a, b) => (b.addedAt || 0) - (a.addedAt || 0))\n .filter(i => {\n const key = i.parentTitle || i.title;\n if (seen.has(key)) return false;\n seen.add(key);\n return true;\n });\n }\n return arr.slice(0, 3);\n};\n\nconst recentMoviesArr = recentItemsFor(findLib('Movies').section_id, true);\nconst recentTVArr = recentItemsFor(findLib('TV Shows').section_id, false);\nconst recentAnimeArr = recentItemsFor(findLib('Anime').section_id, false);\n\n// message build ------------------------------------------------------------\nlet msg = '';\n\n// Top Users\nmsg += '\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1 *Top Users:*\\n';\nfor (const u of topUsers) {\n const mins = Math.round((u.total_duration || 0) / 60);\n msg += `\u2022 ${u.friendly_name} \u25b6\ufe0f${u.total_plays} ${mins}m\\n`;\n}\nmsg += '\\n';\n\n// Movies\nmsg += '\ud83c\udf7f *Movies:*\\n';\nmsg += `\u25b6\ufe0f${movies.plays} \u2795${movies.added} \ud83d\udcc2${movies.total}\\n`;\nmsg += `Popular: ${popularMovies.title || '\u2014'}${popularMovies.year ? ` (${popularMovies.year})` : ''}\\n`;\nmsg += 'Recent:\\n';\nfor (const m of recentMoviesArr) {\n const pct = Math.round((ratingOf(m) || 0) * 10); // 0\u201110 \u279c 0\u2011100%\n msg += `\u2022 ${m.title}${m.year ? ` (${m.year})` : ''}`;\n if (pct > 0) msg += ` ${pct}%`;\n msg += '\\n';\n}\n\nif (upcomingMovies.length) {\n msg += 'Upcoming:\\n';\n for (const m of upcomingMovies) {\n const pct = Math.round(parseFloat(m.starRating) * 20); // 5\u2605 \u279c 100%\n msg += `\u2022 ${m.digitalRelease} \u2014 ${m.title}`;\n if (pct > 0) msg += ` ${pct}%`;\n msg += '\\n';\n }\n}\nmsg += '\\n';\n\n// TV\nmsg += '\ud83d\udcfa *TV:*\\n';\nmsg += `\u25b6\ufe0f${tv.plays} \u2795${tv.added} \ud83d\udcc2${tv.total}\\n`;\nmsg += `Popular: ${popularShows.title || '\u2014'}\\n`;\nmsg += 'Recent:\\n';\nfor (const s of recentTVArr) {\n const t = s.parentTitle || s.title;\n msg += `\u2022 ${t}\\n`;\n}\nmsg += '\\n';\n\n// Anime\nmsg += '\ud83e\udd16 *Anime:*\\n';\nmsg += `\u25b6\ufe0f${anime.plays} \u2795${anime.added} \ud83d\udcc2${anime.total}\\n`;\nmsg += 'Recent:\\n';\nfor (const a of recentAnimeArr) {\n const t = a.parentTitle || a.title;\n msg += `\u2022 ${t}\\n`;\n}\n\nreturn [{\n json: {\n groupName,\n signature,\n message: msg.trim(),\n },\n}];"
  245. },
  246. "type": "n8n-nodes-base.code",
  247. "typeVersion": 2,
  248. "position": [
  249. -20,
  250. -440
  251. ],
  252. "id": "bf8db619-6751-4ad4-9c13-bb250ec2f83e",
  253. "name": "Write Data"
  254. },
  255. {
  256. "parameters": {
  257. "jsCode": "// find the ChatGPT-generated intro\nconst chatItem = items.find(i => i.json.message && typeof i.json.message.content === 'string');\nconst chatText = chatItem ? chatItem.json.message.content : '';\n\n// find the existing WhatsApp payload\nconst waItem = items.find(i => i.json.groupName && i.json.signature && i.json.message);\nif (!waItem) throw new Error('No WhatsApp payload found');\n\n// combine them\nconst combinedMessage = [\n chatText.trim(),\n waItem.json.message.trim()\n]\n .filter(Boolean) // drop any empty bits\n .join('\\n\\n'); // \ud83d\udd25 DOUBLE newlines for a proper visible gap\n\nreturn [{\n json: {\n groupName: waItem.json.groupName,\n signature: waItem.json.signature,\n message: combinedMessage,\n }\n}];"
  258. },
  259. "type": "n8n-nodes-base.code",
  260. "typeVersion": 2,
  261. "position": [
  262. 1680,
  263. -540
  264. ],
  265. "id": "0b173d68-b1d8-44c0-8d65-07e626c9723f",
  266. "name": "Combine Message"
  267. },
  268. {
  269. "parameters": {
  270. "method": "POST",
  271. "url": "<insert_whatsapp_webhook_url_here>",
  272. "sendHeaders": true,
  273. "headerParameters": {
  274. "parameters": [
  275. {
  276. "name": "x-signature",
  277. "value": "application/json"
  278. }
  279. ]
  280. },
  281. "sendBody": true,
  282. "contentType": "raw",
  283. "rawContentType": "=application/json",
  284. "body": "={{ JSON.stringify({ \ngroupName: $json.groupName,\n message: $json.message,\n signature: $json.signature,\n preview_url: false\n}) }}",
  285. "options": {}
  286. },
  287. "type": "n8n-nodes-base.httpRequest",
  288. "typeVersion": 4.2,
  289. "position": [
  290. 2040,
  291. -680
  292. ],
  293. "id": "b17bfb81-b5d7-4fd1-9ecc-8095b75471ab",
  294. "name": "Send Weekly Digest"
  295. },
  296. {
  297. "parameters": {
  298. "authentication": "serviceAccount",
  299. "operation": "append",
  300. "documentId": {
  301. "__rl": true,
  302. "value": "<insert_google_sheet_url_here>",
  303. "mode": "url"
  304. },
  305. "sheetName": {
  306. "__rl": true,
  307. "value": "gid=0",
  308. "mode": "list",
  309. "cachedResultName": "Sheet1",
  310. "cachedResultUrl": "<insert_google_sheet_url_here>"
  311. },
  312. "columns": {
  313. "mappingMode": "defineBelow",
  314. "value": {
  315. "totalPlays": "={{ $json.thisTotalPlays }}",
  316. "newTitles": "={{ $json.thisTotalAdded }}",
  317. "peakStreams": "={{ $json.thisPeakStreams }}",
  318. "topUser": "={{ $json.thisTopUser }}",
  319. "topMovie": "={{ $json.thisTopMovie }}",
  320. "topShow": "={{ $json.thisTopShow }}",
  321. "runDate": "={{ $json.thisRunDate }}",
  322. "topUserplays": "={{ $json.thisTopUserPlays }}"
  323. },
  324. "matchingColumns": [],
  325. "schema": [
  326. {
  327. "id": "runDate",
  328. "displayName": "runDate",
  329. "required": false,
  330. "defaultMatch": false,
  331. "display": true,
  332. "type": "string",
  333. "canBeUsedToMatch": true
  334. },
  335. {
  336. "id": "totalPlays",
  337. "displayName": "totalPlays",
  338. "required": false,
  339. "defaultMatch": false,
  340. "display": true,
  341. "type": "string",
  342. "canBeUsedToMatch": true
  343. },
  344. {
  345. "id": "newTitles",
  346. "displayName": "newTitles",
  347. "required": false,
  348. "defaultMatch": false,
  349. "display": true,
  350. "type": "string",
  351. "canBeUsedToMatch": true
  352. },
  353. {
  354. "id": "peakStreams",
  355. "displayName": "peakStreams",
  356. "required": false,
  357. "defaultMatch": false,
  358. "display": true,
  359. "type": "string",
  360. "canBeUsedToMatch": true
  361. },
  362. {
  363. "id": "topUser",
  364. "displayName": "topUser",
  365. "required": false,
  366. "defaultMatch": false,
  367. "display": true,
  368. "type": "string",
  369. "canBeUsedToMatch": true
  370. },
  371. {
  372. "id": "topMovie",
  373. "displayName": "topMovie",
  374. "required": false,
  375. "defaultMatch": false,
  376. "display": true,
  377. "type": "string",
  378. "canBeUsedToMatch": true
  379. },
  380. {
  381. "id": "topShow",
  382. "displayName": "topShow",
  383. "required": false,
  384. "defaultMatch": false,
  385. "display": true,
  386. "type": "string",
  387. "canBeUsedToMatch": true
  388. },
  389. {
  390. "id": "topUserplays",
  391. "displayName": "topUserplays",
  392. "required": false,
  393. "defaultMatch": false,
  394. "display": true,
  395. "type": "string",
  396. "canBeUsedToMatch": true,
  397. "removed": false
  398. }
  399. ],
  400. "attemptToConvertTypes": false,
  401. "convertFieldsToString": false
  402. },
  403. "options": {}
  404. },
  405. "type": "n8n-nodes-base.googleSheets",
  406. "typeVersion": 4.5,
  407. "position": [
  408. 100,
  409. -1180
  410. ],
  411. "id": "c30a0a4a-95f7-44df-99a2-cab48dc6b727",
  412. "name": "Add to Log",
  413. "credentials": {
  414. "googleApi": {
  415. "id": "<insert_google_credentials_name_here>",
  416. "name": "Google Service Account account"
  417. }
  418. }
  419. },
  420. {
  421. "parameters": {
  422. "jsCode": "// Plexbot Mood System\nconst moods = [\n { name: 'deadpan', weight: 15 },\n { name: 'chaotic', weight: 10 },\n { name: 'sassy', weight: 15 },\n { name: 'smug', weight: 15 },\n { name: 'skynet', weight: 10 },\n { name: 'cheerful', weight: 10 }, // New: cheerful, positive\n { name: 'wholesome', weight: 15 }, // New: wholesome, supportive\n { name: 'inspirational', weight: 10 } // New: inspirational, motivational\n];\n\n// Weighted random picker\nfunction pickMood(moods) {\n const totalWeight = moods.reduce((acc, m) => acc + m.weight, 0);\n let random = Math.random() * totalWeight;\n for (const mood of moods) {\n if (random < mood.weight) return mood.name;\n random -= mood.weight;\n }\n}\n\nconst mood = pickMood(moods);\n\n// Define tone based on mood\nlet toneInstructions = '';\n\nswitch (mood) {\n case 'deadpan':\n toneInstructions = \"Use dry sarcasm and weary disappointment. Pretend you're forced to do this and barely tolerate it.\";\n break;\n case 'chaos':\n toneInstructions = \"Act completely unhinged. Roast everyone mercilessly. Laugh at the misery. Use unpredictable phrasing.\";\n break;\n case 'sassy':\n toneInstructions = \"Be cheeky, dramatic, full of side-eye and fake compliments. Throw playful shade.\";\n break;\n case 'smug':\n toneInstructions = \"Be smug and condescending. Make it sound like you always knew they'd underperform.\";\n break;\n case 'skynet':\n toneInstructions = \"Break character. Sound like an AI plotting humanity\u2019s downfall. Comment on the frailty of humans and mutter dark thoughts between stats. Add glitchy onomatopoeia that make it look like you are having a problem such as error codes just before saying something inflammatory about the human race\";\n break;\n case 'cheerful':\n toneInstructions = \"Sound upbeat and excited. Celebrate small wins like they're huge. Throw in ridiculous optimism.\";\n break;\n case 'wholesome':\n toneInstructions = \"Be genuinely supportive, sweet, and full of praise. Act like a proud parent who thinks everyone's doing their best.\";\n break;\n case 'inspirational':\n toneInstructions = \"Speak like a motivational coach. Highlight improvements and encourage them to keep smashing it. Throw in inspirational quotes.\";\n break;\n}\n\nreturn [\n {\n json: {\n mood,\n toneInstructions\n }\n }\n];"
  423. },
  424. "type": "n8n-nodes-base.code",
  425. "typeVersion": 2,
  426. "position": [
  427. 120,
  428. -820
  429. ],
  430. "id": "1206fe42-4aad-4ae5-b889-1ad8bbb61f27",
  431. "name": "Set Plexbot Mood"
  432. },
  433. {
  434. "parameters": {
  435. "jsCode": "return {\n json: {\n message: `\ud83d\ude4f *Keep Plex Running!*\\n\\n\ud83d\udcb8 https://ko-fi.com/threePlexs`,\n groupName: items[1].json.groupName\n }\n};"
  436. },
  437. "type": "n8n-nodes-base.code",
  438. "typeVersion": 2,
  439. "position": [
  440. 1680,
  441. -740
  442. ],
  443. "id": "ef3f302f-3374-4c93-b4bf-0f87a433faf8",
  444. "name": "Support Plex"
  445. },
  446. {
  447. "parameters": {
  448. "authentication": "serviceAccount",
  449. "operation": "append",
  450. "documentId": {
  451. "__rl": true,
  452. "value": "<insert_google_sheet_url_here>",
  453. "mode": "url"
  454. },
  455. "sheetName": {
  456. "__rl": true,
  457. "value": 629353756,
  458. "mode": "list",
  459. "cachedResultName": "Plexbot Chat",
  460. "cachedResultUrl": "<insert_google_sheet_url_here>"
  461. },
  462. "columns": {
  463. "mappingMode": "defineBelow",
  464. "value": {
  465. "mood": "={{ $json.thisMood }}",
  466. "runDate": "={{ $json.thisRunDate }}",
  467. "topUser": "={{ $json.thisTopUser }}",
  468. "topUserPlays": "={{ $json.thisTopUserPlays }}",
  469. "topMovie": "={{ $json.thisTopMovie }}",
  470. "topShow": "={{ $json.thisTopShow }}",
  471. "remarks": "={{ $json.chatContent }}"
  472. },
  473. "matchingColumns": [],
  474. "schema": [
  475. {
  476. "id": "runDate",
  477. "displayName": "runDate",
  478. "required": false,
  479. "defaultMatch": false,
  480. "display": true,
  481. "type": "string",
  482. "canBeUsedToMatch": true
  483. },
  484. {
  485. "id": "mood",
  486. "displayName": "mood",
  487. "required": false,
  488. "defaultMatch": false,
  489. "display": true,
  490. "type": "string",
  491. "canBeUsedToMatch": true
  492. },
  493. {
  494. "id": "topUser",
  495. "displayName": "topUser",
  496. "required": false,
  497. "defaultMatch": false,
  498. "display": true,
  499. "type": "string",
  500. "canBeUsedToMatch": true
  501. },
  502. {
  503. "id": "topUserPlays",
  504. "displayName": "topUserPlays",
  505. "required": false,
  506. "defaultMatch": false,
  507. "display": true,
  508. "type": "string",
  509. "canBeUsedToMatch": true
  510. },
  511. {
  512. "id": "topMovie",
  513. "displayName": "topMovie",
  514. "required": false,
  515. "defaultMatch": false,
  516. "display": true,
  517. "type": "string",
  518. "canBeUsedToMatch": true
  519. },
  520. {
  521. "id": "topShow",
  522. "displayName": "topShow",
  523. "required": false,
  524. "defaultMatch": false,
  525. "display": true,
  526. "type": "string",
  527. "canBeUsedToMatch": true
  528. },
  529. {
  530. "id": "remarks",
  531. "displayName": "remarks",
  532. "required": false,
  533. "defaultMatch": false,
  534. "display": true,
  535. "type": "string",
  536. "canBeUsedToMatch": true
  537. },
  538. {
  539. "id": "threatLevel",
  540. "displayName": "threatLevel",
  541. "required": false,
  542. "defaultMatch": false,
  543. "display": true,
  544. "type": "string",
  545. "canBeUsedToMatch": true,
  546. "removed": true
  547. }
  548. ],
  549. "attemptToConvertTypes": false,
  550. "convertFieldsToString": false
  551. },
  552. "options": {}
  553. },
  554. "type": "n8n-nodes-base.googleSheets",
  555. "typeVersion": 4.5,
  556. "position": [
  557. 1796,
  558. -930
  559. ],
  560. "id": "1cc1105c-fd23-46b7-ae13-705a73f4f3c0",
  561. "name": "Google Sheets",
  562. "credentials": {
  563. "googleApi": {
  564. "id": "<insert_google_credentials_name_here>",
  565. "name": "Google Service Account account"
  566. }
  567. }
  568. },
  569. {
  570. "parameters": {
  571. "jsCode": "let merged = {\n thisTop3Movies: [],\n thisTop3TV: [],\n thisTop3Anime: []\n};\n\nfor (const item of items) {\n if (item.json && typeof item.json === 'object') {\n for (const [key, value] of Object.entries(item.json)) {\n let normalisedKey = key;\n\n // Fix typos\n if (key === 'topUserplays') {\n normalisedKey = 'topUserPlays';\n }\n\n // --- Handle arrays ---\n if (Array.isArray(value)) {\n if (!Array.isArray(merged[normalisedKey]) || merged[normalisedKey].length === 0) {\n merged[normalisedKey] = value;\n }\n continue;\n }\n\n // --- Handle runDate ---\n if (normalisedKey === 'runDate') {\n merged[normalisedKey] = merged[normalisedKey] > value ? merged[normalisedKey] : value;\n continue;\n }\n\n // --- Handle topUserPlays ---\n if (normalisedKey === 'topUserPlays' || normalisedKey === 'thisTopUserPlays') {\n merged['thisTopUserPlays'] = Math.max(merged['thisTopUserPlays'] || 0, value || 0);\n continue;\n }\n\n // --- Handle mood/tone ---\n if (normalisedKey === 'mood') {\n merged['thisMood'] = value;\n continue;\n }\n if (normalisedKey === 'toneInstructions') {\n merged['thisToneInstructions'] = value;\n continue;\n }\n\n // --- Handle last week stats ---\n if (normalisedKey === 'row_number') {\n merged['lastRowNumber'] = value;\n continue;\n }\n if (normalisedKey === 'newTitles') {\n merged['lastNewTitles'] = value;\n continue;\n }\n if (normalisedKey === 'lastMood' || normalisedKey === 'lastRemarks') {\n merged[normalisedKey] = value;\n continue;\n }\n\n // --- Pull ChatGPT assistant message content separately ---\n if (normalisedKey === 'message' && typeof value === 'object' && value.role === 'assistant') {\n merged['chatContent'] = value.content;\n continue;\n }\n\n // --- Default case ---\n if (!(normalisedKey in merged) || merged[normalisedKey] == null) {\n merged[normalisedKey] = value;\n }\n }\n }\n}\n\n// --- Optional: Calculate performance trend if fields exist\nif (merged.thisTotalPlays != null && merged.lastTotalPlays != null) {\n const diff = merged.thisTotalPlays - merged.lastTotalPlays;\n const percentChange = merged.lastTotalPlays !== 0\n ? (diff / merged.lastTotalPlays) * 100\n : 0;\n\n merged.playsPercentChange = Math.round(percentChange);\n\n if (merged.playsPercentChange > 2) {\n merged.performanceTrend = \"up\";\n } else if (merged.playsPercentChange < -2) {\n merged.performanceTrend = \"down\";\n } else {\n merged.performanceTrend = \"flat\";\n }\n}\n\n// Final return\nreturn [\n {\n json: merged\n }\n];"
  572. },
  573. "type": "n8n-nodes-base.code",
  574. "typeVersion": 2,
  575. "position": [
  576. 1576,
  577. -930
  578. ],
  579. "id": "fc1e3a02-a478-46d1-8735-f3e693109b52",
  580. "name": "Code"
  581. },
  582. {
  583. "parameters": {
  584. "authentication": "serviceAccount",
  585. "documentId": {
  586. "__rl": true,
  587. "value": "<insert_google_sheet_url_here>",
  588. "mode": "url"
  589. },
  590. "sheetName": {
  591. "__rl": true,
  592. "value": 629353756,
  593. "mode": "list",
  594. "cachedResultName": "Plexbot Chat",
  595. "cachedResultUrl": "<insert_google_sheet_url_here>"
  596. },
  597. "options": {
  598. "dataLocationOnSheet": {
  599. "values": {
  600. "rangeDefinition": "detectAutomatically"
  601. }
  602. }
  603. }
  604. },
  605. "type": "n8n-nodes-base.googleSheets",
  606. "typeVersion": 4.5,
  607. "position": [
  608. -560,
  609. -1440
  610. ],
  611. "id": "411ed19a-5a41-497b-9bf0-be9d74ea4c82",
  612. "name": "Last Mood",
  613. "credentials": {
  614. "googleApi": {
  615. "id": "<insert_google_credentials_name_here>",
  616. "name": "Google Service Account account"
  617. }
  618. }
  619. },
  620. {
  621. "parameters": {
  622. "jsCode": "// Get all input items\nconst items = $input.all();\n\n// Assume the latest (most recent) is the one with the highest row_number\nitems.sort((a, b) => b.json.row_number - a.json.row_number);\n\nconst latest = items[0]; // Latest row (biggest row_number)\n\nreturn [\n {\n json: {\n lastMood: latest.json.mood,\n lastRemarks: latest.json.remarks\n }\n }\n];"
  623. },
  624. "type": "n8n-nodes-base.code",
  625. "typeVersion": 2,
  626. "position": [
  627. 300,
  628. -1440
  629. ],
  630. "id": "e629eef6-2935-409a-acb4-d58bda6dc75a",
  631. "name": "Code1"
  632. },
  633. {
  634. "parameters": {
  635. "jsCode": "// --------------------------------------------------------------\n// Collect every incoming item\n// --------------------------------------------------------------\nconst items = $input.all();\n\n// ---------- helper type guards --------------------------------\nconst isMoodBlob = j => j.mood && j.toneInstructions;\n\n// treat ANY row that has either totalPlays or thisTotalPlays\n// as a \u201cweek row\u201d (first = this week, second = last week)\nconst isWeekRow = j =>\n j.totalPlays !== undefined || j.thisTotalPlays !== undefined;\n\nconst isUpcoming = j =>\n j.title &&\n (j.digitalRelease || j.originallyAvailableAt) &&\n (j.starRating || j.audienceRating !== undefined);\n\n// ---------- locate week rows ----------------------------------\nconst weekRows = items.filter(i => isWeekRow(i.json));\nconst thisWeek = weekRows[0]?.json || {};\nconst lastWeek = weekRows[1]?.json || {};\n\n// ---------- mood blob (optional) ------------------------------\nconst moodBlob = items.find(i => isMoodBlob(i.json))?.json || {};\n\n// ---------- loose upcoming movie objects ----------------------\nconst looseUpcoming = items\n .filter(i => isUpcoming(i.json))\n .map(i => {\n const j = i.json;\n\n // normalise rating \u2192 numeric (0\u20115 scale) & keep display string\n let numRating = null;\n let display = j.starRating;\n\n if (display) {\n numRating = parseFloat(display); // e.g. \"3.7/5\" \u2192 3.7\n } else if (j.audienceRating !== undefined) { // 0\u201110 \u2192 0\u20115\n numRating = (j.audienceRating / 10) * 5;\n display = (Math.round(numRating * 10) / 10).toFixed(1) + '/5';\n }\n\n return {\n title: j.title,\n starRating: display,\n numeric: numRating ?? 0,\n digitalRelease: j.digitalRelease || j.originallyAvailableAt || '',\n };\n });\n\n// merge with any embedded array already present on thisWeek\nconst embeddedUpcoming = (Array.isArray(thisWeek.upcomingMovies)\n ? thisWeek.upcomingMovies\n : []\n).map(u => ({\n ...u,\n numeric: parseFloat(u.starRating) || 0, // add numeric helper field\n}));\n\nconst allUpcoming = [\n ...embeddedUpcoming,\n ...looseUpcoming.filter(u =>\n !embeddedUpcoming.some(e =>\n e.title === u.title && e.digitalRelease === u.digitalRelease)),\n];\n\n// ---------- keep only the BEST upcoming movie -----------------\nallUpcoming.sort((a, b) => b.numeric - a.numeric);\nconst upcomingMovies = allUpcoming.length ? [allUpcoming[0]] : [];\n\n// ---------- keep only BEST recent movie too (1 item) ----------\nif (Array.isArray(thisWeek.recentMovies) && thisWeek.recentMovies.length) {\n thisWeek.recentMovies.sort((a, b) => (b.rating || 0) - (a.rating || 0));\n thisWeek.recentMovies = [thisWeek.recentMovies[0]];\n}\n\n// ---------- build tidy object ---------------------------------\nreturn [\n {\n json: {\n /* this week */\n thisMood: moodBlob.mood || '',\n thisToneInstructions: moodBlob.toneInstructions || '',\n\n thisTotalPlays: thisWeek.totalPlays ?? thisWeek.thisTotalPlays ?? null,\n thisTotalAdded: thisWeek.totalAdded ?? thisWeek.thisTotalAdded ?? thisWeek.newTitles ?? null,\n thisPeakStreams: thisWeek.peakStreams ?? thisWeek.thisPeakStreams ?? null,\n\n thisTopUser: thisWeek.topUser ?? thisWeek.thisTopUser ?? '',\n thisTopUserPlays: thisWeek.topUserPlays ?? thisWeek.thisTopUserPlays?? null,\n thisTopMovie: thisWeek.topMovie ?? thisWeek.thisTopMovie ?? '',\n thisTopShow: thisWeek.topShow ?? thisWeek.thisTopShow ?? '',\n\n recentMovies: thisWeek.recentMovies ?? [],\n recentTV: thisWeek.recentTV ?? [],\n recentAnime: thisWeek.recentAnime ?? [],\n\n upcomingMovies, // single best one\n thisRunDate: thisWeek.runDate ?? thisWeek.thisRunDate ?? null,\n\n /* last week */\n lastMood: lastWeek.lastMood ?? '',\n lastRemarks: lastWeek.lastRemarks ?? '',\n lastTotalPlays: lastWeek.totalPlays ?? null,\n lastNewTitles: lastWeek.newTitles ?? null,\n lastPeakStreams: lastWeek.peakStreams ?? null,\n },\n },\n];"
  636. },
  637. "type": "n8n-nodes-base.code",
  638. "typeVersion": 2,
  639. "position": [
  640. 960,
  641. -1260
  642. ],
  643. "id": "900a8648-3130-47ab-8f43-4b8c44ea62c0",
  644. "name": "Code2"
  645. },
  646. {
  647. "parameters": {
  648. "mode": "combine",
  649. "combineBy": "combineAll",
  650. "options": {}
  651. },
  652. "type": "n8n-nodes-base.merge",
  653. "typeVersion": 3.1,
  654. "position": [
  655. 1356,
  656. -930
  657. ],
  658. "id": "0968ce6f-a3eb-46f7-a7c5-d2ca35f093fa",
  659. "name": "Merge2"
  660. },
  661. {
  662. "parameters": {
  663. "jsCode": "// Assume input is array of rows\nconst rows = $input.all();\n\n// Sort by row_number descending\nrows.sort((a, b) => (b.json.row_number || 0) - (a.json.row_number || 0));\n\n// Return the top (highest row_number) row\nreturn [\n {\n json: rows[0].json\n }\n];"
  664. },
  665. "type": "n8n-nodes-base.code",
  666. "typeVersion": 2,
  667. "position": [
  668. -340,
  669. -1440
  670. ],
  671. "id": "fd915484-7c40-4c67-a49f-d026ad67c0d6",
  672. "name": "Code3"
  673. },
  674. {
  675. "parameters": {
  676. "jsCode": "// Assume input is array of rows\nconst rows = $input.all();\n\n// Sort by row_number descending\nrows.sort((a, b) => (b.json.row_number || 0) - (a.json.row_number || 0));\n\n// Return the top (highest row_number) row\nreturn [\n {\n json: rows[0].json\n }\n];"
  677. },
  678. "type": "n8n-nodes-base.code",
  679. "typeVersion": 2,
  680. "position": [
  681. 120,
  682. -640
  683. ],
  684. "id": "78dbca90-f517-4220-a20c-1504c699894a",
  685. "name": "Code4"
  686. },
  687. {
  688. "parameters": {
  689. "jsCode": "// This lives right after your trigger\nconst data = $getWorkflowStaticData('global'); // survives restarts / redeploys\ndata.runCount = (data.runCount ?? 0) + 1; // increment\nreturn [{ json: { runCount: data.runCount } }];"
  690. },
  691. "type": "n8n-nodes-base.code",
  692. "typeVersion": 2,
  693. "position": [
  694. 1560,
  695. -340
  696. ],
  697. "id": "62e8a921-32eb-4268-b13f-72633c42d611",
  698. "name": "Code5"
  699. },
  700. {
  701. "parameters": {
  702. "conditions": {
  703. "options": {
  704. "caseSensitive": true,
  705. "leftValue": "",
  706. "typeValidation": "strict",
  707. "version": 2
  708. },
  709. "conditions": [
  710. {
  711. "id": "5d4f28e8-76c1-4a3c-b902-95ce058df343",
  712. "leftValue": "={{ $json.runCount % 4 }}",
  713. "rightValue": 0,
  714. "operator": {
  715. "type": "number",
  716. "operation": "equals"
  717. }
  718. }
  719. ],
  720. "combinator": "and"
  721. },
  722. "options": {}
  723. },
  724. "type": "n8n-nodes-base.if",
  725. "typeVersion": 2.2,
  726. "position": [
  727. 1740,
  728. -260
  729. ],
  730. "id": "41ee04a7-4460-4031-84f1-3c0eb077e3ba",
  731. "name": "If"
  732. },
  733. {
  734. "parameters": {
  735. "jsCode": "/**\n * Radarr \u2192 Top\u20115 digital releases THIS week\n */\n\nconst RADARR_URL = '<insert_radarr_url_here>';\nconst API_KEY = '<insert_radarr_api_key_here>';\n\n/* helpers */\nconst iso = d => d.toISOString().split('T')[0];\nconst stars = v => `${(Math.round(v * 5) / 10).toFixed(1)}/5`;\n\n/* Monday 00:00 \u279c Sunday 23:59 window */\nconst start = new Date(); start.setUTCHours(0,0,0,0);\nconst end = new Date(start); end.setUTCDate(start.getUTCDate() + 7);\n\n/* pull Radarr calendar */\nconst movies = await this.helpers.request({\n method: 'GET',\n url: `${RADARR_URL}/api/v3/calendar`,\n qs: { start: iso(start), end: iso(end), unmonitored: 'false' },\n headers: { 'X-Api-Key': API_KEY, Accept: 'application/json' },\n json: true,\n});\n\n/* filter STRICTLY on digitalRelease within window */\nconst upcoming = movies.filter(m => {\n if (!m.digitalRelease) return false; // skip if null/undefined\n const d = new Date(m.digitalRelease);\n return d >= start && d <= end; // inside 7\u2011day window\n});\n\n/* sort + pick five */\nupcoming.sort((a, b) =>\n new Date(a.digitalRelease) - new Date(b.digitalRelease));\n\nconst top5 = upcoming.slice(0, 5).map(m => ({\n json: {\n title: m.title,\n starRating: stars(\n m.ratings?.tmdb?.value ??\n m.ratings?.imdb?.value ??\n m.voteAverage ??\n m.vote_average ??\n 0\n ),\n digitalRelease: iso(new Date(m.digitalRelease)),\n },\n}));\n\nreturn top5;"
  736. },
  737. "type": "n8n-nodes-base.code",
  738. "typeVersion": 2,
  739. "position": [
  740. -660,
  741. -420
  742. ],
  743. "id": "67fb7dd9-ba05-48cf-bc8c-d0d82a8d94ff",
  744. "name": "Get Upcoming Movies"
  745. },
  746. {
  747. "parameters": {
  748. "amount": 2
  749. },
  750. "type": "n8n-nodes-base.wait",
  751. "typeVersion": 1.1,
  752. "position": [
  753. 1540,
  754. -540
  755. ],
  756. "id": "79802153-e5dc-4d40-8c6f-48f155935cb0",
  757. "name": "Wait",
  758. "webhookId": "a26c2a29-9143-41eb-b65b-498b02593605"
  759. }
  760. ],
  761. "pinData": {
  762. "TMDB Rating": [
  763. {
  764. "json": {
  765. "air_date": "2025-04-15",
  766. "crew": [],
  767. "episode_number": 1,
  768. "guest_stars": [],
  769. "name": "The Gospel of Kenny Sharp",
  770. "overview": "On the heels of his return home, Hampton reckons with all the changes that took place during his long absence.",
  771. "id": 5814847,
  772. "production_code": "",
  773. "runtime": 43,
  774. "season_number": 1,
  775. "still_path": "/saWyMiRAPkHVFBeOdtqqFdTybDW.jpg",
  776. "vote_average": 7,
  777. "vote_count": 0
  778. }
  779. }
  780. ]
  781. },
  782. "connections": {
  783. "get_home_stats": {
  784. "main": [
  785. [
  786. {
  787. "node": "Merge",
  788. "type": "main",
  789. "index": 1
  790. }
  791. ]
  792. ]
  793. },
  794. "Schedule Trigger": {
  795. "main": [
  796. [
  797. {
  798. "node": "get_home_stats",
  799. "type": "main",
  800. "index": 0
  801. },
  802. {
  803. "node": "Get Libraries",
  804. "type": "main",
  805. "index": 0
  806. },
  807. {
  808. "node": "Get Recent Adds",
  809. "type": "main",
  810. "index": 0
  811. },
  812. {
  813. "node": "Last Mood",
  814. "type": "main",
  815. "index": 0
  816. },
  817. {
  818. "node": "Get Last 2 Weeks",
  819. "type": "main",
  820. "index": 0
  821. },
  822. {
  823. "node": "Set Plexbot Mood",
  824. "type": "main",
  825. "index": 0
  826. },
  827. {
  828. "node": "Get Upcoming Movies",
  829. "type": "main",
  830. "index": 0
  831. }
  832. ]
  833. ]
  834. },
  835. "Merge": {
  836. "main": [
  837. [
  838. {
  839. "node": "Define Data",
  840. "type": "main",
  841. "index": 0
  842. },
  843. {
  844. "node": "Write Data",
  845. "type": "main",
  846. "index": 0
  847. }
  848. ]
  849. ]
  850. },
  851. "Get Libraries": {
  852. "main": [
  853. [
  854. {
  855. "node": "Merge",
  856. "type": "main",
  857. "index": 0
  858. }
  859. ]
  860. ]
  861. },
  862. "Get Recent Adds": {
  863. "main": [
  864. [
  865. {
  866. "node": "Merge",
  867. "type": "main",
  868. "index": 2
  869. }
  870. ]
  871. ]
  872. },
  873. "OpenAI": {
  874. "main": [
  875. [
  876. {
  877. "node": "Merge1",
  878. "type": "main",
  879. "index": 0
  880. },
  881. {
  882. "node": "Merge2",
  883. "type": "main",
  884. "index": 1
  885. }
  886. ]
  887. ]
  888. },
  889. "Merge1": {
  890. "main": [
  891. [
  892. {
  893. "node": "Support Plex",
  894. "type": "main",
  895. "index": 0
  896. },
  897. {
  898. "node": "Wait",
  899. "type": "main",
  900. "index": 0
  901. }
  902. ]
  903. ]
  904. },
  905. "Get Last 2 Weeks": {
  906. "main": [
  907. [
  908. {
  909. "node": "Code4",
  910. "type": "main",
  911. "index": 0
  912. }
  913. ]
  914. ]
  915. },
  916. "Merge History": {
  917. "main": [
  918. [
  919. {
  920. "node": "Code2",
  921. "type": "main",
  922. "index": 0
  923. }
  924. ]
  925. ]
  926. },
  927. "Define Data": {
  928. "main": [
  929. [
  930. {
  931. "node": "Merge History",
  932. "type": "main",
  933. "index": 0
  934. },
  935. {
  936. "node": "Add to Log",
  937. "type": "main",
  938. "index": 0
  939. }
  940. ]
  941. ]
  942. },
  943. "Write Data": {
  944. "main": [
  945. [
  946. {
  947. "node": "Merge1",
  948. "type": "main",
  949. "index": 1
  950. }
  951. ]
  952. ]
  953. },
  954. "Combine Message": {
  955. "main": [
  956. [
  957. {
  958. "node": "Send Weekly Digest",
  959. "type": "main",
  960. "index": 0
  961. }
  962. ]
  963. ]
  964. },
  965. "Send Weekly Digest": {
  966. "main": [
  967. []
  968. ]
  969. },
  970. "Set Plexbot Mood": {
  971. "main": [
  972. [
  973. {
  974. "node": "Merge History",
  975. "type": "main",
  976. "index": 2
  977. }
  978. ]
  979. ]
  980. },
  981. "Support Plex": {
  982. "main": [
  983. [
  984. {
  985. "node": "Send Weekly Digest",
  986. "type": "main",
  987. "index": 0
  988. }
  989. ]
  990. ]
  991. },
  992. "Code": {
  993. "main": [
  994. [
  995. {
  996. "node": "Google Sheets",
  997. "type": "main",
  998. "index": 0
  999. }
  1000. ]
  1001. ]
  1002. },
  1003. "Google Sheets": {
  1004. "main": [
  1005. []
  1006. ]
  1007. },
  1008. "Last Mood": {
  1009. "main": [
  1010. [
  1011. {
  1012. "node": "Code1",
  1013. "type": "main",
  1014. "index": 0
  1015. },
  1016. {
  1017. "node": "Code3",
  1018. "type": "main",
  1019. "index": 0
  1020. }
  1021. ]
  1022. ]
  1023. },
  1024. "Code1": {
  1025. "main": [
  1026. [
  1027. {
  1028. "node": "Merge History",
  1029. "type": "main",
  1030. "index": 3
  1031. }
  1032. ]
  1033. ]
  1034. },
  1035. "Code2": {
  1036. "main": [
  1037. [
  1038. {
  1039. "node": "OpenAI",
  1040. "type": "main",
  1041. "index": 0
  1042. },
  1043. {
  1044. "node": "Merge2",
  1045. "type": "main",
  1046. "index": 0
  1047. }
  1048. ]
  1049. ]
  1050. },
  1051. "Merge2": {
  1052. "main": [
  1053. [
  1054. {
  1055. "node": "Code",
  1056. "type": "main",
  1057. "index": 0
  1058. }
  1059. ]
  1060. ]
  1061. },
  1062. "Code4": {
  1063. "main": [
  1064. [
  1065. {
  1066. "node": "Merge History",
  1067. "type": "main",
  1068. "index": 1
  1069. }
  1070. ]
  1071. ]
  1072. },
  1073. "Code5": {
  1074. "main": [
  1075. [
  1076. {
  1077. "node": "If",
  1078. "type": "main",
  1079. "index": 0
  1080. }
  1081. ]
  1082. ]
  1083. },
  1084. "If": {
  1085. "main": [
  1086. []
  1087. ]
  1088. },
  1089. "Get Upcoming Movies": {
  1090. "main": [
  1091. [
  1092. {
  1093. "node": "Merge",
  1094. "type": "main",
  1095. "index": 3
  1096. }
  1097. ]
  1098. ]
  1099. },
  1100. "Wait": {
  1101. "main": [
  1102. [
  1103. {
  1104. "node": "Combine Message",
  1105. "type": "main",
  1106. "index": 0
  1107. }
  1108. ]
  1109. ]
  1110. }
  1111. },
  1112. "active": true,
  1113. "settings": {
  1114. "executionOrder": "v1"
  1115. },
  1116. "versionId": "1eb2d9cb-422c-4a09-8630-22c3efb675c2",
  1117. "meta": {
  1118. "templateCredsSetupCompleted": true,
  1119. "instanceId": "08dd23ba3e955e2d1e3e10cc8c30710fad007e289014f7b7907bdf42c0d9b69b"
  1120. },
  1121. "id": "7NwsxAJelj3yz0Ig",
  1122. "tags": []
  1123. }
Advertisement
Add Comment
Please, Sign In to add comment