/** * Shinigami * base: https://g.shinigami.asia/ * Creator: ShanMolvyr * Jangan Hapus Kreator hargai rakyat kecil * Note: cek https://snippet.vyr.my.id/shanmolvyr/shinigami/README.md * Sumber: https://whatsapp.com/channel/0029VbB4Kw8EFeXfeExaXc3Q */ const BASE_API = 'https://api.shngm.io'; const ASSETS_BASE = 'https://assets.shngm.id'; const HEADERS = { Accept: 'application/json', 'Content-Type': 'application/json', Origin: 'https://g.shinigami.asia', Referer: 'https://g.shinigami.asia/', 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; K) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/140.0.0.0 Safari/537.36', }; async function api(path) { const res = await fetch(`${BASE_API}${path}`, { headers: HEADERS }); if (!res.ok) throw new Error(`HTTP ${res.status}: ${BASE_API}${path}`); return res.json(); } function getMangaList(p = {}) { const q = new URLSearchParams({ page: p.page ?? 1, page_size: p.pageSize ?? 24, sort: p.sort ?? 'latest', sort_order: p.sortOrder ?? 'desc', }); if (p.format != null) q.set('format', p.format); if (p.isUpdate != null) q.set('is_update', p.isUpdate); if (p.isRecommended != null) q.set('is_recommended', p.isRecommended); if (p.type != null) q.set('type', p.type); return api(`/v1/manga/list?${q}`); } function getMangaTop(p = {}) { const q = new URLSearchParams({ filter: p.filter ?? 'daily', page: p.page ?? 1, page_size: p.pageSize ?? 10, }); return api(`/v1/manga/top?${q}`); } const getMangaDetail = (mangaId) => api(`/v1/manga/detail/${mangaId}`); const getChapterDetail = (chapterId) => api(`/v1/chapter/detail/${chapterId}`); function getChapterList(mangaId, p = {}) { const q = new URLSearchParams({ page: p.page ?? 1, page_size: p.pageSize ?? 100, sort_by: 'chapter_number', sort_order: p.sortOrder ?? 'desc', }); return api(`/v1/chapter/${mangaId}/list?${q}`); } function getImageUrls(chapterData) { const base = chapterData.base_url + chapterData.chapter.path; return chapterData.chapter.data.map(f => base + f); } function searchManga(p = {}) { const q = new URLSearchParams({ page: p.page ?? 1, page_size: p.pageSize ?? 24, sort: 'latest', sort_order: 'desc', genre_include_mode: 'or', genre_exclude_mode: 'or', q: p.q ?? '', }); if (p.format != null) q.set('format', p.format); if (p.genreInclude != null) q.set('genre_include', p.genreInclude); if (p.genreExclude != null) q.set('genre_exclude', p.genreExclude); return api(`/v1/manga/list?${q}`); } const getGenreList = () => api('/v1/genre/list'); const getFormatList = () => api('/v1/format/list?page=1'); const getAuthorList = (q = '') => api(`/v1/author/list?q=${encodeURIComponent(q)}`); function getMangaNotice(mangaId, p = {}) { const q = new URLSearchParams({ page: p.page ?? 1, page_size: p.pageSize ?? 3 }); return api(`/v1/notice/${mangaId}/list?${q}`); } function getAnnouncements(p = {}) { const q = new URLSearchParams({ page: p.page ?? 1, page_size: p.pageSize ?? 10 }); return api(`/v1/announcement/list?${q}`); } if (require.main === module) { const [,, cmd, ...args] = process.argv; async function run() { switch (cmd) { case 'home': console.log(JSON.stringify(await getMangaList({ pageSize: 10 }), null, 2)); break; case 'trending': console.log(JSON.stringify(await getMangaTop({ filter: args[0] || 'daily' }), null, 2)); break; case 'detail': if (!args[0]) throw new Error('Usage: node index.js detail '); console.log(JSON.stringify(await getMangaDetail(args[0]), null, 2)); break; case 'chapters': if (!args[0]) throw new Error('Usage: node index.js chapters [asc|desc]'); (await getChapterList(args[0], { sortOrder: args[1] || 'desc' })).data .forEach(ch => console.log(`Ch.${String(ch.chapter_number).padEnd(6)} | ${ch.chapter_id} | ${ch.release_date}`)); break; case 'read': { if (!args[0]) throw new Error('Usage: node index.js read '); const r = await getChapterDetail(args[0]); const ch = r.data; console.log(`Chapter : ${ch.chapter_number}`); console.log(`Manga : ${ch.manga_id}`); console.log(`Prev : Ch.${ch.prev_chapter_number ?? '-'} (${ch.prev_chapter_id ?? '-'})`); console.log(`Next : Ch.${ch.next_chapter_number ?? '-'} (${ch.next_chapter_id ?? '-'})`); console.log(`Pages : ${ch.chapter.data.length}\n`); getImageUrls(ch).forEach(url => console.log(url)); break; } case 'search': if (!args[0]) throw new Error('Usage: node index.js search '); console.log(JSON.stringify(await searchManga({ q: args.join(' ') }), null, 2)); break; case 'genres': console.log(JSON.stringify(await getGenreList(), null, 2)); break; case 'formats': console.log(JSON.stringify(await getFormatList(), null, 2)); break; case 'announcements': console.log(JSON.stringify(await getAnnouncements(), null, 2)); break; default: console.log(`Usage: node index.js [args] home trending [daily|weekly|monthly] detail chapters [asc|desc] read search genres formats announcements`); } } run().catch(err => { console.error(err.message); process.exit(1); }); } module.exports = { getMangaList, getMangaTop, getMangaDetail, getChapterList, getChapterDetail, getImageUrls, searchManga, getGenreList, getFormatList, getAuthorList, getMangaNotice, getAnnouncements, BASE_API, ASSETS_BASE, };