├── package.json ├── LICENSE ├── README.md └── index.js /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "requester", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "axios": "^1.11.0", 14 | "dotenv": "^17.2.1", 15 | "qrcode-terminal": "^0.12.0", 16 | "whatsapp-web.js": "^1.32.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Shahid Akram 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # JellyseerrWhatsAppRequester 2 | A Bot for requesting media via WhatsApp and adding requests to Jellyseerr. 3 | 4 | # 🎬 WhatsApp Jellyseerr Request Bot 5 | 6 | This is a **WhatsApp bot** that lets you search and request **movies or TV series** directly from WhatsApp. 7 | It integrates with [Jellyseerr](https://github.com/Fallenbagel/jellyseerr) so you and your friends can easily request media. 8 | 9 | --- 10 | 11 | ## ✨ Features 12 | - 🔎 Search for Movies or TV Series by name 13 | - 🎥 Provides **IMDb / TVDb links** to confirm the result 14 | - 📩 Request Movies or Full Series (all seasons automatically) 15 | - ✅ Requests appear in Jellyseerr for approval (unless you use an admin API key) 16 | - 🗂 Lightweight and easy to run (Node.js + whatsapp-web.js) 17 | 18 | --- 19 | 20 | ## 📦 Requirements 21 | - Node.js (>= 18.x) 22 | - Jellyseerr server running and accessible 23 | - WhatsApp account for the bot 24 | 25 | --- 26 | 27 | ## ⚙️ Setup & Installation 28 | 29 | 1. Clone this repo: 30 | ```bash 31 | git clone https://github.com/drlovesan/JellyseerrWhatsAppRequester.git 32 | cd JellyseerrWhatsAppRequester 33 | 34 | 35 | Install dependencies: 36 | 37 | npm install 38 | 39 | 40 | Edit the index.js file and update values with your actual info: 41 | 42 | const JELLYSEERR_URL = 'http://localhost:5055'; 43 | 44 | const API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; 45 | 46 | 47 | ⚠️ Important: 48 | 49 | If you use an Admin API Key, requests are auto-approved. 50 | 51 | To require approval, create a normal Jellyseerr user and use that API key here. 52 | 53 | Start the bot: 54 | 55 | node bot.js 56 | 57 | Scan the QR code with your WhatsApp to connect. 58 | 59 | 💬 Usage 60 | 61 | Request a movie: 62 | 63 | !request movie Inception 64 | 65 | 66 | Request a series (all seasons will be requested): 67 | 68 | !request series Breaking Bad 69 | 70 | 71 | Example reply: 72 | 73 | Found: Breaking Bad (2008) 74 | IMDb: https://www.imdb.com/title/tt0903747/ 75 | 76 | ✅ Request sent to Jellyseerr 77 | 78 | 🚀 Roadmap 79 | 80 | Add download status feedback 81 | 82 | Add Jellyfin integration for request tracking 83 | 84 | Multi-user role-based control 85 | 86 | Developed fully using CHATGPT by Shahid Akram 87 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // index.js 2 | require('dotenv').config(); 3 | const { Client, LocalAuth } = require('whatsapp-web.js'); 4 | const qrcode = require('qrcode-terminal'); 5 | const axios = require('axios'); 6 | 7 | // ===== CONFIG ===== 8 | const JELLYSEERR_URL = 'http://localhost:5055'; 9 | const API_KEY = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'; 10 | 11 | // To store ongoing search sessions { userId: { results: [], type: 'movie'|'tv' } } 12 | let pendingSelections = {}; 13 | 14 | const client = new Client({ 15 | authStrategy: new LocalAuth(), 16 | puppeteer: { headless: true } 17 | }); 18 | 19 | // Show QR in terminal 20 | client.on('qr', qr => { 21 | qrcode.generate(qr, { small: true }); 22 | console.log('Scan this QR with your WhatsApp'); 23 | }); 24 | 25 | client.on('ready', () => { 26 | console.log('✅ WhatsApp bot is ready!'); 27 | }); 28 | 29 | // Main message handler 30 | client.on('message', async msg => { 31 | const chat = await msg.getChat(); 32 | if (chat.isGroup) return; 33 | 34 | const senderId = msg.from; 35 | const text = msg.body.trim(); 36 | 37 | // Handle pending selection 38 | if (pendingSelections[senderId] && /^[0-9]+$/.test(text)) { 39 | let selectionIndex = parseInt(text) - 1; 40 | let session = pendingSelections[senderId]; 41 | 42 | if (selectionIndex >= 0 && selectionIndex < session.results.length) { 43 | let item = session.results[selectionIndex]; 44 | let mediaType = session.type; 45 | let mediaId = item.id; 46 | 47 | try { 48 | await requestMedia(mediaId, mediaType, item); 49 | await msg.reply(`✅ "${item.title || item.name}" has been requested successfully!`); 50 | } catch (err) { 51 | await msg.reply(`❌ Failed to request "${item.title || item.name}".`); 52 | console.error(err.response?.data || err); 53 | } 54 | 55 | delete pendingSelections[senderId]; 56 | } else { 57 | await msg.reply(`⚠️ Invalid selection. Please enter a number from the list.`); 58 | } 59 | return; 60 | } 61 | 62 | // Command parsing 63 | if (text.toLowerCase().startsWith('!request')) { 64 | let type = 'movie'; // default 65 | let searchTerm = text.replace(/^!request\s*/i, ''); 66 | 67 | if (searchTerm.toLowerCase().startsWith('movie ')) { 68 | type = 'movie'; 69 | searchTerm = searchTerm.slice(6); 70 | } else if (searchTerm.toLowerCase().startsWith('series ')) { 71 | type = 'tv'; 72 | searchTerm = searchTerm.slice(7); 73 | } 74 | 75 | if (!searchTerm.trim()) { 76 | await msg.reply(`⚠️ Please provide a search term. Example:\n!request movie ironman`); 77 | return; 78 | } 79 | 80 | try { 81 | let results = await searchJellyseerr(searchTerm.trim(), type); 82 | if (results.length === 0) { 83 | await msg.reply(`No ${type === 'movie' ? 'movies' : 'series'} found for "${searchTerm}"`); 84 | return; 85 | } 86 | 87 | // Save session 88 | pendingSelections[senderId] = { results, type }; 89 | 90 | // Build response with IMDb/TVDB links 91 | let responseText = `🔍 Found ${results.length} ${type === 'movie' ? 'movies' : 'series'} for "${searchTerm}":\n\n`; 92 | results.forEach((item, idx) => { 93 | let year = item.releaseDate ? item.releaseDate.split('-')[0] : (item.firstAirDate ? item.firstAirDate.split('-')[0] : 'N/A'); 94 | let cast = item.mediaInfo?.credits?.cast?.slice(0, 3).map(c => c.name).join(', ') || 'Unknown cast'; 95 | let overview = item.overview ? (item.overview.length > 100 ? item.overview.slice(0, 100) + '...' : item.overview) : 'No description'; 96 | 97 | // Links 98 | let imdbLink = item.imdbId ? `https://www.imdb.com/title/${item.imdbId}/` : ''; 99 | let tvdbLink = item.tvdbId ? `https://thetvdb.com/?id=${item.tvdbId}` : ''; 100 | let linkText = imdbLink || tvdbLink ? `🔗 ${imdbLink || tvdbLink}` : ''; 101 | 102 | responseText += `${idx + 1}. *${item.title || item.name}* (${year})\n 🎭 ${cast}\n 📜 ${overview}\n ${linkText}\n\n`; 103 | }); 104 | responseText += `Reply with the number (1-${results.length}) to request.`; 105 | 106 | await msg.reply(responseText); 107 | } catch (err) { 108 | console.error(err.response?.data || err); 109 | await msg.reply(`❌ Error searching Jellyseerr.`); 110 | } 111 | } 112 | }); 113 | 114 | // ===== FUNCTIONS ===== 115 | async function searchJellyseerr(query, type) { 116 | const url = `${JELLYSEERR_URL}/api/v1/search?query=${encodeURIComponent(query)}&page=1`; 117 | const res = await axios.get(url, { headers: { 'X-Api-Key': API_KEY } }); 118 | let results = res.data.results.filter(r => r.mediaType === type); 119 | 120 | // Fetch more details for cast + external IDs 121 | for (let item of results) { 122 | try { 123 | const detailsUrl = `${JELLYSEERR_URL}/api/v1/${type}/${item.id}`; 124 | const detailsRes = await axios.get(detailsUrl, { headers: { 'X-Api-Key': API_KEY } }); 125 | item.mediaInfo = detailsRes.data; 126 | item.imdbId = detailsRes.data.externalIds?.imdbId; 127 | item.tvdbId = detailsRes.data.externalIds?.tvdbId; 128 | } catch {} 129 | } 130 | return results; 131 | } 132 | 133 | async function requestMedia(mediaId, type, item) { 134 | const url = `${JELLYSEERR_URL}/api/v1/request`; 135 | 136 | let payload = { mediaType: type, mediaId: mediaId }; 137 | 138 | // If TV series, request all seasons 139 | if (type === 'tv' && item?.mediaInfo?.seasons) { 140 | payload.seasons = item.mediaInfo.seasons.map(s => s.seasonNumber).filter(n => n > 0); // skip specials 141 | } 142 | 143 | await axios.post(url, payload, { headers: { 'X-Api-Key': API_KEY } }); 144 | } 145 | 146 | // Start bot 147 | client.initialize(); 148 | --------------------------------------------------------------------------------