├── README.md └── api ├── handler └── handler.js ├── index.js ├── package.json ├── routes ├── api.js └── routes.js └── src ├── lib ├── getBatch.js ├── mapGenres.js ├── pagination.js ├── scapeOngoingAnime.js ├── scrapeAnimeByGenre.js ├── scrapeAnimeEpisodes.js ├── scrapeBatch.js ├── scrapeCompleteAnime.js ├── scrapeEpisode.js ├── scrapeGenreLists.js ├── scrapeSearchResult.js └── scrapeSingleAnime.js ├── otakudesu.js ├── types └── types.js └── utils ├── anime.js ├── animeByGenre.js ├── batch.js ├── completeAnime.js ├── episode.js ├── episodes.js ├── genreLists.js ├── home.js ├── movie.js ├── movies.js ├── ongoingAnime.js └── search.js /README.md: -------------------------------------------------------------------------------- 1 | # 🎌 KitaNime - Anime Streaming Platform 2 | 3 | ![Node.js](https://img.shields.io/badge/Node.js-339933?style=for-the-badge&logo=nodedotjs&logoColor=white) ![Express.js](https://img.shields.io/badge/Express.js-000000?style=for-the-badge&logo=express&logoColor=white) ![Pug](https://img.shields.io/badge/Pug-A86454?style=for-the-badge&logo=pug&logoColor=white) ![SQLite](https://img.shields.io/badge/SQLite-07405E?style=for-the-badge&logo=sqlite&logoColor=white) ![Tailwind CSS](https://img.shields.io/badge/Tailwind_CSS-38B2AC?style=for-the-badge&logo=tailwind-css&logoColor=white) ![Axios](https://img.shields.io/badge/Axios-5A29E4?style=for-the-badge&logo=axios&logoColor=white) 4 | 5 | > KitaNime adalah platform streaming anime modern dengan subtitle Indonesia yang menyediakan ribuan anime berkualitas tinggi. Dilengkapi dengan sistem scraping otomatis, admin panel yang powerful, dan antarmuka yang responsif untuk pengalaman menonton anime terbaik. 6 | 7 | Repository page v2: [![KitaNime-v2](https://d5studio.my.id/images/favicon.ico)](https://github.com/IkuzaDev/kitanimev2) 8 | 9 | ## ✨ Fitur Utama 10 | 11 | 🎬 **Streaming Anime** 12 | - Ribuan anime dengan subtitle Indonesia 13 | - Kualitas video HD (480p, 720p, 1080p) 14 | - Player video yang responsif dengan kontrol lengkap 15 | - Autoplay Next Episode & Resume video pada episode berikutnya 16 | 17 | 📱 **Antarmuka Modern** 18 | - Desain responsif untuk semua perangkat 19 | - Navigasi yang intuitif dan user-friendly 20 | - Search dan filter anime yang canggih 21 | 22 | 🔄 **Sistem Scraping** 23 | - Scraping otomatis dari sumber terpercaya 24 | - Update anime ongoing secara real-time 25 | - Manajemen episode dan batch download 26 | 27 | ⚙️ **Admin Panel** 28 | - Dashboard admin yang komprehensif 29 | - Manajemen API endpoints 30 | - Sistem iklan terintegrasi 31 | - Monitoring dan analytics 32 | 33 | ## 🖼️ Preview Aplikasi 34 | 35 | > Preview dari KitaNime Anime Streaming Platform 36 | 37 | ![Homepage KitaNime](https://raw.githubusercontent.com/IkuzaDev/kitanimev2/refs/heads/main/public/images/preview-home.png) 38 | ![Anime Detail](https://raw.githubusercontent.com/IkuzaDev/kitanimev2/refs/heads/main/public/images/preview-detail.png) 39 | ![Video Player](https://raw.githubusercontent.com/IkuzaDev/kitanimev2/refs/heads/main/public/images/preview-player.png) 40 | ![Admin Dashboard](https://raw.githubusercontent.com/IkuzaDev/kitanimev2/refs/heads/main/public/images/preview-admin.png) 41 | 42 | ## 🧩 Teknologi yang Digunakan 43 | 44 | ### Frontend 45 | - **View Engine**: Pug templating untuk rendering HTML yang elegan 46 | - **Styling**: Tailwind CSS untuk desain yang modern dan responsif 47 | - **JavaScript**: Vanilla JS dengan Plyr untuk video player 48 | - **Icons**: SVG icons dan custom graphics 49 | 50 | ### Backend 51 | - **Server**: Node.js + Express.js untuk performa tinggi 52 | - **Database**: SQLite3 untuk penyimpanan data yang efisien 53 | - **Authentication**: bcrypt untuk keamanan password 54 | - **Session**: Express-session untuk manajemen user 55 | - **Security**: Helmet.js untuk keamanan HTTP headers 56 | 57 | ### API & Scraping 58 | - **HTTP Client**: Axios untuk API requests 59 | - **Web Scraping**: Cheerio untuk parsing HTML 60 | - **Data Processing**: Custom utilities untuk data transformation 61 | - **Caching**: Built-in response caching system 62 | 63 | ## 🏗️ Arsitektur Sistem 64 | 65 | KitaNime terdiri dari dua komponen utama: 66 | 67 | ### 📺 Frontend Application (`/page`) 68 | - Web interface untuk user 69 | - Admin panel untuk manajemen 70 | - Video streaming dan player 71 | - Search dan navigation system 72 | 73 | ### 🔌 API Service (`/api`) 74 | - RESTful API untuk data anime 75 | - Web scraping service 76 | - Data processing dan caching 77 | - External API integration 78 | 79 | ## 🚀 Cara Instalasi 80 | 81 | ### Prerequisites 82 | - Node.js (v16 atau lebih baru) 83 | - npm atau yarn 84 | - Git 85 | 86 | ### 1. Clone Repository 87 | ```bash 88 | git clone https://github.com/IkuzaDev/kitanime.git 89 | cd kitanime 90 | ``` 91 | 92 | ### 2. Setup API Service 93 | ```bash 94 | cd api 95 | npm install 96 | npm run dev 97 | ``` 98 | API akan berjalan di `http://localhost:3000` 99 | 100 | ### 3. Setup Frontend Application 101 | ```bash 102 | cd ../page 103 | npm install 104 | npm start 105 | ``` 106 | Web application akan berjalan di `http://localhost:3001` 107 | 108 | ### 4. Konfigurasi Database 109 | Database SQLite akan otomatis dibuat saat pertama kali menjalankan aplikasi. 110 | 111 | ## 🔑 Default Login 112 | 113 | | Role | Username | Password | 114 | |------|----------|----------| 115 | | Admin | admin | admin123 | 116 | 117 | ## 📂 Struktur Project 118 | 119 | ``` 120 | kitanime/ 121 | ├── api/ # API Service 122 | │ ├── src/ 123 | │ │ ├── lib/ # Core scraping libraries 124 | │ │ ├── utils/ # Utility functions 125 | │ │ └── types/ # Type definitions 126 | │ ├── routes/ # API routes 127 | │ ├── handler/ # Request handlers 128 | │ └── index.js # API entry point 129 | │ 130 | ├── page/ # Frontend Application 131 | │ ├── routes/ # Express routes 132 | │ │ ├── index.js # Home routes 133 | │ │ ├── anime.js # Anime routes 134 | │ │ ├── admin.js # Admin routes 135 | │ │ └── api.js # API proxy routes 136 | │ ├── views/ # Pug templates 137 | │ │ ├── admin/ # Admin panel views 138 | │ │ ├── layout.pug # Main layout 139 | │ │ ├── index.pug # Homepage 140 | │ │ ├── anime-detail.pug 141 | │ │ ├── episode-player.pug 142 | │ │ └── ... 143 | │ ├── models/ # Database models 144 | │ │ └── database.js # SQLite configuration 145 | │ ├── middleware/ # Express middleware 146 | │ │ ├── adSlots.js # Ad management 147 | │ │ └── cookieConsent.js 148 | │ ├── services/ # Business logic 149 | │ │ └── animeApi.js # API service client 150 | │ ├── public/ # Static assets 151 | │ │ ├── css/ # Stylesheets 152 | │ │ ├── js/ # Client-side scripts 153 | │ │ └── images/ # Images and media 154 | │ ├── data/ # Database files 155 | │ │ └── kitanime.db # SQLite database 156 | │ └── app.js # Main application 157 | │ 158 | └── README.md # Project documentation 159 | ``` 160 | 161 | ## 🗺️ Routes & API Endpoints 162 | 163 | ### 🏠 Frontend Routes 164 | 165 | **Public Routes** 166 | - `GET /` - Homepage dengan anime ongoing dan complete 167 | - `GET /ongoing` - Daftar anime ongoing dengan pagination 168 | - `GET /complete` - Daftar anime complete dengan pagination 169 | - `GET /genres` - Daftar semua genre anime 170 | - `GET /genres/:slug` - Anime berdasarkan genre 171 | - `GET /search` - Pencarian anime 172 | - `GET /movies` - Daftar anime movie 173 | 174 | **Anime Routes** 175 | - `GET /anime/:slug` - Detail anime 176 | - `GET /anime/:slug/episodes` - Daftar episode anime 177 | - `GET /anime/:slug/episode/:episode` - Player episode 178 | - `GET /anime/:slug/batch` - Download batch links 179 | 180 | **Movie Routes** 181 | - `GET /movies/:year/:month/:slug` - Detail movie 182 | - `GET /movies/:year/:month/:slug/watch` - Player movie 183 | 184 | **Admin Routes** 185 | - `GET /admin/login` - Login admin 186 | - `GET /admin/dashboard` - Dashboard admin 187 | - `GET /admin/api-endpoints` - Manajemen API endpoints 188 | - `GET /admin/ad-slots` - Manajemen slot iklan 189 | - `GET /admin/settings` - Pengaturan sistem 190 | 191 | ### 🔌 API Endpoints 192 | 193 | **System Info** 194 | - `GET /v1/` - Informasi sistem dan status API 195 | 196 | **Anime Data** 197 | - `GET /v1/home` - Data homepage (ongoing + complete) 198 | - `GET /v1/ongoing-anime/:page` - Anime ongoing dengan pagination 199 | - `GET /v1/complete-anime/:page` - Anime complete dengan pagination 200 | - `GET /v1/anime/:slug` - Detail anime 201 | - `GET /v1/anime/:slug/episodes` - Daftar episode anime 202 | - `GET /v1/anime/:slug/episodes/:episode` - Detail episode 203 | 204 | **Search & Filter** 205 | - `GET /v1/search/:keyword` - Pencarian anime 206 | - `GET /v1/genres` - Daftar genre 207 | - `GET /v1/genres/:slug/:page` - Anime berdasargi genre 208 | 209 | **Movies** 210 | - `GET /v1/movies/:page` - Daftar movie dengan pagination 211 | - `GET /v1/movies/:year/:month/:slug` - Detail movie 212 | 213 | ## 🎯 Fitur Khusus 214 | 215 | ### 🔄 Auto Scraping System 216 | - Scraping otomatis dari sumber anime terpercaya 217 | - Update data anime ongoing secara berkala 218 | - Caching system untuk performa optimal 219 | - Error handling dan retry mechanism 220 | 221 | ### 📱 Responsive Design 222 | - Mobile-first approach 223 | - Adaptive layout untuk tablet dan desktop 224 | - Touch-friendly navigation 225 | - Optimized untuk berbagai ukuran layar 226 | 227 | ### 🎬 Advanced Video Player 228 | - HTML5 video player dengan Plyr 229 | - Multiple quality options (480p, 720p, 1080p) 230 | - Subtitle support 231 | - Fullscreen dan picture-in-picture mode 232 | - Keyboard shortcuts dan gesture controls 233 | 234 | ### 🔍 Smart Search 235 | - Real-time search suggestions 236 | - Advanced filtering options 237 | - Search by title, genre, year 238 | - Autocomplete dan typo tolerance 239 | 240 | ### 📊 Admin Analytics 241 | - Traffic monitoring 242 | - Popular anime tracking 243 | - User engagement metrics 244 | - System performance monitoring 245 | 246 | ## 🛠️ Konfigurasi 247 | 248 | ### Environment Variables 249 | 250 | **API Service (.env)** 251 | ```env 252 | PORT=3000 253 | NODE_ENV=production 254 | ``` 255 | 256 | **Frontend Application** 257 | ```javascript 258 | // page/config/config.js 259 | module.exports = { 260 | port: process.env.PORT || 3001, 261 | apiBaseUrl: process.env.API_URL || 'http://localhost:3000', 262 | sessionSecret: process.env.SESSION_SECRET || 'kitanime-secret', 263 | database: { 264 | path: './data/kitanime.db' 265 | } 266 | }; 267 | ``` 268 | 269 | ### Database Schema 270 | 271 | **API Endpoints Table** 272 | ```sql 273 | CREATE TABLE api_endpoints ( 274 | id INTEGER PRIMARY KEY AUTOINCREMENT, 275 | name TEXT NOT NULL UNIQUE, 276 | url TEXT NOT NULL, 277 | is_active INTEGER DEFAULT 0, 278 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 279 | ); 280 | ``` 281 | 282 | **Ad Slots Table** 283 | ```sql 284 | CREATE TABLE ad_slots ( 285 | id INTEGER PRIMARY KEY AUTOINCREMENT, 286 | name TEXT NOT NULL, 287 | position TEXT NOT NULL, 288 | content TEXT, 289 | is_active INTEGER DEFAULT 1, 290 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 291 | ); 292 | ``` 293 | 294 | **Admin Users Table** 295 | ```sql 296 | CREATE TABLE admin_users ( 297 | id INTEGER PRIMARY KEY AUTOINCREMENT, 298 | username TEXT UNIQUE NOT NULL, 299 | password TEXT NOT NULL, 300 | email TEXT, 301 | created_at DATETIME DEFAULT CURRENT_TIMESTAMP 302 | ); 303 | ``` 304 | 305 | ## 🔧 Development 306 | 307 | ### Setup Development Environment 308 | 309 | 1. **Install Dependencies** 310 | ```bash 311 | # Install API dependencies 312 | cd api && npm install 313 | 314 | # Install Frontend dependencies 315 | cd ../page && npm install 316 | ``` 317 | 318 | 2. **Run in Development Mode** 319 | ```bash 320 | # Terminal 1 - API Service 321 | cd api 322 | npm run dev 323 | 324 | # Terminal 2 - Frontend Application 325 | cd page 326 | npm run dev 327 | ``` 328 | 329 | 3. **Database Setup** 330 | ```bash 331 | # Database akan otomatis dibuat saat pertama kali menjalankan aplikasi 332 | # Lokasi: page/data/kitanime.db 333 | ``` 334 | 335 | ### Code Style & Standards 336 | 337 | - **ESLint**: Untuk code linting dan formatting 338 | - **Prettier**: Untuk code formatting consistency 339 | - **Conventional Commits**: Untuk commit message standards 340 | - **JSDoc**: Untuk dokumentasi function dan class 341 | 342 | ### Testing 343 | 344 | ```bash 345 | # Run tests 346 | npm test 347 | 348 | # Run tests with coverage 349 | npm run test:coverage 350 | 351 | # Run specific test file 352 | npm test -- --grep "anime api" 353 | ``` 354 | 355 | ## 🚀 Deployment 356 | 357 | ### Production Deployment 358 | 359 | 1. **Build Application** 360 | ```bash 361 | # Build API service 362 | cd api 363 | npm run build 364 | 365 | # Prepare frontend assets 366 | cd ../page 367 | npm run build 368 | ``` 369 | 370 | 2. **Start Services** 371 | ```bash 372 | # Start API service 373 | cd api 374 | npm start 375 | 376 | # Start frontend application 377 | cd ../page 378 | npm start 379 | ``` 380 | 381 | ### Docker Deployment 382 | 383 | ```dockerfile 384 | # Dockerfile example 385 | FROM node:18-alpine 386 | 387 | WORKDIR /app 388 | COPY package*.json ./ 389 | RUN npm ci --only=production 390 | 391 | COPY . . 392 | EXPOSE 3000 393 | 394 | CMD ["npm", "start"] 395 | ``` 396 | 397 | 398 | ## 🔄 Fitur yang Akan Datang 399 | 400 | - 📱 **Mobile App**: Aplikasi Android dan iOS native 401 | - 🔔 **Push Notifications**: Notifikasi episode baru 402 | - 👤 **User Accounts**: Sistem registrasi dan profile user 403 | - ❤️ **Favorites**: Bookmark anime favorit 404 | - 📝 **Reviews**: Sistem rating dan review anime 405 | - 🌐 **Multi-language**: Support subtitle multi-bahasa 406 | - 📊 **Advanced Analytics**: Dashboard analytics yang lebih detail 407 | - 🎮 **Gamification**: Achievement dan point system 408 | - 💬 **Comments**: Sistem komentar per episode 409 | - 📺 **Recommendations**: AI-powered anime recommendations 410 | 411 | ## 🤝 Kontribusi 412 | 413 | Kontribusi selalu diterima! Berikut cara berkontribusi: 414 | 415 | 1. **Fork** repository ini 416 | 2. **Create** feature branch (`git checkout -b feature/AmazingFeature`) 417 | 3. **Commit** perubahan (`git commit -m 'Add some AmazingFeature'`) 418 | 4. **Push** ke branch (`git push origin feature/AmazingFeature`) 419 | 5. **Open** Pull Request 420 | 421 | ### Guidelines Kontribusi 422 | 423 | - Ikuti code style yang sudah ada 424 | - Tambahkan tests untuk fitur baru 425 | - Update dokumentasi jika diperlukan 426 | - Pastikan semua tests passing 427 | 428 | ## 📄 Lisensi 429 | 430 | Proyek ini dilisensikan di bawah [MIT License](LICENSE). 431 | 432 | ## 🙏 Acknowledgments 433 | 434 | - **Otakudesu**: Sumber data anime utama 435 | - **Anoboy**: Sumber data anime movie 436 | - **Plyr**: Video player yang amazing 437 | - **Tailwind CSS**: Framework CSS yang powerful 438 | - **Express.js**: Web framework yang reliable 439 | - **Cheerio**: HTML parsing yang mudah 440 | 441 | ## 📞 Kontak & Support 442 | 443 | - 📧 **Email**: dragon.studio.official@gmail.com 444 | - 🐛 **Bug Reports**: [GitHub Issues](https://github.com/IkuzaDev/kitanime/issues) 445 | - 💡 **Feature Requests**: [GitHub Discussions](https://github.com/IkuzaDev/kitanime/discussions) 446 | 447 | --- 448 | 449 |
450 |

Made with ❤️ by IkuzaDev

451 |

© 2025 IkuzaDev. All rights reserved.

452 |
453 | -------------------------------------------------------------------------------- /api/handler/handler.js: -------------------------------------------------------------------------------- 1 | import otakudesu from '../src/otakudesu.js'; 2 | const searchAnimeHandler = async (req, res) => { 3 | const { keyword } = req.params; 4 | let data; 5 | try { 6 | data = await otakudesu.search(keyword); 7 | } 8 | catch (e) { 9 | console.log(e); 10 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 11 | } 12 | return res.status(200).json({ status: 'Ok', data }); 13 | }; 14 | const homeHandler = async (_, res) => { 15 | let data; 16 | try { 17 | data = await otakudesu.home(); 18 | } 19 | catch (e) { 20 | console.log(e); 21 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 22 | } 23 | return res.status(200).json({ status: 'Ok', data }); 24 | }; 25 | const ongoingAnimeHandler = async (req, res) => { 26 | const { page } = req.params; 27 | if (page) { 28 | if (!parseInt(page)) 29 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be a number!' }); 30 | if (parseInt(page) < 1) 31 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be greater than 0!' }); 32 | } 33 | let result; 34 | try { 35 | result = page ? await otakudesu.ongoingAnime(parseInt(page)) : await otakudesu.ongoingAnime(); 36 | } 37 | catch (e) { 38 | console.log(e); 39 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 40 | } 41 | const { paginationData, ongoingAnimeData } = result; 42 | if (!paginationData) 43 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 44 | return res.status(200).json({ status: 'Ok', data: ongoingAnimeData, pagination: paginationData }); 45 | }; 46 | const completeAnimeHandler = async (req, res) => { 47 | const { page } = req.params; 48 | if (page) { 49 | if (!parseInt(page)) 50 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be a number!' }); 51 | if (parseInt(page) < 1) 52 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be greater than 0!' }); 53 | } 54 | let result; 55 | try { 56 | result = page ? await otakudesu.completeAnime(parseInt(page)) : await otakudesu.completeAnime(); 57 | } 58 | catch (e) { 59 | console.log(e); 60 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 61 | } 62 | const { paginationData, completeAnimeData } = result; 63 | if (!paginationData) 64 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 65 | return res.status(200).json({ status: 'Ok', data: completeAnimeData, pagination: paginationData }); 66 | }; 67 | const singleAnimeHandler = async (req, res) => { 68 | const { slug } = req.params; 69 | let data; 70 | try { 71 | data = await otakudesu.anime(slug); 72 | } 73 | catch (e) { 74 | console.log(e); 75 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 76 | } 77 | if (!data) 78 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 79 | return res.status(200).json({ status: 'Ok', data }); 80 | }; 81 | const episodesHandler = async (req, res) => { 82 | const { slug } = req.params; 83 | let data; 84 | try { 85 | data = await otakudesu.episodes(slug); 86 | } 87 | catch (e) { 88 | console.log(e); 89 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 90 | } 91 | if (!data) 92 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 93 | return res.status(200).json({ status: 'Ok', data }); 94 | }; 95 | const episodeByEpisodeSlugHandler = async (req, res) => { 96 | const { slug } = req.params; 97 | let data; 98 | try { 99 | data = await otakudesu.episode({ episodeSlug: slug }); 100 | } 101 | catch (e) { 102 | console.log(e); 103 | return res.status(500).json({ status: 'Ok', message: 'Internal server error' }); 104 | } 105 | if (!data) 106 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 107 | return res.status(200).json({ status: 'Ok', data }); 108 | }; 109 | const episodeByEpisodeNumberHandler = async (req, res) => { 110 | const { slug: animeSlug, episode } = req.params; 111 | let data; 112 | try { 113 | data = await otakudesu.episode({ animeSlug, episodeNumber: parseInt(episode) }); 114 | } 115 | catch (e) { 116 | console.log(e); 117 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 118 | } 119 | if (!data) 120 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 121 | return res.status(200).json({ status: 'Ok', data }); 122 | }; 123 | const batchByBatchSlugHandler = async (req, res) => { 124 | const { slug } = req.params; 125 | let data; 126 | try { 127 | data = await otakudesu.batch({ batchSlug: slug }); 128 | } 129 | catch (e) { 130 | console.log(e); 131 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 132 | } 133 | return res.status(200).json({ status: 'Ok', data }); 134 | }; 135 | const batchHandler = async (req, res) => { 136 | const { slug } = req.params; 137 | let data; 138 | try { 139 | data = await otakudesu.batch({ animeSlug: slug }); 140 | } 141 | catch (e) { 142 | console.log(e); 143 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 144 | } 145 | return data ? res.status(200).json({ status: 'Ok', data }) : res.status(404).json({ 146 | status: 'Error', 147 | message: 'This anime doesn\'t have a batch yet ;_;' 148 | }); 149 | }; 150 | const genreListsHandler = async (_, res) => { 151 | let data; 152 | try { 153 | data = await otakudesu.genreLists(); 154 | } 155 | catch (e) { 156 | console.log(e); 157 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 158 | } 159 | return res.status(200).json({ status: 'Ok', data }); 160 | }; 161 | const animeByGenreHandler = async (req, res) => { 162 | const { slug, page } = req.params; 163 | if (page) { 164 | if (!parseInt(page)) 165 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be a number!' }); 166 | if (parseInt(page) < 1) 167 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be greater than 0!' }); 168 | } 169 | let data; 170 | try { 171 | data = await otakudesu.animeByGenre(slug, page); 172 | } 173 | catch (e) { 174 | console.log(e); 175 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 176 | } 177 | return res.status(200).json({ status: 'Ok', data }); 178 | }; 179 | 180 | const moviesHandler = async (req, res) => { 181 | const { page } = req.params; 182 | console.log('page: ', page); 183 | if (page) { 184 | if (!parseInt(page)) 185 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be a number!' }); 186 | if (parseInt(page) < 1) 187 | return res.status(400).json({ status: 'Error', message: 'The page parameter must be greater than 0!' }); 188 | } 189 | let data; 190 | try { 191 | data = await otakudesu.movies(page); 192 | } 193 | catch (e) { 194 | console.log(e); 195 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 196 | } 197 | return res.status(200).json({ status: 'Ok', data }); 198 | }; 199 | const singleMovieHandler = async (req, res) => { 200 | var { year, month, slug } = req.params; 201 | slug = `/${year}/${month}/${slug}`; 202 | console.log('slug: ', slug); 203 | let data; 204 | try { 205 | data = await otakudesu.movie(slug); 206 | } 207 | catch (e) { 208 | console.log(e); 209 | return res.status(500).json({ status: 'Error', message: 'Internal server error' }); 210 | } 211 | if (!data) 212 | return res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' }); 213 | return res.status(200).json({ status: 'Ok', data }); 214 | }; 215 | 216 | export default { 217 | searchAnimeHandler, 218 | moviesHandler, 219 | singleMovieHandler, 220 | homeHandler, 221 | singleAnimeHandler, 222 | episodesHandler, 223 | ongoingAnimeHandler, 224 | completeAnimeHandler, 225 | episodeByEpisodeSlugHandler, 226 | episodeByEpisodeNumberHandler, 227 | batchByBatchSlugHandler, 228 | batchHandler, 229 | genreListsHandler, 230 | animeByGenreHandler 231 | }; 232 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import cors from 'cors'; 3 | import routes from './routes/routes.js'; 4 | import 'dotenv/config'; 5 | const app = express(); 6 | const port = process.env.PORT ?? 3000; 7 | app.use(cors()); 8 | 9 | app.use((req, res, next) => { 10 | console.log(`[${new Date().toISOString()}] ${req.ip} ${req.method} ${req.originalUrl}`); 11 | 12 | next(); 13 | }); 14 | app.use(routes); 15 | app.listen(port, () => { 16 | console.log(`App is listening on port ${port}, http://localhost:${port}`); 17 | }); 18 | -------------------------------------------------------------------------------- /api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "otakudesu-api", 3 | "version": "0.0.1", 4 | "description": "Unofficial otakudesu api", 5 | "main": "./dist/index.js", 6 | "repository": "https://github.com/rzkfyn/otakudesu-api", 7 | "author": "rzkfyn", 8 | "license": "MIT", 9 | "type": "module", 10 | "scripts": { 11 | "dev": "nodemon index.js", 12 | "build": "tsc --build ./tsconfig.json", 13 | "start": "node ./dist/index.js" 14 | }, 15 | "dependencies": { 16 | "@types/cors": "^2.8.12", 17 | "axios": "^1.6.0", 18 | "cheerio": "^1.0.0-rc.12", 19 | "cors": "^2.8.5", 20 | "dotenv": "^16.0.3", 21 | "express": "^4.21.0", 22 | "gofile-downloader": "^0.0.2" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /api/routes/api.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import os from 'os'; 3 | import fs from 'fs'; 4 | import path from 'path'; 5 | import { execSync } from 'child_process'; 6 | import handler from '../handler/handler.js'; 7 | const api = Router(); 8 | api.get('/', (_, res) => { 9 | console.log('test'); 10 | 11 | const totalMem = os.totalmem() / (1024 * 1024 * 1024); 12 | const freeMem = os.freemem() / (1024 * 1024 * 1024); 13 | 14 | const platform = os.platform(); 15 | const release = os.release(); 16 | const arch = os.arch(); 17 | 18 | let diskInfo = {}; 19 | try { 20 | const df = execSync('df -h /').toString(); 21 | const lines = df.trim().split('\n'); 22 | const parts = lines[1].split(/\s+/); 23 | diskInfo = { 24 | size: parts[1], 25 | used: parts[2], 26 | avail: parts[3], 27 | usePercent: parts[4], 28 | mount: parts[5], 29 | }; 30 | } catch { 31 | diskInfo = { error: 'df command failed or not available' }; 32 | } 33 | 34 | res.status(200).json({ 35 | status: 'OK', 36 | message: 'Srcaper API otakudesu', 37 | system: { 38 | ram: { 39 | totalGB: totalMem.toFixed(2), 40 | freeGB: freeMem.toFixed(2), 41 | }, 42 | os: { 43 | platform, 44 | release, 45 | arch, 46 | }, 47 | disk: diskInfo, 48 | }, 49 | }); 50 | }); 51 | api.get('/home', handler.homeHandler); 52 | api.get('/search/:keyword', handler.searchAnimeHandler); 53 | api.get('/ongoing-anime/:page?', handler.ongoingAnimeHandler); 54 | api.get('/complete-anime/:page?', handler.completeAnimeHandler); 55 | api.get('/anime/:slug', handler.singleAnimeHandler); 56 | api.get('/anime/:slug/episodes', handler.episodesHandler); 57 | api.get('/anime/:slug/episodes/:episode', handler.episodeByEpisodeNumberHandler); 58 | api.get('/episode/:slug', handler.episodeByEpisodeSlugHandler); 59 | api.get('/batch/:slug', handler.batchByBatchSlugHandler); 60 | api.get('/anime/:slug/batch', handler.batchHandler); 61 | api.get('/genres', handler.genreListsHandler); 62 | api.get('/genres/:slug/:page?', handler.animeByGenreHandler); 63 | api.get('/movies/:page', handler.moviesHandler); 64 | api.get('/movies/:year/:month/:slug', handler.singleMovieHandler); 65 | 66 | export default api; 67 | -------------------------------------------------------------------------------- /api/routes/routes.js: -------------------------------------------------------------------------------- 1 | import { Router } from 'express'; 2 | import api from './api.js'; 3 | const routes = Router(); 4 | routes.get('/', (_, res) => res.status(200).json({ status: 'Ok', message: 'Srcaper API otakudesu' })); 5 | routes.use('/v1', api); 6 | routes.use((_, res) => res.status(404).json({ status: 'Error', message: 'There\'s nothing here ;_;' })); 7 | export default routes; 8 | -------------------------------------------------------------------------------- /api/src/lib/getBatch.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const getBatch = (html) => { 3 | const $ = load(html); 4 | const batch = $('.venser #serieslist ~ .episodelist ul li:first-child span:first-child a').attr('href'); 5 | const uploaded_at = $('.venser #serieslist ~ .episodelist ul li:first-child span.zeebr:first').text(); 6 | return batch?.match('episode') ? null : { 7 | slug: batch?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/batch\//, '').replace('/', ''), 8 | otakudesu_url: batch, 9 | uploaded_at 10 | }; 11 | }; 12 | export default getBatch; 13 | -------------------------------------------------------------------------------- /api/src/lib/mapGenres.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 3 | const mapGenres = (html) => { 4 | const result = []; 5 | const genres = html.split('') 6 | .filter(item => item.trim() !== '') 7 | .map(item => `${item}`); 8 | genres.forEach(genre => { 9 | const $ = load(genre); 10 | result.push({ 11 | name: $('a').text(), 12 | slug: $('a').attr('href')?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/genres\//, '').replace('/', ''), 13 | otakudesu_url: $('a').attr('href') 14 | }); 15 | }); 16 | return result; 17 | }; 18 | export default mapGenres; 19 | -------------------------------------------------------------------------------- /api/src/lib/pagination.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const pagination = (html, anoboy = false) => { 3 | console.log('isAnoboy', anoboy); 4 | const $ = load(html); 5 | const current_page = anoboy ? parseInt($('.wp-pagenavi .current').text()) : parseInt($('.pagination .pagenavix .page-numbers.current').text()); 6 | const last_visible_page = anoboy ? parseInt($('.wp-pagenavi .page.larger:last').text()) : parseInt($('.pagination .pagenavix .page-numbers:last').prev('a.page-numbers').text()); 7 | const next_page = current_page < last_visible_page ? current_page + 1 : null; 8 | const previous_page = current_page > 1 ? current_page - 1 : null; 9 | const has_next_page = current_page < last_visible_page; 10 | const has_previous_page = current_page > 1; 11 | if (!current_page) 12 | return false; 13 | return { 14 | current_page, 15 | last_visible_page: current_page < last_visible_page ? last_visible_page : current_page, 16 | has_next_page, 17 | next_page, 18 | has_previous_page, 19 | previous_page 20 | }; 21 | }; 22 | export default pagination; 23 | -------------------------------------------------------------------------------- /api/src/lib/scapeOngoingAnime.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const scrapeOngoingAnime = (html) => { 3 | const result = []; 4 | const animes = html.split('') 5 | .filter(item => item.trim() !== '') 6 | .map(item => `${item}`); 7 | animes.forEach(anime => { 8 | const $ = load(anime); 9 | result.push({ 10 | title: $('.detpost .thumb .thumbz .jdlflm').text(), 11 | slug: $('.detpost .thumb a').attr('href')?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/anime\//, '').replace('/', ''), 12 | poster: $('.detpost .thumb .thumbz img').attr('src'), 13 | current_episode: $('.detpost .epz').text().trim(), 14 | release_day: $('.detpost .epztipe').text().trim(), 15 | newest_release_date: $('.detpost .newnime').text(), 16 | otakudesu_url: $('.detpost .thumb a').attr('href') 17 | }); 18 | }); 19 | return result; 20 | }; 21 | export default scrapeOngoingAnime; 22 | -------------------------------------------------------------------------------- /api/src/lib/scrapeAnimeByGenre.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | import pagination from './pagination.js'; 3 | import mapGenres from './mapGenres.js'; 4 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 5 | const scrapeAnimeByGenre = (html) => { 6 | const $ = load(html); 7 | const animeElements = $('.venser .page .col-anime-con').toString() 8 | .split('
') 9 | .filter((element) => element.trim() !== '') 10 | .map((element) => `
${element}`); 11 | const result = []; 12 | animeElements.forEach((animeEl) => { 13 | const $ = load(animeEl); 14 | const episodeCount = $('.col-anime .col-anime-eps').text().replace(/[A-z]/g, '').trim(); 15 | const genres = mapGenres($('.col-anime .col-anime-genre a').toString()); 16 | result.push({ 17 | title: $('.col-anime .col-anime-title a').text(), 18 | slug: $('.col-anime .col-anime-trailer a').attr('href')?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/anime\//, '').replace('/', ''), 19 | poster: $('.col-anime .col-anime-cover img').attr('src'), 20 | rating: $('.col-anime .col-anime-rating').text() ?? null, 21 | episode_count: episodeCount === '' ? null : episodeCount, 22 | season: $('.col-anime .col-anime-date').text(), 23 | studio: $('.col-anime .col-anime-studio').text(), 24 | genres, 25 | synopsis: $('.col-anime .col-synopsis p').text(), 26 | otakudesu_url: $('.col-anime .col-anime-trailer a').attr('href') 27 | }); 28 | }); 29 | return { 30 | anime: result, 31 | pagination: pagination(html) 32 | }; 33 | }; 34 | export default scrapeAnimeByGenre; 35 | -------------------------------------------------------------------------------- /api/src/lib/scrapeAnimeEpisodes.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const scrapeAnimeEpisodes = (html) => { 3 | const result = []; 4 | let $ = load(html); 5 | $ = load(`
${$('.episodelist').toString()}
`); 6 | const episodeList = $('.episodelist:nth-child(2) ul').html()?.split('').filter(item => item.trim() !== '').map(item => `${item}`); 7 | if (!episodeList) 8 | return undefined; 9 | for (const episode of episodeList) { 10 | const $ = load(episode); 11 | result.unshift({ 12 | episode: $('li span:first a')?.text(), 13 | slug: $('li span:first a')?.attr('href')?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/episode\//, '').replace('/', ''), 14 | otakudesu_url: $('li span:first a')?.attr('href') 15 | }); 16 | } 17 | return result; 18 | }; 19 | export default scrapeAnimeEpisodes; 20 | -------------------------------------------------------------------------------- /api/src/lib/scrapeBatch.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const scrapeBatch = (html) => { 3 | const $ = load(html); 4 | const batch = $('.download2 .batchlink h4').text(); 5 | const urlGroups = $('.download2 .batchlink ul li').toString() 6 | .split('') 7 | .filter((item) => item.trim() !== '') 8 | .map((item) => `${item}
  • `); 9 | const urls = []; 10 | const download_urls = []; 11 | urlGroups.forEach((urlGroup) => { 12 | const $ = load(urlGroup); 13 | const providers = $('a').toString() 14 | .split('') 15 | .filter((item) => item.trim() !== '') 16 | .map((item) => `${item}`); 17 | providers.forEach((provider) => { 18 | const $ = load(provider); 19 | urls.push({ 20 | provider: $('a').text(), 21 | url: $('a').attr('href') 22 | }); 23 | }); 24 | download_urls.push({ 25 | resolution: $('li strong').text().replace(/([A-z][A-z][0-9] )/, ''), 26 | file_size: $('li i').text(), 27 | urls 28 | }); 29 | }); 30 | return { 31 | batch, 32 | download_urls 33 | }; 34 | }; 35 | export default scrapeBatch; 36 | -------------------------------------------------------------------------------- /api/src/lib/scrapeCompleteAnime.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const scrapeCompleteAnime = (html) => { 3 | const result = []; 4 | const animes = html.split('
  • ') 5 | .filter(item => item.trim() !== '') 6 | .map(item => `${item}`); 7 | animes.forEach(anime => { 8 | const $ = load(anime); 9 | result.push({ 10 | title: $('.detpost .thumb .thumbz .jdlflm').text(), 11 | slug: $('.detpost .thumb a').attr('href')?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/anime\//, '').replace('/', ''), 12 | poster: $('.detpost .thumb .thumbz img').attr('src'), 13 | episode_count: $('.detpost .epz').text().trim().replace(' Episode', ''), 14 | rating: $('.detpost .epztipe').text().trim(), 15 | last_release_date: $('.detpost .newnime').text(), 16 | otakudesu_url: $('.detpost .thumb a').attr('href') 17 | }); 18 | }); 19 | return result; 20 | }; 21 | export default scrapeCompleteAnime; 22 | -------------------------------------------------------------------------------- /api/src/lib/scrapeEpisode.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | import axios from 'axios'; 3 | const { BASEURL } = process.env ?? 'https://otakudesu.best'; 4 | const scrapeEpisode = async (html) => { 5 | const $ = load(html); 6 | const episode = getEpisodeTitle($); 7 | const download_urls = createDownloadData($); 8 | const previous_episode = getPrevEpisode($); 9 | const stream_url = getStreamUrl($); 10 | const next_episode = getNextEpisode($); 11 | const anime = getAnimeData($); 12 | const qualityList = await getStreamQuality($); 13 | if (!episode) 14 | return undefined; 15 | return { 16 | episode, 17 | anime, 18 | has_next_episode: next_episode ? true : false, 19 | next_episode, 20 | has_previous_episode: previous_episode ? true : false, 21 | previous_episode, 22 | stream_url: qualityList['480p'] || stream_url, 23 | steramList : qualityList, 24 | download_urls, 25 | }; 26 | }; 27 | const getEpisodeTitle = ($) => { 28 | return $('.venutama .posttl').text(); 29 | }; 30 | 31 | const getStreamUrl = ($) => { 32 | return $('#pembed iframe').attr('src'); 33 | }; 34 | 35 | const postToGetData = async (action, action2, videoData) => { 36 | const tasks = Object.entries(videoData).map(async ([key, value]) => { 37 | if (!value) return [key, null]; 38 | try { 39 | const url = `https://otakudesu.best/wp-admin/admin-ajax.php`; 40 | const form = new URLSearchParams(); 41 | form.append("id", value.id); 42 | form.append("i", value.i); 43 | form.append("q", value.q); 44 | form.append("action", action); 45 | 46 | let res = await axios.post(url, form.toString(), { 47 | headers: { "Content-Type": "application/x-www-form-urlencoded" } 48 | }); 49 | const form2 = new URLSearchParams(); 50 | form2.append("id", value.id); 51 | form2.append("i", value.i); 52 | form2.append("q", value.q); 53 | form2.append("action", action2); 54 | form2.append("nonce", res.data.data); 55 | 56 | res = await axios.post(url, form2.toString(), { 57 | headers: { "Content-Type": "application/x-www-form-urlencoded" } 58 | }); 59 | const $$ = load(Buffer.from(res.data.data, "base64").toString("utf8")); 60 | const pdrain_url = $$("iframe").attr("src"); 61 | 62 | const pdarin = await axios.get(pdrain_url); 63 | const $$$ = load(pdarin.data); 64 | const finalUrl = $$$('meta[property="og:video:secure_url"]').attr("content"); 65 | 66 | return [key.replace("m", ""), finalUrl]; 67 | } catch (err) { 68 | console.error(`${key} error:`, err.message); 69 | return [key, null]; 70 | } 71 | }); 72 | 73 | const resultsArr = await Promise.all(tasks); 74 | return Object.fromEntries(resultsArr.filter(Boolean)); 75 | }; 76 | 77 | const getStreamQuality = async($) => { 78 | const streamLable = $('.mirrorstream'); 79 | const results = {}; 80 | ["m360p", "m480p", "m720p"].forEach(q => { 81 | const items = streamLable.find(`ul.${q} li a`); 82 | const last = items 83 | .filter((i, el) => { 84 | const text = $(el).text().toLowerCase(); 85 | return text.includes("drain") || text.includes("desu"); 86 | }).first(); 87 | if (last.length) { 88 | results[q] = JSON.parse(Buffer.from(last.attr("data-content"), "base64").toString("utf8")); 89 | } 90 | }); 91 | const actions = []; 92 | $("script").each((i, el) => { 93 | const scriptContent = $(el).html(); 94 | if (!scriptContent) return; 95 | const regex = /action\s*:\s*"([a-z0-9]+)"/gi; 96 | let match; 97 | while ((match = regex.exec(scriptContent)) !== null) { 98 | actions.push(match[1]); 99 | } 100 | }); 101 | const uniqueActions = [...new Set(actions)]; 102 | const init = uniqueActions[1]; 103 | const action = uniqueActions[0]; 104 | const data = await postToGetData(init, action, results); 105 | return data; 106 | }; 107 | 108 | const createDownloadData = ($) => { 109 | const mp4 = getMp4DownloadUrls($); 110 | const mkv = getMkvDownloadUrls($); 111 | return { 112 | mp4, 113 | mkv, 114 | }; 115 | }; 116 | const getMp4DownloadUrls = ($) => { 117 | const result = []; 118 | const mp4DownloadEls = $('.download ul:first li') 119 | .toString() 120 | .split('') 121 | .filter((item) => item.trim() !== '') 122 | .map((item) => `${item}`); 123 | for (const el of mp4DownloadEls) { 124 | const $ = load(el); 125 | const downloadUrls = $('a') 126 | .toString() 127 | .split('') 128 | .filter((item) => item.trim() !== '') 129 | .map((item) => `${item}`); 130 | const urls = []; 131 | for (const downloadUrl of downloadUrls) { 132 | const $ = load(downloadUrl); 133 | urls.push({ 134 | provider: $('a').text(), 135 | url: $('a').attr('href'), 136 | }); 137 | } 138 | result.push({ 139 | resolution: $('strong').text()?.replace(/([A-z][A-z][0-9] )/, ''), 140 | urls, 141 | }); 142 | } 143 | return result; 144 | }; 145 | const getMkvDownloadUrls = ($) => { 146 | const result = []; 147 | const mp4DownloadEls = $('.download ul:last li') 148 | .toString() 149 | .split('') 150 | .filter((item) => item.trim() !== '') 151 | .map((item) => `${item}`); 152 | for (const el of mp4DownloadEls) { 153 | const $ = load(el); 154 | const downloadUrls = $('a') 155 | .toString() 156 | .split('') 157 | .filter((item) => item.trim() !== '') 158 | .map((item) => `${item}`); 159 | const urls = []; 160 | for (const url of downloadUrls) { 161 | const $ = load(url); 162 | urls.push({ 163 | provider: $('a').text(), 164 | url: $('a').attr('href'), 165 | }); 166 | } 167 | result.push({ 168 | resolution: $('strong').text()?.replace(/([A-z][A-z][A-z] )/, ''), 169 | urls, 170 | }); 171 | } 172 | return result; 173 | }; 174 | const getPrevEpisode = ($) => { 175 | if (!$('.flir a:first').attr('href')?.includes(`/episode/`)) return null; 176 | var nextEps = $('.flir a:first').attr('href'); 177 | nextEps = nextEps.split('/episode/')[1].split('-episode-')[1]; 178 | return nextEps.match(/\d+/)[0]; 179 | }; 180 | const getNextEpisode = ($) => { 181 | if (!$('.flir a:last').attr('href')?.includes(`/episode/`)) return null; 182 | var nextEps = $('.flir a:last').attr('href'); 183 | nextEps = nextEps.split('/episode/')[1].split('-episode-')[1]; 184 | return nextEps.match(/\d+/)[0]; 185 | }; 186 | const getAnimeData = ($) => { 187 | if ($('.flir a:nth-child(3)').text().trim() === '' || $('.flir a:nth-child(3)').text() === undefined) { 188 | 189 | return { 190 | slug: $('.flir a:first').attr('href')?.replace(`${BASEURL}/anime/`, '')?.replace('/', ''), 191 | otakudesu_url: $('.flir a:first').attr('href'), 192 | }; 193 | } 194 | return { 195 | slug: $('.flir a:nth-child(2)').attr('href')?.replace(`${BASEURL}/anime/`, '')?.replace('/', ''), 196 | otakudesu_url: $('.flir a:nth-child(2)').attr('href'), 197 | }; 198 | }; 199 | export default scrapeEpisode; 200 | -------------------------------------------------------------------------------- /api/src/lib/scrapeGenreLists.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 3 | const scrapeGenreLists = (html) => { 4 | const $ = load(html); 5 | const result = []; 6 | const genres = $('#venkonten .vezone ul.genres li a').toString() 7 | .split('') 8 | .filter((el) => el.trim() !== '') 9 | .map((el) => `${el}`); 10 | genres.forEach((genre) => { 11 | const $ = load(genre); 12 | result.push({ 13 | name: $('a').text(), 14 | slug: $('a').attr('href')?.replace('/genres/', '').replace('/', ''), 15 | otakudesu_url: `${BASEURL}${$('a').attr('href')}` 16 | }); 17 | }); 18 | return result; 19 | }; 20 | export default scrapeGenreLists; 21 | -------------------------------------------------------------------------------- /api/src/lib/scrapeSearchResult.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | import mapGenres from './mapGenres.js'; 3 | const scrapeSearchResult = (html) => { 4 | const $ = load(html); 5 | const animes = $('.chivsrc li').toString() 6 | .split('') 7 | .filter(item => item.trim() !== '') 8 | .map(item => `${item}`); 9 | const searchResult = []; 10 | animes.forEach(anime => { 11 | const $ = load(anime); 12 | const genres = mapGenres($('.set:nth-child(3)')?.html()?.toString() 13 | .replace('Genres : ', '')); 14 | searchResult.push({ 15 | title: $('h2 a').text(), 16 | slug: $('h2 a').attr('href')?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/anime\//, '').replace('/', ''), 17 | poster: $('img').attr('src'), 18 | genres, 19 | status: $('.set:nth-child(4)').text()?.replace('Status : ', ''), 20 | rating: $('.set:last-child').text()?.replace('Rating : ', ''), 21 | url: $('h2 a').attr('href') 22 | }); 23 | }); 24 | return searchResult; 25 | }; 26 | export default scrapeSearchResult; 27 | -------------------------------------------------------------------------------- /api/src/lib/scrapeSingleAnime.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | import mapGenres from './mapGenres.js'; 3 | import scrapeAnimeEpisodes from './scrapeAnimeEpisodes.js'; 4 | import getBatch from './getBatch.js'; 5 | const scrapeSingleAnime = (html) => { 6 | const result = createAnimeData(html, getPoster(html), getSynopsis(html), scrapeAnimeEpisodes(html)); 7 | return result; 8 | }; 9 | const createAnimeData = (html, poster, synopsis, episode_lists) => { 10 | const $ = load(html); 11 | const title = $('.infozin .infozingle p:first span').text()?.replace('Judul: ', ''); 12 | const japanese_title = $('.infozin .infozingle p:nth-child(2) span').text()?.replace('Japanese: ', ''); 13 | const rating = $('.infozin .infozingle p:nth-child(3) span').text()?.replace('Skor: ', ''); 14 | const produser = $('.infozin .infozingle p:nth-child(4) span').text()?.replace('Produser: ', ''); 15 | const type = $('.infozin .infozingle p:nth-child(5) span').text()?.replace('Tipe: ', ''); 16 | const status = $('.infozin .infozingle p:nth-child(6) span').text()?.replace('Status: ', ''); 17 | const episode_count = $('.infozin .infozingle p:nth-child(7) span').text()?.replace('Total Episode: ', ''); 18 | const duration = $('.infozin .infozingle p:nth-child(8) span').text()?.replace('Durasi: ', ''); 19 | const release_date = $('.infozin .infozingle p:nth-child(9) span').text()?.replace('Tanggal Rilis: ', ''); 20 | const studio = $('.infozin .infozingle p:nth-child(10) span').text()?.replace('Studio: ', ''); 21 | const genres = mapGenres($('.infozin .infozingle p:last span a').toString()); 22 | const batch = getBatch(html); 23 | const recommendations = getRecomendations($('#recommend-anime-series .isi-recommend-anime-series .isi-konten').toString()); 24 | if (!episode_lists) 25 | return undefined; 26 | return { 27 | title, japanese_title, poster, rating, produser, type, status, episode_count, duration, release_date, studio, genres, synopsis, batch, episode_lists, recommendations 28 | }; 29 | }; 30 | const getSynopsis = (html) => { 31 | const $ = load(html); 32 | const synopsis = $('.sinopc').text().split('

    ').map(item => item.replace('

    ', '\n').replace(' ', '')).join(''); 33 | return synopsis; 34 | }; 35 | const getPoster = (html) => { 36 | const $ = load(html); 37 | const poster = $('.fotoanime img').attr('src'); 38 | return poster; 39 | }; 40 | const getRecomendations = (html) => { 41 | const result = []; 42 | const animeEls = html.split('
    ') 43 | .filter((el) => el.trim() !== '') 44 | .map((el) => `${el}`); 45 | animeEls.forEach((el) => { 46 | const $ = load(el); 47 | const title = $('.judul-anime').text(); 48 | const poster = $('.isi-anime img').attr('src'); 49 | const otakudesu_url = $('.isi-anime a').attr('href'); 50 | const slug = otakudesu_url?.replace(/^https:\/\/otakudesu\.[a-zA-Z0-9-]+\/anime\//, '').replace('/', ''); 51 | result.push({ 52 | title, 53 | slug, 54 | poster, 55 | otakudesu_url 56 | }); 57 | }); 58 | return result; 59 | }; 60 | export default scrapeSingleAnime; 61 | -------------------------------------------------------------------------------- /api/src/otakudesu.js: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | import home from './utils/home.js'; 3 | import anime from './utils/anime.js'; 4 | import episodes from './utils/episodes.js'; 5 | import episode from './utils/episode.js'; 6 | import search from './utils/search.js'; 7 | import ongoingAnime from './utils/ongoingAnime.js'; 8 | import completeAnime from './utils/completeAnime.js'; 9 | import batch from './utils/batch.js'; 10 | import genreLists from './utils/genreLists.js'; 11 | import animeByGenre from './utils/animeByGenre.js'; 12 | import movies from './utils/movies.js'; 13 | import movie from './utils/movie.js'; 14 | export default { 15 | home, 16 | movies, 17 | movie, 18 | anime, 19 | episodes, 20 | episode, 21 | search, 22 | ongoingAnime, 23 | completeAnime, 24 | batch, 25 | genreLists, 26 | animeByGenre 27 | }; 28 | -------------------------------------------------------------------------------- /api/src/types/types.js: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /api/src/utils/anime.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import scrapeSingleAnime from '../lib/scrapeSingleAnime.js'; 3 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 4 | const anime = async (slug) => { 5 | const { data } = await axios.get(`${BASEURL}/anime/${slug}`); 6 | const result = scrapeSingleAnime(data); 7 | return result; 8 | }; 9 | export default anime; 10 | -------------------------------------------------------------------------------- /api/src/utils/animeByGenre.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import scrapeAnimeByGenre from '../lib/scrapeAnimeByGenre.js'; 3 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 4 | const animeByGenre = async (genre, page = 1) => { 5 | const response = await axios.get(`${BASEURL}/genres/${genre}/page/${page}`); 6 | const result = scrapeAnimeByGenre(response.data); 7 | return result; 8 | }; 9 | export default animeByGenre; 10 | -------------------------------------------------------------------------------- /api/src/utils/batch.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import getBatch from '../lib/getBatch.js'; 3 | import scrapeBatch from '../lib/scrapeBatch.js'; 4 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 5 | const batch = async ({ batchSlug, animeSlug }) => { 6 | let batch = batchSlug; 7 | if (animeSlug) { 8 | const response = await axios.get(`${BASEURL}/anime/${animeSlug}`); 9 | const batchData = getBatch(response.data); 10 | batch = batchData?.slug; 11 | } 12 | if (!batch) 13 | return false; 14 | const response = await axios.get(`${BASEURL}/batch/${batch}`); 15 | const result = scrapeBatch(response.data); 16 | return result; 17 | }; 18 | export default batch; 19 | -------------------------------------------------------------------------------- /api/src/utils/completeAnime.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { load } from 'cheerio'; 3 | import pagination from '../lib/pagination.js'; 4 | import scrapeCompleteAnime from '../lib/scrapeCompleteAnime.js'; 5 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 6 | const completeAnime = async (page = 1) => { 7 | const { data } = await axios.get(`${BASEURL}/complete-anime/page/${page}`); 8 | const $ = load(data); 9 | const completeAnimeEls = $('.venutama .rseries .rapi .venz ul li').toString(); 10 | const completeAnimeData = scrapeCompleteAnime(completeAnimeEls); 11 | const paginationData = pagination($('.pagination').toString()); 12 | return { 13 | paginationData, 14 | completeAnimeData 15 | }; 16 | }; 17 | export default completeAnime; 18 | -------------------------------------------------------------------------------- /api/src/utils/episode.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import episodes from './episodes.js'; 3 | import scrapeEpisode from '../lib/scrapeEpisode.js'; 4 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 5 | const episode = async ({ episodeSlug, animeSlug, episodeNumber }) => { 6 | let slug = ''; 7 | console.log(episodeSlug, animeSlug, episodeNumber); 8 | if (episodeSlug) 9 | slug = episodeSlug; 10 | if (animeSlug) { 11 | const episodeLists = await episodes(animeSlug); 12 | if (!episodeLists) 13 | return undefined; 14 | const clean = episodeLists.map(ep => { 15 | const match = ep.episode.match(/Episode\s+(\d+)/i); 16 | const num = match ? match[1] : null; 17 | 18 | return { 19 | ...ep, 20 | episode: num 21 | }; 22 | }); 23 | const lowEps = clean[0].episode; 24 | const isFirst = lowEps === '0' && clean[1].slug.includes("-sub-indo-2"); 25 | const split = clean[0].slug?.split('-episode-'); 26 | const topPrefix = split[0]; 27 | const topSuffix = split[1]; 28 | const epNumPart = isFirst && episodeNumber === 1 ? '1-sub-indo-2' : `${episodeNumber == 0 ? 1 : episodeNumber}-sub-indo`; 29 | slug = `${topPrefix}-episode-${epNumPart}`; 30 | console.log(slug, episodeNumber) 31 | } 32 | const { data } = await axios.get(`${BASEURL}/episode/${slug}`); 33 | const result = await scrapeEpisode(data); 34 | return result; 35 | }; 36 | export default episode; 37 | -------------------------------------------------------------------------------- /api/src/utils/episodes.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import scrapeAnimeEpisodes from '../lib/scrapeAnimeEpisodes.js'; 3 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 4 | const episodes = async (slug) => { 5 | const { data } = await axios.get(`${BASEURL}/anime/${slug}`); 6 | const result = scrapeAnimeEpisodes(data); 7 | return result; 8 | }; 9 | export default episodes; 10 | -------------------------------------------------------------------------------- /api/src/utils/genreLists.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import 'dotenv/config'; 3 | import scrapeGenreLists from '../lib/scrapeGenreLists.js'; 4 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 5 | const genreLists = async () => { 6 | const response = await axios.get(`${BASEURL}/genre-list`); 7 | const result = scrapeGenreLists(response.data); 8 | return result; 9 | }; 10 | export default genreLists; 11 | -------------------------------------------------------------------------------- /api/src/utils/home.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { load } from 'cheerio'; 3 | import scrapeOngoingAnime from '../lib/scapeOngoingAnime.js'; 4 | import scrapeCompleteAnime from '../lib/scrapeCompleteAnime.js'; 5 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 6 | console.log(BASEURL); 7 | const home = async () => { 8 | const { data } = await axios.get(BASEURL); 9 | const $ = load(data); 10 | const ongoingAnimeEls = $('.venutama .rseries .rapi:first .venz ul li').toString(); 11 | const completeAnimeEls = $('.venutama .rseries .rapi:last .venz ul li').toString(); 12 | const ongoing_anime = scrapeOngoingAnime(ongoingAnimeEls); 13 | const complete_anime = scrapeCompleteAnime(completeAnimeEls); 14 | return { 15 | ongoing_anime, 16 | complete_anime 17 | }; 18 | }; 19 | export default home; 20 | -------------------------------------------------------------------------------- /api/src/utils/movie.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | import axios from 'axios'; 3 | import {getDownloadLink, getToken, getContent, getWt, listFiles} from 'gofile-downloader'; 4 | const ANOBOY = process.env.ANOBOY || 'https://ww3.anoboy.app'; 5 | const movie = async (slug) => { 6 | console.log(`${ANOBOY}${slug}`); 7 | const { data } = await axios.get(`${ANOBOY}${slug}`); 8 | const $ = load(data); 9 | const movie = {}; 10 | const sinopsi = []; 11 | movie.title = $('.unduhan h3').text().toLowerCase().split('sub')[0]; 12 | movie.poster = ANOBOY + $('.unduhan amp-img').attr('src'); 13 | 14 | const downloadUrls = { 15 | '480p': [], 16 | '720p': [], 17 | '1080p': [] 18 | }; 19 | $('.download .ud .udl').each((index, element) => { 20 | const $$ = load(element); 21 | const label = $$.text().trim().toLowerCase(); 22 | const link = $$('a').attr('href'); 23 | console.log(label, link); 24 | 25 | if (!link) return; 26 | 27 | if (label.includes('480')) { 28 | if(link !== 'none') downloadUrls['480p'].push(link); 29 | } else if (label.includes('720')) { 30 | if(link !== 'none') downloadUrls['720p'].push(link); 31 | } else if (label.includes('1k')) { 32 | if(link !== 'none') downloadUrls['1080p'].push(link); 33 | } 34 | }); 35 | movie.download_urls = downloadUrls; 36 | var stream = downloadUrls['480p'].map((item) => item.includes('mp4upload') ? item : null).filter((item) => item !== null)[0]; 37 | movie.stream_url = stream; 38 | 39 | return movie; 40 | }; 41 | export default movie; 42 | -------------------------------------------------------------------------------- /api/src/utils/movies.js: -------------------------------------------------------------------------------- 1 | import { load } from 'cheerio'; 2 | import axios from 'axios'; 3 | import pagination from '../lib/pagination.js'; 4 | const ANOBOY = process.env.ANOBOY || 'https://ww3.anoboy.app'; 5 | const movies = async (page = 1) => { 6 | console.log(`${ANOBOY}/category/anime-movie/page/${page}`); 7 | const { data } = await axios.get(`${ANOBOY}/category/anime-movie/page/${page}`); 8 | const $ = load(data); 9 | const movies = []; 10 | $('a[rel="bookmark"]').each((index, element) => { 11 | const $ = load(element); 12 | const animex = $('a[rel="bookmark"]').attr('href')?.replace(ANOBOY, '').split('/'); 13 | movies.push({ 14 | title: $('a[rel="bookmark"]').attr('title'), 15 | years: animex[1], 16 | month: animex[2], 17 | slug: animex[3], 18 | poster: ANOBOY + $('amp-img').attr('src'), 19 | otakudesu_url: $('a[rel="bookmark"]').attr('href') 20 | }); 21 | }); 22 | 23 | return { 24 | movies, 25 | pagination: pagination($('.wp-pagenavi').toString(), true) 26 | } 27 | }; 28 | export default movies; 29 | -------------------------------------------------------------------------------- /api/src/utils/ongoingAnime.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import { load } from 'cheerio'; 3 | import pagination from '../lib/pagination.js'; 4 | import scrapeOngoingAnime from '../lib/scapeOngoingAnime.js'; 5 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 6 | const ongoingAnime = async (page = 1) => { 7 | const { data } = await axios.get(`${BASEURL}/ongoing-anime/page/${page}`); 8 | const $ = load(data); 9 | const ongoingAnimeEls = $('.venutama .rseries .rapi .venz ul li').toString(); 10 | const ongoingAnimeData = scrapeOngoingAnime(ongoingAnimeEls); 11 | const paginationData = pagination($('.pagination').toString()); 12 | return { 13 | paginationData, 14 | ongoingAnimeData 15 | }; 16 | }; 17 | export default ongoingAnime; 18 | -------------------------------------------------------------------------------- /api/src/utils/search.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | import scrapesearchresult from '../lib/scrapeSearchResult.js'; 3 | const BASEURL = process.env.BASEURL || 'https://otakudesu.best'; 4 | const search = async (keyword) => { 5 | const response = await axios.get(`${BASEURL}/?s=${keyword}&post_type=anime`); 6 | const html = response.data; 7 | const searchResult = scrapesearchresult(html); 8 | return searchResult; 9 | }; 10 | export default search; 11 | --------------------------------------------------------------------------------