NungIndy

เอกสาร API

ดึงรายชื่อซีรีส์แนวตั้งพากย์ไทยและบรรยายไทย พร้อมลิงก์วิดีโอทุกตอน ไปแสดงบนเว็บหรือแอปของคุณ

เริ่มใช้งาน

  1. สมัครสมาชิก แล้วคัดลอก key จากแดชบอร์ด key ขึ้นต้นด้วย rnj_ ระบบแสดงเต็มครั้งเดียว ทำหายออกใหม่ได้ที่แดชบอร์ด
  2. เลือกแพ็กเกจ เพื่อเปิดใช้ key และขอลิงก์วิดีโอได้ ก่อนชำระเงิน key ยังเรียก API ไม่ได้
  3. ดึงรายชื่อเรื่องด้วย /series เก็บลงฐานของคุณ แล้วตามของใหม่เป็นระยะด้วย updated_since
  4. ตอนผู้ชมกดเล่น ให้เซิร์ฟเวอร์ของคุณขอลิงก์ด้วย /play หรือ /links แล้วส่งลิงก์ให้ <video>

ลองเรียกทุก endpoint ได้จากหน้า ทดสอบ API ในแดชบอร์ด การดูรายการที่นั่นไม่นับ req

ส่ง key

ที่อยู่ API คือ https://api.nungindy.com/v1 ทุกคำขอเป็น GET และตอบเป็น JSON (UTF-8) ส่ง key ใน header X-API-Key หรือ Authorization: Bearer ระบบไม่รับ key ใน query string

curl -H "X-API-Key: rnj_…" "https://api.nungindy.com/v1/series?per_page=5"

PHP

$ch = curl_init("https://api.nungindy.com/v1/series?provider=netshort");
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 20,
  CURLOPT_HTTPHEADER => ["X-API-Key: " . getenv("RENTAPI_KEY")]]);
$data = json_decode(curl_exec($ch), true);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);

Node.js

const r = await fetch("https://api.nungindy.com/v1/series?provider=netshort", {
  headers: { "X-API-Key": process.env.RENTAPI_KEY } });
const data = await r.json();
if (!r.ok) throw new Error(data.error);

Python

import os, requests
r = requests.get("https://api.nungindy.com/v1/series", params={"provider": "netshort"},
                 headers={"X-API-Key": os.environ["RENTAPI_KEY"]}, timeout=20)
data = r.json()

เก็บ key ไว้ที่เซิร์ฟเวอร์เท่านั้น ถ้าใส่ในโค้ดหน้าเว็บ ผู้ชมทุกคนจะเห็นและเอาไปใช้แทนคุณได้

เพดานการใช้งาน

ทุกคำขอนับ 1 req เท่ากัน ไม่ว่าจะเป็นรายชื่อเรื่อง รายละเอียด หรือลิงก์วิดีโอ /links ได้ลิงก์ทั้งเรื่องในคำขอเดียว จึงนับแค่ 1 req เหมือนกัน แต่ละ key มีเพดาน 3 อย่างกับวันหมดอายุ

เพดานต่อ keyเริ่มต้น
คำขอต่อนาที
เกินแล้วรอรอบนาทีถัดไป
60 req
คำขอต่อวัน
นับรวมทุกคำขอ ทั้งรายชื่อเรื่อง รายละเอียด และลิงก์วิดีโอ
30,000 req
IP ต่อวัน
จำนวนเครื่องเซิร์ฟเวอร์ที่ใช้ key นี้ได้ในวันเดียว
4 IP

key ใหม่ได้ 60 req/นาที, 30,000 req/วัน และ 4 IP/วัน IP นับจากเครื่องที่เรียก API เมื่อครบจำนวนแล้ว IP ใหม่ได้รหัส 403 ส่วน IP ที่ใช้ไปแล้ววันนี้เรียกต่อได้ตามปกติ ตัวนับรายวันเริ่มใหม่เที่ยงคืนเวลาไทย ต่ออายุหรือซื้อเพิ่มได้ที่หน้าแพ็กเกจ

ใช้ req ให้คุ้ม

  • เก็บรายการเรื่องไว้ในฐานของคุณ อย่าเรียก /series ทุกครั้งที่มีคนเปิดหน้า
  • ขอลิงก์ตอนผู้ชมกดเล่นจริง และใช้ลิงก์เดิมซ้ำได้จนใกล้ expires_at
  • ผู้ชมดูหลายตอนติดกัน ใช้ /links ครั้งเดียวคุ้มกว่าขอ /play ทีละตอน

รหัสตอบกลับ

พารามิเตอร์ความหมาย
200สำเร็จ
400พารามิเตอร์ไม่ถูกต้อง ข้อความใน error บอกว่าตัวไหน
401ไม่มี key, key ผิด หรือ key ถูกปิด
403key หมดอายุ, ใช้ครบจำนวน IP ของวันนี้, key ไม่มีสิทธิ์ขอลิงก์วิดีโอ หรือเรียกจากโดเมนที่ไม่ได้ลงทะเบียน
404ไม่พบเรื่อง ตอน หรือ endpoint
405ใช้ method อื่นที่ไม่ใช่ GET
429เกินเพดานต่อนาทีหรือต่อวัน หรือส่ง key ผิดหลายครั้ง รอตามจำนวนวินาทีใน header Retry-After
503ระบบขัดข้องชั่วคราว ลองใหม่ได้

คำตอบที่ผิดพลาดมีรูปแบบเดียวเสมอ ข้อความเป็นภาษาไทย แสดงให้ทีมของคุณดูได้เลย

{ "error": "เกินเพดาน 30000 req/วัน เริ่มนับใหม่เที่ยงคืนเวลาไทย" }

รอแล้วลองใหม่เมื่อได้ 429 หรือ 503

async function call(path, tries = 3) {
  for (let i = 0; i < tries; i++) {
    const r = await fetch("https://api.nungindy.com/v1" + path, { headers: { "X-API-Key": process.env.RENTAPI_KEY } });
    if (r.status !== 429 && r.status !== 503) return r;
    const wait = Number(r.headers.get("Retry-After")) || 5;
    if (wait > 120) return r;   // เกินเพดานรายวัน รอถึงพรุ่งนี้
    await new Promise(ok => setTimeout(ok, wait * 1000));
  }
}

เรียกจากเบราว์เซอร์

คำขอที่ไม่มี header Origin (เรียกจากเซิร์ฟเวอร์) ใช้ได้เสมอ ถ้าจำเป็นต้องเรียกจากหน้าเว็บโดยตรง แจ้งทีมงานให้ลงทะเบียนโดเมนของคุณกับ key ก่อน โดเมนอื่นได้รหัส 403 วิธีนี้ยังทำให้ key อยู่ในหน้าเว็บ จึงแนะนำให้เรียกผ่านเซิร์ฟเวอร์ของคุณมากกว่า

GET/v1/providers

รายชื่อแอปต้นทางที่มีเรื่องในคลัง พร้อมจำนวนเรื่องและจำนวนตอน ใช้ค่า slug กรองรายการเรื่อง · icon (96×96) / icon_small (48×48) เป็นรูป WebP ของแอป ไม่มีรูป = null

{ "items": [
  { "slug": "dramabox", "name": "DramaBox", "series": 1224,
    "series_complete": 1224, "r2_episodes": 98110,
    "icon": "https://nungindy.com/icons/dramabox-96.webp",
    "icon_small": "https://nungindy.com/icons/dramabox-48.webp" } ] }

GET/v1/genres

หมวดทั้งหมดพร้อมจำนวนเรื่อง ใช้ค่า slug กรองรายการเรื่อง

{ "items": [ { "slug": "romance", "name": "โรแมนติก", "name_en": "Romance", "series": 412 } ] }

GET/v1/series

รายการเรื่องในคลัง แบ่งหน้าได้สองแบบ: page สำหรับแสดงทีละหน้า หรือ after_id สำหรับไล่ทั้งคลัง

พารามิเตอร์ความหมาย
providerเฉพาะแอปนี้ เช่น netshort (slug จาก /providers)
genreslug ของหมวด หมวดที่ไม่มีอยู่ได้ 404
dubdub เฉพาะพากย์ไทย หรือ sub เฉพาะบรรยายไทย
qค้นจากชื่อเรื่อง
ordernew เข้าใหม่ (ค่าเริ่มต้น) · updated อัปเดตล่าสุด · trending ยอดดู 7 วัน · popular ยอดดูรวม
page, per_pageหน้าที่ต้องการ และจำนวนต่อหน้า ค่าเริ่มต้น 50 สูงสุด 100
after_idไล่ทั้งคลังเรียงตาม id เริ่มที่ 0 แล้วส่ง next_after_id ของคำตอบก่อนหน้า ใช้แล้ว order ไม่มีผล
updated_sinceเฉพาะเรื่องที่ข้อมูลหรือไฟล์เปลี่ยนหลังเวลานี้ รูปแบบ ISO 8601 เช่น 2026-11-11T00:00:00Z
complete1 = เฉพาะเรื่องที่มีไฟล์ครบทุกตอน (ตอนนี้คลังมีแต่เรื่องที่ครบแล้ว)

ตัวอย่างคำตอบ

{ "page": 1, "per_page": 50, "total": 94, "total_pages": 2,
  "items": [ {
    "id": 594349, "title": "ตื่นจากรักร้ายจุติเทพ", "description": "…",
    "locale": "th", "provider": "netshort", "platform": "NetShort", "is_thai_dub": true,
    "episodes": 52, "r2_episodes": 52, "r2_complete": true,
    "codec": "h264", "codec_string": "avc1.64001F",
    "cover": { "small": "https://…-300.webp", "large": "https://…-600.webp" },
    "genres": [ { "slug": "romance", "name": "โรแมนติก", "name_en": "Romance" } ],
    "views_7d": 12, "views_total": 340,
    "created_at": "2026-11-10T03:12:00.000Z", "updated_at": "2026-11-11T01:02:00.000Z",
    "source_updated_at": "2026-11-09T22:40:00.000Z" } ] }

แบบ after_id คำตอบมีแค่ items, per_page และ next_after_id (ไม่มี total) คำตอบเดียวกันถูกเก็บไว้ 60 วินาที

GET/v1/series/:id

รายละเอียดของเรื่องเดียว ทุก field เหมือนในรายการ เพิ่มรายการตอน

{ "id": 594349, "title": "…", "episodes": 52, "r2_episodes": 52, …,
  "episode_list": [
    { "number": 1, "duration_sec": 94, "in_r2": true } ] }

in_r2 บอกว่าตอนนั้นมีไฟล์วิดีโอแล้ว duration_sec เป็น null เมื่อไม่ทราบความยาว

field ของเรื่อง

fieldชนิดความหมาย
idตัวเลขรหัสเรื่อง ไม่เปลี่ยน ใช้เป็นคีย์ในฐานของคุณ
title, descriptionข้อความชื่อและเรื่องย่อภาษาไทย description อาจเป็นข้อความว่าง
localeข้อความth
providerข้อความslug ของแอปต้นทาง
platformข้อความชื่อแอปสำหรับแสดงผล
is_thai_dubtrue/falsetrue = พากย์ไทย, false = เสียงต้นฉบับ บรรยายไทย
episodesตัวเลขจำนวนตอนของเรื่อง
r2_episodesตัวเลขจำนวนตอนที่มีไฟล์วิดีโอ
r2_completetrue/falseมีไฟล์ครบทุกตอน
codech264 / h265 / nullรหัสวิดีโอของเรื่อง ทุกตอนใช้รหัสเดียวกัน null = ระบบยังตรวจไฟล์ไม่เสร็จ (เรื่องที่เพิ่งเข้าคลัง)
codec_stringstring / nullค่าสำหรับ canPlayType เช่น avc1.64001F, hvc1.1.6.L120.90
cover.small, cover.largeURLปก webp กว้าง 300 และ 600 px สัดส่วน 3:4 เปิดจากเว็บของคุณได้ตรง ๆ
genresรายการslug, name, name_en
views_7d, views_totalตัวเลขยอดดู 7 วันล่าสุด และยอดดูรวม
created_atเวลาเวลาที่เรื่องเข้าคลัง
updated_atเวลาเวลาที่ข้อมูลหรือไฟล์ในคลังเปลี่ยนล่าสุด (ใช้กับ updated_since)
source_updated_atเวลาเวลาที่ต้นทางอัปเดตเรื่องล่าสุด

เวลาทุกตัวเป็น ISO 8601 เขตเวลา UTC เช่น 2026-11-11T01:02:00.000Z

GET/v1/play/:id/:ep

ลิงก์วิดีโอของตอนเดียว เหมาะกับการขอตอนผู้ชมกดเล่น นับ 1 req

{ "series_id": 594349, "episode": 1, "storage": "r2",
  "codec": "h264", "codec_string": "avc1.64001F",
  "parts": [ { "src": "https://…/1.mp4?X-Amz-…", "type": "mp4", "codec": "h264", "duration_sec": null, "proxied": false } ],
  "subtitles": [ { "lang": "th", "src": "https://…/vtt/594349-1-th.vtt?s=…" } ],
  "source": "r2", "sources": 1,
  "expires_hint_sec": 21600, "expires_at": "2026-11-11T08:30:00.000Z" }

ทุกตอนมีไฟล์เดียว ใช้ parts[0].src ได้เลย ตอนที่ยังไม่มีไฟล์ได้ 404

นำลิงก์ไปเล่น

ลิงก์เป็นไฟล์ MP4 ธรรมดา (เปิดเล่นได้ทันที ไม่ต้องรอโหลดจบ และกรอได้) ใส่ใน <video> ได้เลย ไม่ต้องใช้ hls.js ภาพเป็นแนวตั้ง 9:16

<video controls playsinline preload="metadata" src="ลิงก์จาก src"
       style="aspect-ratio: 9 / 16; width: 100%; max-height: 90vh; background: #000">
  <track kind="subtitles" srclang="th" label="ไทย" src="ลิงก์ซับ" default>
</video>

ลิงก์วิดีโอใช้ได้กับทุกคนที่ได้ลิงก์ไปจนกว่าจะหมดอายุ ถ้าไม่อยากให้ลิงก์หลุด ให้ขอจากเซิร์ฟเวอร์ตอนผู้ชมกดเล่น แล้วส่งให้เฉพาะผู้ชมคนนั้น

ตัวอย่าง: เซิร์ฟเวอร์ของคุณส่งลิงก์ให้ผู้ชม

// Express — ผู้ชมกดเล่น → หน้าเว็บขอ /watch/594349/1 ของคุณ
app.get("/watch/:id/:ep", async (req, res) => {
  const r = await fetch(`https://api.nungindy.com/v1/play/${+req.params.id}/${+req.params.ep}`,
    { headers: { "X-API-Key": process.env.RENTAPI_KEY } });
  const d = await r.json();
  if (!r.ok) return res.status(r.status).json({ error: d.error });
  res.json({ src: d.parts[0].src, subtitles: d.subtitles, expires_at: d.expires_at });
});

H.264 และ H.265

ไฟล์ส่วนใหญ่เป็น H.264 ซึ่งเล่นได้ทุกเบราว์เซอร์ แต่บางแอปส่งมาเป็น H.265 (HEVC) ซึ่งเครื่องเก่าบางรุ่นเล่นไม่ได้ ภาพจะดำหรือมีแต่เสียง

แอป (provider)H.265
Melolo meloloทุกเรื่อง
PineDrama pinedramaทุกเรื่อง
ShortWave shortwaveบางเรื่อง

แอปอื่นทั้งหมดเป็น H.264 เบราว์เซอร์ที่เล่น H.265 ได้: Safari บน iPhone/iPad/Mac, Chrome และ Edge รุ่นใหม่บน Windows/Mac/Android ที่เครื่องถอดรหัส HEVC ได้ ส่วน Firefox และเครื่องรุ่นเก่ารองรับไม่แน่นอน ขึ้นกับเครื่อง

ดูจาก field codec

ทุกเรื่องมี codec (h264 หรือ h265) และ codec_string ทั้งใน /series, /series/:id, /links และ /play ระบบตรวจจากไฟล์จริงใน R2 เรื่องที่เพิ่งเข้าคลังอาจเป็น null อยู่ไม่กี่นาที

// ในเบราว์เซอร์ของผู้ชม
const v = document.createElement("video");
function playable(s) {
  if (!s.codec_string) return true;   // ยังไม่รู้ ลองเล่นไปก่อน
  return v.canPlayType('video/mp4; codecs="' + s.codec_string + '"') !== "";
}
// playable(series) เป็น false → ซ่อนเรื่องนั้น
// หรือขึ้นข้อความแนะนำให้เปิดด้วย Safari หรือ Chrome รุ่นใหม่

ถ้ากรองฝั่งเซิร์ฟเวอร์ ใช้ codec === "h265" แยกเรื่องที่เครื่องเก่าอาจเล่นไม่ได้

ถ้าเล่นแล้วเกิด error ที่ <video> (เช่น MEDIA_ERR_SRC_NOT_SUPPORTED) ให้แสดงข้อความเดียวกันแทนจอดำ

ซับไทย

เรื่องที่ซับไทยฝังมาในภาพแล้วจะไม่มีไฟล์ซับแยก ส่วนเรื่องที่มีซับแยก ลิงก์ไฟล์ WebVTT อยู่ใน subtitles ลิงก์ซับไม่หมดอายุ และเปิดจากโดเมนอื่นได้ (ส่ง Access-Control-Allow-Origin: *) จึงใส่ใน <track> ได้ตรง ๆ subtitles เป็นรายการว่างเมื่อไม่มีซับแยก

GET/v1/ping

ตรวจว่า key ใช้ได้ ดูเพดาน วันหมดอายุ จำนวนคำขอและ IP ที่ใช้วันนี้ นับ 1 req

{ "ok": true, "ip": "203.0.113.7",
  "key": { "name": "เว็บของคุณ", "prefix": "rnj_ab12", "allow_links": true, "allow_r2": true,
           "r2_ttl_hours": 6, "per_min": 60, "per_day": 30000, "ips_per_day": 4,
           "expires_at": "2026-12-12T16:59:00.000Z" },
  "remaining": { "requests": 29880,
    "usedToday": { "catalog": 100, "play": 20, "total": 120 },
    "ips": { "max": 4, "used": 1, "list": [ { "ip": "203.0.113.7", "calls": 120 } ] },
    "expires_at": "2026-12-12T16:59:00.000Z" },
  "time": "2026-11-12T08:00:00.000Z" }

ค่า null หมายถึงไม่จำกัด allow_links เป็น false = key ดูรายการได้อย่างเดียว ขอลิงก์วิดีโอไม่ได้

ดึงทั้งคลังแล้วตามของใหม่

ครั้งแรก ไล่ทั้งคลังด้วย after_id จนได้ next_after_id เป็น null

GET https://api.nungindy.com/v1/series?after_id=0&per_page=100
GET https://api.nungindy.com/v1/series?after_id=576873&per_page=100   ← ใช้ next_after_id ของรอบก่อน

รอบถัดไป จดเวลาที่เริ่มดึงรอบก่อนไว้ แล้วขอเฉพาะเรื่องที่เปลี่ยนหลังเวลานั้น เรื่องใหม่และเรื่องที่ข้อมูลเปลี่ยนจะติดมาด้วย

GET https://api.nungindy.com/v1/series?updated_since=2026-11-11T00:00:00Z&per_page=100&page=1

เรื่องเข้าคลังเมื่อมีไฟล์ครบทุกตอน คลังอัปเดตทุก 5 นาที ดึงถี่กว่านั้นไม่ได้ข้อมูลใหม่ ดึงชั่วโมงละครั้งก็เพียงพอ เรื่องที่ถูกนำออกจากคลังจะหายจากรายการ และ /series/:id ได้ 404 ให้ซ่อนเรื่องนั้นในเว็บของคุณ

ความเข้ากันได้

ชื่อและความหมายของ field ใน /v1 จะไม่เปลี่ยน ถ้าจำเป็นต้องเปลี่ยนจะออกเป็น /v2 แยก ระหว่างนี้อาจมี field ใหม่เพิ่มเข้ามา โค้ดของคุณควรข้าม field ที่ไม่รู้จัก สารบัญแบบ JSON อยู่ที่ https://api.nungindy.com/v1